package de.assecutor.votianlt.messaging.plugin.mqtt;

import com.hivemq.client.mqtt.MqttClient;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish;
import de.assecutor.votianlt.messaging.plugin.*;
import de.assecutor.votianlt.messaging.plugin.ConnectionStateEvent.ConnectionState;
import lombok.extern.slf4j.Slf4j;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

/**
 * MQTT implementation of the MessagingPlugin interface.
 * Uses HiveMQ MQTT 5 client for communication.
 *
 * Topic Structure (managed internally):
 * - Server -> Client: /client/{clientId}/{messageType}
 * - Client -> Server: /server/{clientId}/{messageType}
 * - ACK Server -> Client: /client/{clientId}/ack (messageId in payload)
 * - ACK Client -> Server: /server/{clientId}/ack (messageId in payload)
 */
@Slf4j
public class MqttMessagingPlugin implements MessagingPlugin {

    private static final String PLUGIN_NAME = "mqtt";
    private static final String PLUGIN_VERSION = "2.0.0";

    // Topic templates
    private static final String TOPIC_TO_CLIENT = "/client/%s/%s";           // /client/{clientId}/{messageType}
    private static final String TOPIC_ACK_TO_CLIENT = "/client/%s/ack";      // /client/{clientId}/ack (messageId in payload)

    // Subscription patterns
    private static final String PATTERN_FROM_CLIENT = "/server/+/%s";       // /server/+/{messageType}
    private static final String PATTERN_ACK_FROM_CLIENT = "/server/+/ack";  // /server/+/ack

    private Mqtt5AsyncClient mqttClient;
    private ConnectionStateListener connectionListener;
    private final Map<String, ClientMessageHandler> messageHandlers = new ConcurrentHashMap<>();
    private AckHandler ackHandler;
    private volatile boolean connected = false;

    // Configuration keys
    private static final String CONFIG_BROKER_HOST = "broker.host";
    private static final String CONFIG_BROKER_PORT = "broker.port";
    private static final String CONFIG_USERNAME = "username";
    private static final String CONFIG_PASSWORD = "password";
    private static final String CONFIG_CLIENT_ID = "client.id";
    private static final String CONFIG_CLEAN_START = "clean.start";
    private static final String CONFIG_CONNECTION_TIMEOUT = "connection.timeout.seconds";
    private static final String CONFIG_KEEP_ALIVE = "keep.alive.seconds";

