package vassistent.service;

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import vassistent.model.AppState;
import vassistent.util.Logger;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

/**
 * MQTT client wrapper that manages connection state, topic listeners, and callbacks.
 */
public class MqttClientService implements MqttCallback {

    private AppState appState;
    private static final String DEFAULT_BROKER_URL = "tcp://localhost:1883";
    private static final String DEFAULT_CLIENT_ID = "JavaClientPublisherSubscriber";
    private final String brokerUrl;
    private final String clientId;

    private final Map<String, Consumer<String>> topicListeners =
            new ConcurrentHashMap<>();

    private MqttClient client;

    /**
     * Creates and connects an MQTT client for the application.
     *
     * @param appState shared state updated with connection status
     */
    public MqttClientService(AppState appState) {
        this(
                appState,
                DEFAULT_BROKER_URL,
                DEFAULT_CLIENT_ID,
                null,
                true
        );
    }

    /**
     * Creates and connects an MQTT client with configurable broker settings.
     *
     * @param appState shared state updated with connection status
     * @param brokerUrl MQTT broker URL (for example {@code tcp://localhost:1883})
     * @param clientId MQTT client identifier
     */
    public MqttClientService(AppState appState, String brokerUrl, String clientId) {
        this(
                appState,
                brokerUrl,
                clientId,
                null,
                true
        );
    }

    /**
     * Creates an MQTT service with an optionally injected client (primarily for tests).
     *
     * @param appState shared state updated with connection status
     * @param injectedClient externally provided MQTT client, or {@code null} to create a default client
     * @param connectOnStart whether the service should connect immediately
     */
    MqttClientService(AppState appState, MqttClient injectedClient, boolean connectOnStart) {
        this(
                appState,
                DEFAULT_BROKER_URL,
                DEFAULT_CLIENT_ID,
                injectedClient,
                connectOnStart
        );
    }

    /**
     * Creates an MQTT service with configurable broker settings and optional injected client.
     *
     * @param appState shared state updated with connection status
     * @param brokerUrl MQTT broker URL
     * @param clientId MQTT client identifier
     * @param injectedClient externally provided MQTT client, or {@code null} to create a default client
     * @param connectOnStart whether the service should connect immediately
     */
    MqttClientService(
            AppState appState,
            String brokerUrl,
            String clientId,
            MqttClient injectedClient,
            boolean connectOnStart
    ) {
        this.appState = appState;
        this.brokerUrl = brokerUrl;
        this.clientId = clientId;

        try {
            client = injectedClient != null
                    ? injectedClient
                    : new MqttClient(
                    brokerUrl,
                    clientId,
                    new MemoryPersistence()
            );

            client.setCallback(this);

            if (connectOnStart) {
                connectClient();
            }

        } catch (MqttException e) {
            Logger.error("MQTT", "Failed to connect", e);
            appState.setMqttConnected(false);
        }
    }

    /**
     * Connects the configured MQTT client with default options.
     *
     * @throws MqttException if connection fails
     */
    private void connectClient() throws MqttException {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setCleanSession(true);
        options.setAutomaticReconnect(true);

        Logger.info("MQTT", "Connecting to broker " + brokerUrl);
        client.connect(options);
        appState.setMqttConnected(true);
        Logger.info("MQTT", "Connection established");
    }

    /**
     * Disconnects the MQTT client when connected.
     */
    public void disconnect() {
        try {
            if (client != null && client.isConnected()) {
                client.disconnect();
                Logger.info("MQTT", "Connection closed");
            }
        } catch (MqttException e) {
            Logger.error("MQTT", "Failed to disconnect", e);
        }
    }

    /**
     * Subscribes to a topic and registers a payload listener.
     *
     * @param topic MQTT topic
     * @param listener callback receiving payload strings
     */
    public void subscribe(String topic, Consumer<String> listener) {
        topicListeners.put(topic, listener);
        try {
            client.subscribe(topic);
            Logger.info("MQTT", "Subscribed to topic: " + topic);
        } catch (MqttException e) {
            Logger.error("MQTT", "Subscribe failed: " + topic, e);
        }
    }

    /**
     * Publishes a message to a topic.
     *
     * @param topic MQTT topic
     * @param message payload body
     * @param qos MQTT quality-of-service level
     */
    public void publish(String topic, String message, int qos) {
        try {
            MqttMessage mqttMessage =
                    new MqttMessage(message.getBytes(StandardCharsets.UTF_8));
            mqttMessage.setQos(qos);

            client.publish(topic, mqttMessage);

            Logger.debug(
                    "MQTT",
                    "Publish -> Topic=" + topic + ", QoS=" + qos + ", Payload=" + message
            );
        } catch (MqttException e) {
            Logger.error("MQTT", "Publish failed", e);
        }
    }

    /**
     * MQTT callback invoked when the broker connection is lost.
     *
     * @param cause cause of the connection loss
     */
    @Override
    public void connectionLost(Throwable cause) {
        Logger.warn(
                "MQTT",
                "Connection lost: " + cause.getMessage()
        );

        appState.setMqttConnected(false);
    }

    /**
     * MQTT callback invoked for each incoming message.
     *
     * @param topic topic of the received message
     * @param message received MQTT message
     * @throws Exception propagated by callback handling if listener execution fails
     */
    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        String payload =
                new String(message.getPayload(), StandardCharsets.UTF_8);

        //Logger.debug(
        //        "MQTT",
        //        "Message received -> Topic=" + topic + ", Payload=" + payload
        //);

        Consumer<String> listener = topicListeners.get(topic);
        if (listener != null) {
            listener.accept(payload);
            Logger.debug("MQTT", "Payload accepted");
        } else {
            Logger.warn(
                    "MQTT",
                    "No listener for topic: " + topic
            );
        }
    }

    /**
     * MQTT callback invoked when a published message is fully delivered.
     *
     * @param token delivery token associated with the completed publish
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        Logger.debug(
                "MQTT",
                "Delivery complete, MessageId=" + token.getMessageId()
        );
    }


}
