package de.assecutor.votianlt.mqtt;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import de.assecutor.votianlt.config.MqttProperties;
import de.assecutor.votianlt.controller.MessageController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Service;
import org.springframework.context.annotation.Lazy;

import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;

/**
 * Manages a single MQTT v5 client connection with Spring lifecycle using HiveMQ
 * MQTT Client.
 */
@Service
@Slf4j
public class MqttV5ClientManager implements SmartLifecycle {

    private final MqttProperties props;
    private final MessageController messageController;
    private final ObjectMapper objectMapper = new ObjectMapper();

    private volatile boolean running = false;
    private Mqtt5AsyncClient client;

    public MqttV5ClientManager(MqttProperties props, @Lazy MessageController messageController) {
        this.props = props;
        this.messageController = messageController;
    }

    @Override
    public void start() {
        if (!props.isEnabled()) {
            log.warn("MQTT is disabled via app.mqtt.enabled=false");
            return;
        }
        try {
            String clientId = buildClientId(props.getClientId());
            URI uri = URI.create(props.getBrokerUri());
            String host = uri.getHost();
            int port = 42099;

            var builder = Mqtt5Client.builder().identifier(clientId).serverHost(host).serverPort(port);
            if (props.isAutomaticReconnect()) {
                builder = builder.automaticReconnectWithDefaultConfig();
            }
            client = builder.buildAsync();

            var connect = client.connectWith().cleanStart(props.isCleanStart()).keepAlive(props.getKeepAlive())
                    .sessionExpiryInterval(props.getSessionExpiryInterval()).simpleAuth()
                    .username("app")
                    .password("apppwd".getBytes(StandardCharsets.UTF_8)).applySimpleAuth();

            log.info("[MQTT] Connecting to {} with clientId={} ...", props.getBrokerUri(), clientId);
            connect.send().join();
            log.info("[MQTT] Connected");

            // Handle all incoming publishes
            client.publishes(MqttGlobalPublishFilter.ALL, publish -> {
                String topic = publish.getTopic().toString();
                byte[] bytes;
                try {
                    ByteBuffer buf = publish.getPayload().orElse(null);
                    bytes = buf != null ? toByteArray(buf) : new byte[0];
                } catch (Throwable t) {
                    bytes = new byte[0];
                }
                handleInbound(topic, bytes);
            });

            // Subscribe to topics with QoS
            String[] topics = new String[] { "/server/+/task/photo/completed", "/server/+/task/confirm",
                    "/server/+/task/completed", "/server/+/task_completed", "/server/+/job/status",
                    "/server/+/jobs/assigned", "/server/login" };
            MqttQos qos = mapQos(props.getDefaultQos());
            for (String topic : topics) {
                client.subscribeWith().topicFilter(topic).qos(qos).send().join();
            }
            running = true;
            log.info("[MQTT] Subscribed to {} topics (QoS={}), awaiting messages ...", topics.length, qos);
        } catch (Exception e) {
            log.error("Failed to start HiveMQ MQTT client: {}", e.getMessage(), e);
        }
    }

    private static byte[] toByteArray(ByteBuffer buffer) {
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        return bytes;
    }

    private String buildClientId(String base) {
        String b = (base == null || base.isBlank()) ? "server" : base;
        if (!b.contains("${random.uuid}")) {
            return b + "-" + UUID.randomUUID();
        }
        return b.replace("${random.uuid}", UUID.randomUUID().toString());
    }

    private void handleInbound(String topic, byte[] payload) {
        String json = new String(payload, StandardCharsets.UTF_8);
        try {
            Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
            });
            routeInbound(topic, map);
        } catch (Exception ex) {
            log.error("Failed to parse inbound MQTT JSON on {}: {}", topic, ex.getMessage());
        }
    }

    private void routeInbound(String topic, Map<String, Object> payload) {
        try {
            // The consolidated topic /server/{clientId}/task_completed is used by apps to
            // report completion of any task type. Only PHOTO and CONFIRMATION require
            // specialized processing on the server side. All other task types are handled
            // by the
            // generic handler handleTaskCompleted(). This keeps routing simple while
            // allowing
            // special logic (e.g., photo persistence) where necessary.
            if (topic.matches("/server/.+/task_completed")) {
                try {
                    Object tt = payload.get("taskType");
                    String taskType = tt != null ? tt.toString() : null;
                    messageController.handleTaskCompleted(payload, taskType);
                } catch (Exception e) {
                    log.error("Error routing task_completed by taskType: {}", e.getMessage(), e);
                }
            } else if (topic.matches("/server/.+/jobs/assigned")) {
                try {
                    // Extract clientId from topic: /server/{clientId}/jobs/assigned
                    String[] parts = topic.split("/");
                    String clientId = parts.length > 2 ? parts[2] : null;
                    if (clientId != null && !clientId.isBlank()) {
                        payload.put("clientId", clientId);
                    } else {
                        log.warn("Couldn't extract clientId from topic {} for jobs/assigned", topic);
                    }
                    messageController.handleGetAssignedJobs(payload);
                } catch (Exception e) {
                    log.error("Error handling jobs/assigned on {}: {}", topic, e.getMessage(), e);
                }
            } else if (topic.equals("/server/login")) {
                var om = new ObjectMapper();
                de.assecutor.votianlt.dto.AppLoginRequest req = om.convertValue(payload,
                        de.assecutor.votianlt.dto.AppLoginRequest.class);
                messageController.handleAppLogin(req);
            } else {
                log.debug("No route for topic {}", topic);
            }
        } catch (Exception e) {
            log.error("Error routing inbound MQTT message on {}: {}", topic, e.getMessage(), e);
        }
    }

    @Override
    public void stop() {
        try {
            if (client != null) {
                client.disconnect().join();
            }
        } catch (Exception e) {
            log.warn("Error during MQTT client shutdown: {}", e.getMessage());
        } finally {
            running = false;
            client = null;
        }
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    private MqttQos mapQos(int q) {
        return switch (q) {
        case 0 -> MqttQos.AT_MOST_ONCE;
        case 1 -> MqttQos.AT_LEAST_ONCE;
        default -> MqttQos.EXACTLY_ONCE;
        };
    }

    public void publish(String topic, byte[] payload, int qos, boolean retained) {
        try {
            if (client == null) {
                log.warn("[MQTT] Not connected, dropping publish topic={}", topic);
                return;
            }
            client.publishWith().topic(topic).payload(payload).qos(mapQos(qos)).retain(retained).send()
                    .whenComplete((ack, ex) -> {
                        if (ex != null) {
                            log.error("Failed to publish to {}: {}", topic, ex.getMessage(), ex);
                        }
                    });
        } catch (Exception e) {
            log.error("Failed to publish to {}: {}", topic, e.getMessage(), e);
        }
    }
}