    @Override
    public void init(PluginConfig config) throws PluginException {
        log.info("[MqttPlugin] Initializing MQTT plugin");

        try {
            notifyConnectionState(ConnectionState.INITIALIZING, null);

            // Extract configuration
            String brokerHost = config.getString(CONFIG_BROKER_HOST, "localhost");
            int brokerPort = config.getInt(CONFIG_BROKER_PORT, 1883);
            String username = config.getString(CONFIG_USERNAME);
            String password = config.getString(CONFIG_PASSWORD);
            String clientId = config.getString(CONFIG_CLIENT_ID, "votianlt-" + UUID.randomUUID());
            boolean cleanStart = config.getBoolean(CONFIG_CLEAN_START, true);
            int connectionTimeout = config.getInt(CONFIG_CONNECTION_TIMEOUT, 60);
            int keepAlive = config.getInt(CONFIG_KEEP_ALIVE, 60);

            log.info("[MqttPlugin] Connecting to {}:{} with clientId: {} (timeout: {}s, keepAlive: {}s)",
                    brokerHost, brokerPort, clientId, connectionTimeout, keepAlive);

            // Build MQTT client
            var clientBuilder = MqttClient.builder()
                    .useMqttVersion5()
                    .identifier(clientId)
                    .serverHost(brokerHost)
                    .serverPort(brokerPort)
                    .automaticReconnect()
                        .initialDelay(1, java.util.concurrent.TimeUnit.SECONDS)
                        .maxDelay(30, java.util.concurrent.TimeUnit.SECONDS)
                        .applyAutomaticReconnect();

            mqttClient = clientBuilder.buildAsync();

            // Build connect options
            var connectBuilder = com.hivemq.client.mqtt.mqtt5.message.connect.Mqtt5Connect.builder()
                    .cleanStart(cleanStart)
                    .keepAlive(keepAlive);

            if (username != null && password != null) {
                connectBuilder.simpleAuth()
                        .username(username)
                        .password(password.getBytes(StandardCharsets.UTF_8))
                        .applySimpleAuth();
            }

            // Connect asynchronously
            notifyConnectionState(ConnectionState.CONNECTING, null);

            log.info("[MqttPlugin] Starting async connection to {}:{}", brokerHost, brokerPort);

            mqttClient.connect(connectBuilder.build())
                    .orTimeout(connectionTimeout, java.util.concurrent.TimeUnit.SECONDS)
                    .whenComplete((connAck, throwable) -> {
                        if (throwable != null) {
                            String errorMsg = String.format("Connection to %s:%d failed: %s",
                                    brokerHost, brokerPort, throwable.getMessage());
                            log.error("[MqttPlugin] {}", errorMsg, throwable);

                            // Check for specific error types
                            if (throwable instanceof java.util.concurrent.TimeoutException) {
                                log.error("[MqttPlugin] Connection timeout - broker may be unreachable or firewall blocking connection");
                            } else if (throwable.getCause() instanceof java.net.UnknownHostException) {
                                log.error("[MqttPlugin] Unknown host - DNS resolution failed for {}", brokerHost);
                            } else if (throwable.getCause() instanceof java.net.ConnectException) {
                                log.error("[MqttPlugin] Connection refused - broker may be down or port {} is blocked", brokerPort);
                            }

                            connected = false;
                            notifyConnectionState(ConnectionState.ERROR, errorMsg);
                        } else {
                            log.info("[MqttPlugin] Connected successfully - connAck: {}", connAck);
                            connected = true;
                            setupGlobalMessageHandler();
                            log.info("[MqttPlugin] Notifying CONNECTED state");
                            notifyConnectionState(ConnectionState.CONNECTED, null);
                            log.info("[MqttPlugin] CONNECTED state notification sent");
                        }
                    });

            log.info("[MqttPlugin] Initialization complete - connection in progress");

        } catch (Exception e) {
            log.error("[MqttPlugin] Initialization failed: {}", e.getMessage(), e);
            throw new PluginException("Failed to initialize MQTT plugin", e);
        }
    }

    @Override
    public void exit() throws PluginException {
        log.info("[MqttPlugin] Shutting down MQTT plugin");

        try {
            notifyConnectionState(ConnectionState.DISCONNECTING, null);

            if (mqttClient != null) {
                // Check actual client connection state, not just our flag
                var clientState = mqttClient.getState();
                if (clientState.isConnected()) {
                    try {
                        mqttClient.disconnect().join();
                        log.info("[MqttPlugin] Disconnected successfully");
                    } catch (Exception disconnectEx) {
                        // Log but don't throw - client may already be disconnected
                        log.warn("[MqttPlugin] Disconnect failed (client may already be disconnected): {}",
                                disconnectEx.getMessage());
                    }
                } else {
                    log.info("[MqttPlugin] Client already disconnected (state: {})", clientState);
                }
            }

            connected = false;
            messageHandlers.clear();
            ackHandler = null;
            notifyConnectionState(ConnectionState.DISCONNECTED, null);

        } catch (Exception e) {
            log.error("[MqttPlugin] Shutdown failed: {}", e.getMessage(), e);
            // Don't throw on shutdown - just log the error
            connected = false;
            messageHandlers.clear();
            ackHandler = null;
        }
    }

    @Override
    public void setConnectionListener(ConnectionStateListener listener) {
        this.connectionListener = listener;
    }

