package com.lingniu.ingest.inbound.mqtt.client;

import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
import com.lingniu.ingest.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityLookup;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.inbound.mqtt.config.MqttInboundProperties;
import com.lingniu.ingest.inbound.mqtt.model.MqttPayload;
import com.lingniu.ingest.inbound.mqtt.profile.MqttProfileRegistry;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * MQTT 多 endpoint 生命周期管理。
 *
 * <p>为每个 endpoint 创建一个 Paho 异步客户端 → 连接 → 订阅 → 注册回调 →
 * 回调里解析 payload → {@link Dispatcher#dispatch(RawFrame)}。
 *
 * <p>MQTT 回调不做业务处理，虚拟线程由 Dispatcher 侧的 Disruptor 承接。
 */
public final class MqttEndpointManager implements AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(MqttEndpointManager.class);

    private final MqttInboundProperties props;
    private final MqttProfileRegistry profileRegistry;
    private final VehicleIdentityResolver identityResolver;
    private final Dispatcher dispatcher;
    private final List<MqttAsyncClient> clients = new ArrayList<>();
    private boolean started;

    public MqttEndpointManager(MqttInboundProperties props,
                               MqttProfileRegistry profileRegistry,
                               VehicleIdentityResolver identityResolver,
                               Dispatcher dispatcher) {
        this.props = props;
        this.profileRegistry = profileRegistry;
        if (identityResolver == null) {
            throw new IllegalArgumentException("identityResolver must not be null");
        }
        this.identityResolver = identityResolver;
        this.dispatcher = dispatcher;
    }

    public synchronized void start() {
        if (started) {
            return;
        }
        started = true;
        if (props.getEndpoints().isEmpty()) {
            log.warn("mqtt inbound enabled but no endpoints configured");
            return;
        }
        log.info("mqtt inbound starting endpoints={}", props.getEndpoints().size());
        for (MqttInboundProperties.Endpoint ep : props.getEndpoints()) {
            try {
                // 多 endpoint 互不影响：单个连接初始化失败会派发 operationalError，但不会阻止其他 endpoint。
                log.info("mqtt endpoint [{}] initializing uri={} topic={} profile={} qos={} cleanSession={} tlsHostnameVerification={}",
                        ep.getName(), ep.getUri(), ep.getTopic(), ep.getProfile(), ep.getQos(), ep.isCleanSession(),
                        ep.getTls() == null || ep.getTls().isHostnameVerificationEnabled());
                MqttAsyncClient client = buildClient(ep);
                clients.add(client);
                connectAndSubscribe(client, ep);
            } catch (Exception e) {
                log.error("mqtt endpoint [{}] init failed, skipped", ep.getName(), e);
                publishOperationalError(ep, "init", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
            }
        }
    }

    private MqttAsyncClient buildClient(MqttInboundProperties.Endpoint ep) throws MqttException {
        String clientId = ep.getClientId() == null || ep.getClientId().isBlank()
                ? "lingniu-" + UUID.randomUUID() : ep.getClientId();
        return new MqttAsyncClient(pahoServerUri(ep.getUri()), clientId, new MemoryPersistence());
    }

    private void connectAndSubscribe(MqttAsyncClient client, MqttInboundProperties.Endpoint ep) throws MqttException {
        client.setCallback(new MqttCallbackExtended() {
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                if (!reconnect) {
                    return;
                }
                try {
                    client.subscribe(ep.getTopic(), mapQos(ep.getQos())).waitForCompletion(timeoutMs(ep));
                    log.info("mqtt endpoint [{}] re-subscribed topic={} qos={}", ep.getName(), ep.getTopic(), ep.getQos());
                } catch (Exception e) {
                    log.error("mqtt endpoint [{}] re-subscribe failed topic={}", ep.getName(), ep.getTopic(), e);
                    publishOperationalError(ep, "subscribe",
                            e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
                }
            }

            @Override
            public void connectionLost(Throwable cause) {
                log.warn("mqtt endpoint [{}] connection lost", ep.getName(), cause);
            }

            @Override
            public void messageArrived(String topic, MqttMessage message) {
                log.info("mqtt endpoint [{}] received topic={} bytes={}", ep.getName(), topic, message.getPayload().length);
                onMessage(ep, topic, message.getPayload());
            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {
                // inbound only
            }
        });

        MqttConnectOptions options = connectOptions(ep);
        client.connect(options).waitForCompletion(timeoutMs(ep));
        log.info("mqtt endpoint [{}] connected serverUri={} clientId={}",
                ep.getName(), client.getServerURI(), client.getClientId());
        client.subscribe(ep.getTopic(), mapQos(ep.getQos())).waitForCompletion(timeoutMs(ep));
        log.info("mqtt endpoint [{}] subscribed topic={} qos={}", ep.getName(), ep.getTopic(), ep.getQos());
    }

    private void onMessage(MqttInboundProperties.Endpoint ep, String topic, byte[] payload) {
        try {
            MqttPayload parsed = profileRegistry.parse(ep.getName(), ep.getProfile(), topic, payload);
            String externalDeviceId = firstNonBlank(parsed.deviceId(), parsed.vin());
            IdentityResolution identity = resolveIdentity(parsed);
            parsed = parsed.withIdentity(identity.identity(), identity.errorMessage());
            Map<String, String> meta = new HashMap<>(4);
            // metadata 同时保留外部设备号和内部 VIN，便于排查绑定表错误导致的车辆归属问题。
            meta.put("vin", identity.identity().vin());
            meta.put("externalVin", parsed.externalVin() == null ? "" : parsed.externalVin());
            meta.put("phone", parsed.phone() == null ? "" : parsed.phone());
            meta.put("mqttDeviceId", externalDeviceId);
            meta.put("plateNo", parsed.plateNo() == null ? "" : parsed.plateNo());
            meta.put("identityResolved", Boolean.toString(identity.identity().resolved()));
            meta.put("identitySource", identity.identity().source().name());
            if (identity.errorMessage() != null) {
                meta.put("identityError", "true");
                meta.put("identityErrorMessage", identity.errorMessage());
            }
            meta.put("endpoint", ep.getName());
            meta.put("topic", topic);
            meta.put("profile", parsed.profile() == null ? "" : parsed.profile());
            if (parsed.data() != null && parsed.data().path("_parseError").asBoolean(false)) {
                meta.put("parseError", "true");
                meta.put("parseErrorMessage", parsed.data().path("_parseErrorMessage").asText(""));
                meta.put("unsupportedProfile",
                        Boolean.toString(parsed.data().path("_unsupportedProfile").asBoolean(false)));
            }
            RawFrame rf = new RawFrame(
                    ProtocolId.MQTT_YUTONG, 0, 0,
                    parsed, payload, meta, Instant.now());
            dispatcher.dispatch(rf);
        } catch (Exception e) {
            log.warn("mqtt endpoint [{}] message handling failed topic={} len={}",
                    ep.getName(), topic, payload.length, e);
            // 单条消息异常也转成 RawFrame 错误事件，避免 MQTT 回调线程吞掉现场。
            publishMessageError(ep, topic, payload, e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
        }
    }

    private IdentityResolution resolveIdentity(MqttPayload payload) {
        String externalDeviceId = firstNonBlank(payload.deviceId(), payload.vin());
        String externalVin = payload.externalVin() == null ? "" : payload.externalVin();
        String phone = payload.phone() == null ? "" : payload.phone();
        String plateNo = payload.plateNo() == null ? "" : payload.plateNo();
        try {
            VehicleIdentity bound = identityResolver.resolve(new VehicleIdentityLookup(
                    ProtocolId.MQTT_YUTONG,
                    externalVin,
                    phone,
                    externalDeviceId,
                    plateNo));
            if (bound.resolved() || !looksLikeVin(externalDeviceId)) {
                return new IdentityResolution(bound, null);
            }
            return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
                    ProtocolId.MQTT_YUTONG,
                    externalDeviceId,
                    phone,
                    externalDeviceId,
                    plateNo)), null);
        } catch (RuntimeException e) {
            return new IdentityResolution(
                    new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
                    e.getMessage() == null ? "" : e.getMessage());
        }
    }

    private static String firstNonBlank(String... values) {
        for (String value : values) {
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return "";
    }

    private void publishOperationalError(MqttInboundProperties.Endpoint ep, String phase, String reason) {
        // 连接/订阅级别错误也进入 Dispatcher，这样监控和历史查询能看到接入端本身的问题。
        String endpointName = ep == null ? "" : ep.getName();
        String profile = ep == null ? "" : ep.getProfile();
        String topic = ep == null ? "" : ep.getTopic();
        String json = "{\"operationalError\":true"
                + ",\"endpoint\":\"" + escape(endpointName) + "\""
                + ",\"profile\":\"" + escape(profile) + "\""
                + ",\"topic\":\"" + escape(topic) + "\""
                + ",\"phase\":\"" + escape(phase) + "\""
                + ",\"reason\":\"" + escape(reason) + "\"}";
        byte[] raw = json.getBytes(StandardCharsets.UTF_8);
        MqttPayload payload = MqttProfileRegistry.operationalErrorPayload(
                endpointName, profile, topic, raw, phase, reason);
        Map<String, String> meta = new HashMap<>();
        meta.put("vin", "unknown");
        meta.put("identityResolved", "false");
        meta.put("identitySource", "UNKNOWN");
        meta.put("endpoint", endpointName == null ? "" : endpointName);
        meta.put("topic", topic == null ? "" : topic);
        meta.put("profile", profile == null ? "" : profile);
        meta.put("operationalError", "true");
        meta.put("phase", phase == null ? "" : phase);
        meta.put("reason", reason == null ? "" : reason);
        dispatcher.dispatch(new RawFrame(
                ProtocolId.MQTT_YUTONG,
                0,
                0,
                payload,
                raw,
                meta,
                Instant.now()));
    }

    private void publishMessageError(MqttInboundProperties.Endpoint ep, String topic, byte[] raw, String reason) {
        String endpointName = ep == null ? "" : ep.getName();
        String profile = ep == null ? "" : ep.getProfile();
        byte[] payloadBytes = raw == null ? new byte[0] : raw;
        MqttPayload payload = MqttProfileRegistry.operationalErrorPayload(
                endpointName,
                profile,
                topic,
                payloadBytes,
                "message",
                reason);
        Map<String, String> meta = new HashMap<>();
        meta.put("vin", "unknown");
        meta.put("identityResolved", "false");
        meta.put("identitySource", "UNKNOWN");
        meta.put("endpoint", endpointName == null ? "" : endpointName);
        meta.put("topic", topic == null ? "" : topic);
        meta.put("profile", profile == null ? "" : profile);
        meta.put("operationalError", "true");
        meta.put("phase", "message");
        meta.put("reason", reason == null ? "" : reason);
        dispatcher.dispatch(new RawFrame(
                ProtocolId.MQTT_YUTONG,
                0,
                0,
                payload,
                payloadBytes,
                meta,
                Instant.now()));
    }

    private static String escape(String value) {
        if (value == null) {
            return "";
        }
        return value.replace("\\", "\\\\").replace("\"", "\\\"");
    }

    private static MqttConnectOptions connectOptions(MqttInboundProperties.Endpoint ep) {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setAutomaticReconnect(true);
        options.setCleanSession(ep.isCleanSession());
        if (ep.getKeepAliveSeconds() > 0) {
            options.setKeepAliveInterval(ep.getKeepAliveSeconds());
        }
        if (ep.getConnectionTimeoutSeconds() > 0) {
            options.setConnectionTimeout(ep.getConnectionTimeoutSeconds());
        }
        if (hasText(ep.getUsername())) {
            options.setUserName(ep.getUsername());
            options.setPassword(ep.getPassword() == null ? new char[0] : ep.getPassword().toCharArray());
        }
        if (isSslUri(ep.getUri()) && hasCustomTls(ep.getTls())) {
            options.setSocketFactory(MqttTlsSupport.socketFactory(ep.getTls()));
            if (ep.getTls() != null && !ep.getTls().isHostnameVerificationEnabled()) {
                options.setSSLHostnameVerifier((hostname, session) -> true);
            }
        }
        return options;
    }

    private static String pahoServerUri(String value) {
        URI uri = URI.create(value);
        String scheme = uri.getScheme();
        String pahoScheme = switch (scheme == null ? "" : scheme.toLowerCase()) {
            case "ssl", "tls", "mqtts" -> "ssl";
            default -> "tcp";
        };
        int port = uri.getPort() > 0 ? uri.getPort() : defaultPort(scheme);
        return pahoScheme + "://" + uri.getHost() + ":" + port;
    }

    private static int mapQos(int level) {
        return switch (level) {
            case 0 -> 0;
            case 2 -> 2;
            default -> 1;
        };
    }

    private static long timeoutMs(MqttInboundProperties.Endpoint ep) {
        int seconds = ep.getConnectionTimeoutSeconds() > 0 ? ep.getConnectionTimeoutSeconds() : 10;
        return TimeUnit.SECONDS.toMillis(seconds);
    }

    private static int defaultPort(String scheme) {
        if (scheme == null) return 1883;
        return switch (scheme.toLowerCase()) {
            case "ssl", "tls", "mqtts" -> 8883;
            default -> 1883;
        };
    }

    private static boolean looksLikeVin(String value) {
        return value != null && value.trim().length() == 17;
    }

    private record IdentityResolution(VehicleIdentity identity, String errorMessage) {}

    private static boolean hasCustomTls(MqttInboundProperties.Tls tls) {
        return tls != null && (hasText(tls.getCaPem())
                || (hasText(tls.getClientPem()) && hasText(tls.getClientKey())));
    }

    private static boolean isSslUri(String value) {
        String scheme = URI.create(value).getScheme();
        return scheme != null && switch (scheme.toLowerCase()) {
            case "ssl", "tls", "mqtts" -> true;
            default -> false;
        };
    }

    private static boolean hasText(String value) {
        return value != null && !value.isBlank();
    }

    @Override
    public void close() {
        for (MqttAsyncClient c : clients) {
            try {
                if (c.isConnected()) {
                    c.disconnectForcibly(1000, 1000, false);
                }
                c.close(true);
            } catch (Exception ignored) {
                // best-effort
            }
        }
        log.info("mqtt endpoint manager stopped, clients={}", clients.size());
    }
}