    @Override
    public CompletableFuture<Void> sendToClient(String clientId, String messageType, byte[] payload, SendOptions options) throws PluginException {
        if (!connected) {
            return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected"));
        }

        String topic = String.format(TOPIC_TO_CLIENT, clientId, messageType);
        log.debug("[MqttPlugin] Sending to client {} (type: {}) on topic: {}", clientId, messageType, topic);

        return sendToTopic(topic, payload, options);
    }

    @Override
    public CompletableFuture<Void> sendAckToClient(String clientId, String messageId, byte[] payload, SendOptions options) throws PluginException {
        if (!connected) {
            return CompletableFuture.failedFuture(new PluginException("MQTT client is not connected"));
        }

        String topic = String.format(TOPIC_ACK_TO_CLIENT, clientId);
        log.debug("[MqttPlugin] Sending ACK to client {} for message {} on topic: {}", clientId, messageId, topic);

        return sendToTopic(topic, payload, options);
    }

    @Override
    public void registerMessageHandler(String messageType, ClientMessageHandler handler) throws PluginException {
        if (!connected) {
            throw new PluginException("MQTT client is not connected");
        }

        messageHandlers.put(messageType, handler);

        // Special case for login: subscribe to /server/login (without clientId)
        if ("login".equals(messageType)) {
            String loginTopic = "/server/login";
            log.info("[MqttPlugin] Registering handler for message type '{}' with topic: {}", messageType, loginTopic);

            mqttClient.subscribeWith()
                    .topicFilter(loginTopic)
                    .qos(MqttQos.EXACTLY_ONCE)
                    .send()
                    .whenComplete((subAck, throwable) -> {
                        if (throwable != null) {
                            log.error("[MqttPlugin] Subscription to {} failed: {}", loginTopic, throwable.getMessage());
                            messageHandlers.remove(messageType);
                        } else {
                            log.info("[MqttPlugin] Successfully subscribed to: {}", loginTopic);
                        }
                    });
        } else {
            // Standard pattern: /server/+/{messageType}
            String topicPattern = String.format(PATTERN_FROM_CLIENT, messageType);
            log.info("[MqttPlugin] Registering handler for message type '{}' with pattern: {}", messageType, topicPattern);

            mqttClient.subscribeWith()
                    .topicFilter(topicPattern)
                    .qos(MqttQos.EXACTLY_ONCE)
                    .send()
                    .whenComplete((subAck, throwable) -> {
                        if (throwable != null) {
                            log.error("[MqttPlugin] Subscription to {} failed: {}", topicPattern, throwable.getMessage());
                            messageHandlers.remove(messageType);
                        } else {
                            log.info("[MqttPlugin] Successfully subscribed to: {}", topicPattern);
                        }
                    });
        }
    }

    @Override
    public void registerAckHandler(AckHandler handler) throws PluginException {
        if (!connected) {
            throw new PluginException("MQTT client is not connected");
        }

        log.info("[MqttPlugin] Registering ACK handler with pattern: {}", PATTERN_ACK_FROM_CLIENT);

        this.ackHandler = handler;

        // Subscribe to ACK topic pattern
        mqttClient.subscribeWith()
                .topicFilter(PATTERN_ACK_FROM_CLIENT)
                .qos(MqttQos.EXACTLY_ONCE)
                .send()
                .whenComplete((subAck, throwable) -> {
                    if (throwable != null) {
                        log.error("[MqttPlugin] Subscription to {} failed: {}", PATTERN_ACK_FROM_CLIENT, throwable.getMessage());
                        this.ackHandler = null;
                    } else {
                        log.info("[MqttPlugin] Successfully subscribed to: {}", PATTERN_ACK_FROM_CLIENT);
                    }
                });
    }

    @Override
    public boolean isConnected() {
        return connected;
    }

    @Override
    public String getPluginName() {
        return PLUGIN_NAME;
    }

    @Override
    public String getPluginVersion() {
        return PLUGIN_VERSION;
    }

    @Override
    public PluginMetadata getMetadata() {
        return PluginMetadata.builder()
                .name(PLUGIN_NAME)
                .version(PLUGIN_VERSION)
                .description("MQTT v5 messaging plugin using HiveMQ client")
                .supportsWildcards(true)
                .supportsRetainedMessages(true)
                .supportsQos(true)
                .maxQosLevel(2)
                .build();
    }

    /**
     * Setup global message handler to route incoming messages to registered handlers.
     */
    private void setupGlobalMessageHandler() {
        mqttClient.publishes(com.hivemq.client.mqtt.MqttGlobalPublishFilter.ALL, publish -> {
            handleIncomingMessage(publish);
        });

        log.info("[MqttPlugin] Global message handler configured");
    }

    /**
     * Handle incoming MQTT message and route to appropriate handler.
     */
    private void handleIncomingMessage(Mqtt5Publish publish) {
        String topic = publish.getTopic().toString();
        byte[] payload = publish.getPayloadAsBytes();

        log.debug("[MqttPlugin] Received message on topic: {}", topic);

        try {
            // Check if it's an ACK message (topic ends with /ack)
            if (topic.startsWith("/server/") && topic.endsWith("/ack")) {
                handleAckMessage(topic, payload);
            }
            // Check if it's a client message
            else if (topic.startsWith("/server/")) {
                handleClientMessage(topic, payload);
            }
            else {
                log.warn("[MqttPlugin] Received message on unexpected topic: {}", topic);
            }
        } catch (Exception e) {
            log.error("[MqttPlugin] Error handling message on topic {}: {}", topic, e.getMessage(), e);
        }
    }




    /**
     * Handle ACK message from client.
     * Topic format: /server/{clientId}/ack (messageId in payload)
     */
    private void handleAckMessage(String topic, byte[] payload) {
        if (ackHandler == null) {
            log.warn("[MqttPlugin] Received ACK but no handler registered: {}", topic);
            return;
        }

        // Extract clientId from topic: /server/{clientId}/ack
        String[] parts = topic.split("/");
        if (parts.length >= 4) {
            String clientId = parts[2];  // clientId is at index 2

            // Extract messageId from payload
            String payloadStr = new String(payload, StandardCharsets.UTF_8);
            String messageId = extractMessageIdFromPayload(payloadStr);

            if (messageId != null) {
                log.debug("[MqttPlugin] Routing ACK for message: {} from client: {}", messageId, clientId);
                ackHandler.onAckReceived(messageId, payload);
            } else {
                log.warn("[MqttPlugin] Could not extract messageId from ACK payload: {}", payloadStr);
            }
        } else {
            log.warn("[MqttPlugin] Invalid ACK topic format: {}", topic);
        }
    }

    /**
     * Extract messageId from ACK payload.
     * Expected payload format: JSON with "messageId" field, e.g., {"messageId": "abc-123"}
     * or plain messageId string.
     */
    private String extractMessageIdFromPayload(String payload) {
        if (payload == null || payload.isBlank()) {
            return null;
        }

        payload = payload.trim();

        // Try to extract from JSON format: {"messageId": "..."}
        if (payload.startsWith("{")) {
            // Simple JSON parsing for messageId field
            int keyIndex = payload.indexOf("\"messageId\"");
            if (keyIndex == -1) {
                keyIndex = payload.indexOf("'messageId'");
            }
            if (keyIndex >= 0) {
                int colonIndex = payload.indexOf(":", keyIndex);
                if (colonIndex >= 0) {
                    int valueStart = payload.indexOf("\"", colonIndex);
                    if (valueStart == -1) {
                        valueStart = payload.indexOf("'", colonIndex);
                    }
                    if (valueStart >= 0) {
                        char quote = payload.charAt(valueStart);
                        int valueEnd = payload.indexOf(quote, valueStart + 1);
                        if (valueEnd > valueStart) {
                            return payload.substring(valueStart + 1, valueEnd);
                        }
                    }
                }
            }
        }

        // If not JSON, treat the entire payload as the messageId
        return payload;
    }

    /**
     * Handle client message.
     * Topic format: /server/{clientId}/{messageType} or /server/{messageType} (for login)
     * messageType can contain slashes, e.g., "jobs/assigned"
     */
    private void handleClientMessage(String topic, byte[] payload) {
        // Extract clientId and messageType from topic
        String[] parts = topic.split("/");

        // Handle /server/login (without clientId)
        if (parts.length == 3 && "login".equals(parts[2])) {
            String messageType = parts[2];
            ClientMessageHandler handler = messageHandlers.get(messageType);
            if (handler != null) {
                log.debug("[MqttPlugin] Routing login message (type: {})", messageType);
                handler.onMessageReceived(null, payload);
            } else {
                log.warn("[MqttPlugin] No handler registered for message type: {}", messageType);
            }
            return;
        }

        // Handle /server/{clientId}/{messageType} where messageType can contain slashes
        if (parts.length >= 4) {
            String clientId = parts[2];
            // Join all parts from index 3 onwards to form the full messageType
            // e.g., /server/clientId/jobs/assigned -> messageType = "jobs/assigned"
            String messageType = String.join("/", java.util.Arrays.copyOfRange(parts, 3, parts.length));

            ClientMessageHandler handler = messageHandlers.get(messageType);
            if (handler != null) {
                log.debug("[MqttPlugin] Routing message from client {} (type: {})", clientId, messageType);
                handler.onMessageReceived(clientId, payload);
            } else {
                log.warn("[MqttPlugin] No handler registered for message type: {}", messageType);
            }
        } else {
            log.warn("[MqttPlugin] Invalid client message topic format: {}", topic);
        }
    }

    /**
     * Send message to a specific MQTT topic.
     */
    private CompletableFuture<Void> sendToTopic(String topic, byte[] payload, SendOptions options) {
        try {
            var publishBuilder = Mqtt5Publish.builder()
                    .topic(topic)
                    .payload(payload)
                    .qos(mapQos(options.getQos()))
                    .retain(options.isRetained());

            return mqttClient.publish(publishBuilder.build())
                    .thenApply(publishResult -> {
                        log.debug("[MqttPlugin] Message published to topic: {}", topic);
                        return null;
                    });
        } catch (Exception e) {
            log.error("[MqttPlugin] Failed to publish to topic {}: {}", topic, e.getMessage(), e);
            return CompletableFuture.failedFuture(new PluginException("Failed to publish message", e));
        }
    }

    /**
     * Map QoS level to MQTT QoS.
     */
    private MqttQos mapQos(int qos) {
        return switch (qos) {
            case 0 -> MqttQos.AT_MOST_ONCE;
            case 1 -> MqttQos.AT_LEAST_ONCE;
            case 2 -> MqttQos.EXACTLY_ONCE;
            default -> MqttQos.AT_LEAST_ONCE;
        };
    }

    /**
     * Notify connection state listener.
     */
    private void notifyConnectionState(ConnectionState state, String message) {
        log.debug("[MqttPlugin] notifyConnectionState called: state={}, listener={}", state, connectionListener != null ? "present" : "null");
        if (connectionListener != null) {
            ConnectionStateEvent event = ConnectionStateEvent.builder()
                    .state(state)
                    .previousState(null)
                    .errorMessage(message)
                    .pluginName(PLUGIN_NAME)
                    .build();
            try {
                log.debug("[MqttPlugin] Calling connectionListener.onConnectionStateChanged");
                connectionListener.onConnectionStateChanged(event);
                log.debug("[MqttPlugin] connectionListener.onConnectionStateChanged completed");
            } catch (Exception e) {
                log.error("[MqttPlugin] Error in connection listener: {}", e.getMessage(), e);
            }
        } else {
            log.warn("[MqttPlugin] Connection listener is null, cannot notify state: {}", state);
        }
    }
}
