package com.lingniu.ingest.inbound.xinda;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gps31.push.netty.PushClient;
import com.gps31.push.netty.PushMsg;
import com.gps31.push.netty.client.TcpClient;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.RawArchiveKeys;
import com.lingniu.ingest.api.pipeline.RawFrame;
import com.lingniu.ingest.core.dispatcher.Dispatcher;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
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.xinda.config.XindaPushProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

/**
 * 信达 Push 接入客户端。基于 {@code org.lingniu:gps-push-client:1.0} 的 {@link PushClient}
 * 实现，完全使用信达真实协议帧格式（帧头 / 长度 / cmd / json / 帧尾 + 序列号 + 心跳）。
 *
 * <p>行为：
 * <ul>
 *   <li>{@link #start()} 调用基类 {@code TcpClient.start()} 建立连接并自动重连
 *   <li>基类在 {@code channelActive} 自动发送 {@code 0x0001 登录} 帧，携带 userName / pwd / csTag / desc
 *   <li>登录成功收到应答后（由基类内部处理）会自动订阅 {@code subMsgIds}（默认空，需显式调用 setSubMsgIds）
 *   <li>{@link #messageReceived(TcpClient, PushMsg)} 为业务入口：cmd 字符串区分消息类型
 *   <li>位置 {@code 0200} / 报警 {@code 0300} / 透传 {@code 0401} → RawFrame → Dispatcher
 * </ul>
 *
 * <p>类继承基类 {@code PushClient}，不再需要手工处理帧编解码、心跳、重连、订阅等机制。
 */
public class XindaPushClient extends PushClient {

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

    private final XindaPushProperties props;
    private final VehicleIdentityResolver identityResolver;
    private final Dispatcher dispatcher;

    public XindaPushClient(XindaPushProperties props, Dispatcher dispatcher) {
        this(props, new InMemoryVehicleIdentityService(), dispatcher);
    }

    public XindaPushClient(XindaPushProperties props,
                           VehicleIdentityResolver identityResolver,
                           Dispatcher dispatcher) {
        super();
        this.props = props;
        this.identityResolver = identityResolver == null ? new InMemoryVehicleIdentityService() : identityResolver;
        this.dispatcher = dispatcher;
        this.isLog = false;
    }

    /**
     * 启动客户端：填入 host/port/user/pwd/desc，配置订阅消息 ID，交给基类建立连接。
     */
    public void start() {
        if (props.getHost() == null || props.getHost().isBlank()) {
            publishOperationalError("startup", "host not configured");
            return;
        }
        if (props.getUsername() == null || props.getPassword() == null) {
            publishOperationalError("startup", "credentials missing");
            return;
        }
        setHost(props.getHost());
        setPort(props.getPort());
        setUserName(props.getUsername());
        setPwd(props.getPassword());
        setDesc(props.getClientDesc());
        // 订阅消息 ID：信达基类 PushClient.setSubMsgIds 用管道符 "|" 作为分隔符
        // （内部调用 StringUtil.splitStr(s, "|")）
        if (props.getSubscribeMsgIds() != null && !props.getSubscribeMsgIds().isEmpty()) {
            setSubMsgIds(String.join("|", props.getSubscribeMsgIds()));
        }
        // 基类 PushClient 构造器默认 isLog=false / isLogBytes=false；
        // 这里尊重默认值，不再覆盖为 true，避免 <<<Log发送 / >>>Log接收 每条帧都被 ERROR 级打印。
        // 真要打开 wire-level 诊断时，临时改为 setLog(true) 即可。
        try {
            log.info("[xinda-push] starting host={} port={} user={} subMsgIds={}",
                    getHost(), getPort(), getUserName(), getSubMsgIds());
            super.start();
        } catch (Exception e) {
            log.error("[xinda-push] start failed", e);
            publishOperationalError("startup", e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
        }
    }

    /**
     * Spring 生命周期停止钩子。
     */
    public void stop() {
        try {
            destroy();
            log.info("[xinda-push] stopped");
        } catch (Exception e) {
            log.warn("[xinda-push] stop error", e);
        }
    }

    // ========================================================================
    // 业务回调：收到完整的 PushMsg 后分派事件
    // ========================================================================

    @Override
    public void messageReceived(TcpClient client, PushMsg msg) throws Exception {
        super.messageReceived(client, msg);
        String cmd = msg.getCmd();
        if (cmd == null) return;
        try {
            // 信达只把业务消息派发到 Dispatcher；登录/心跳/订阅 ACK 只影响连接状态和日志。
            switch (cmd) {
                case "8001" -> handleLoginAck(msg);
                case "8002" -> log.debug("[xinda-push] heartbeat ack");
                case "8003" -> log.info("[xinda-push] subscribe ack json={}", msg.getJson());
                case "0200", "0300", "0401" -> dispatchBusinessMessage(msg, false);
                default -> {
                    log.debug("[xinda-push] rx unknown business cmd={} json={}", cmd, msg.getJson());
                    dispatchBusinessMessage(msg, true);
                }
            }
        } catch (Exception e) {
            log.warn("[xinda-push] dispatch failed cmd={} json={}", cmd, msg.getJson(), e);
            // 推送回调异常也生成错误 RawFrame，保证原始 JSON 不丢。
            publishMessageError(msg, e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());
        }
    }

    /**
     * 收到 8001 登录应答后处理：若 {@code rspResult=0} 则主动发送 0003 订阅命令。
     *
     * <p>信达订阅命令的 body 格式（从旧 {@code PushClientHandler} 和 8003 错误提示反推得到）：
     * <pre>{@code
     * {
     *   "seq":     "1",                             // 序列号
     *   "action":  "add",                           // 操作: add 订阅 / del 取消
     *   "msgIds":  "[\"0200\",\"0300\",\"0401\"]"  // JSON 数组字符串（注意是字符串，不是数组）
     * }
     * }</pre>
     * <p>
     * 其中 {@code msgIds} 的值来自基类 {@link PushClient#getSubCmdSet()}，内容由
     * {@link PushClient#setSubMsgIds(String)} 填充（分隔符是 {@code |}）。
     */
    private void handleLoginAck(PushMsg msg) throws Exception {
        String json = msg.getJson() == null ? "" : msg.getJson();
        log.info("[xinda-push] login ack json={}", json);
        JSONObject parsed;
        try {
            parsed = JSON.parseObject(json);
        } catch (Exception e) {
            publishOperationalError("login", "login ack parse failed: "
                    + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
            return;
        }
        String rsp = parsed == null ? null : parsed.getString("rspResult");
        if (!"0".equals(rsp)) {
            log.error("[xinda-push] login REJECTED rspResult={}", rsp);
            publishOperationalError("login", "login rejected rspResult=" + (rsp == null ? "" : rsp));
            return;
        }
        Map<String, Object> body = new java.util.HashMap<>();
        body.put("seq", "1");
        body.put("action", "add");
        body.put("msgIds", JSON.toJSONString(getSubCmdSet()));
        PushMsg subscribe = getInstance("0003", body);
        sendMsg(subscribe);
        log.info("[xinda-push] subscribe sent action=add msgIds={}", getSubCmdSet());
    }

    @Override
    public void channelActive(TcpClient client) throws Exception {
        super.channelActive(client);
        log.info("[xinda-push] channel active, login frame sent");
    }

    @Override
    public void channelInactive(TcpClient client) throws Exception {
        super.channelInactive(client);
        log.warn("[xinda-push] channel inactive, will auto-reconnect");
    }

    private void dispatchBusinessMessage(PushMsg msg, boolean unknownCommand) {
        String json = msg.getJson() == null ? "" : msg.getJson();
        XindaPushMessage payload = new XindaPushMessage(msg.getCmd(), json);
        byte[] raw = json.getBytes(StandardCharsets.UTF_8);
        JsonParseResult parsed = parseForMetadata(json);
        JSONObject identityFields = parsed.json();
        IdentityResolution identity = resolveIdentity(identityFields);
        Map<String, String> meta = new HashMap<>();
        // 保留外部字段和内部 VIN 的双轨元数据，方便定位绑定错误或第三方字段变更。
        meta.put("source", "xinda-push");
        meta.put("cmd", payload.command());
        putRawFields(meta, identityFields);
        meta.put("vin", identity.identity().vin());
        meta.put("externalVin", firstNonBlank(identityFields, "vin"));
        meta.put("phone", phone(identityFields));
        meta.put("deviceId", deviceId(identityFields));
        meta.put("plateNo", plateNo(identityFields));
        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());
        }
        if (unknownCommand) {
            meta.put("unknownCommand", "true");
        }
        if (payload.malformedCommand()) {
            meta.put("malformedCommand", "true");
        }
        if (parsed.parseError()) {
            meta.put("parseError", "true");
            meta.put("parseErrorMessage", parsed.errorMessage());
        }
        meta.put(RawArchiveKeys.META_PARSED_JSON, parsedJson(payload.command(), json, parsed));
        dispatcher.dispatch(new RawFrame(
                ProtocolId.XINDA_PUSH,
                payload.commandCode(),
                0,
                payload,
                raw,
                meta,
                Instant.now()));
    }

    private void publishOperationalError(String phase, String reason) {
        log.error("[xinda-push] operational error phase={} reason={}", phase, reason);
        JSONObject json = new JSONObject();
        json.put("operationalError", true);
        json.put("phase", phase == null ? "" : phase);
        json.put("reason", reason == null ? "" : reason);
        XindaPushMessage payload = new XindaPushMessage("0000", json.toJSONString());
        byte[] raw = payload.json().getBytes(StandardCharsets.UTF_8);
        Map<String, String> meta = new HashMap<>();
        meta.put("source", "xinda-push");
        meta.put("cmd", payload.command());
        meta.put("vin", "unknown");
        meta.put("identityResolved", "false");
        meta.put("identitySource", "UNKNOWN");
        meta.put("operationalError", "true");
        meta.put("phase", phase == null ? "" : phase);
        meta.put("reason", reason == null ? "" : reason);
        meta.put(RawArchiveKeys.META_PARSED_JSON,
                parsedJson(payload.command(), payload.json(), new JsonParseResult(json, false, "")));
        dispatcher.dispatch(new RawFrame(
                ProtocolId.XINDA_PUSH,
                payload.commandCode(),
                0,
                payload,
                raw,
                meta,
                Instant.now()));
    }

    private void publishMessageError(PushMsg msg, String reason) {
        String cmd = msg == null || msg.getCmd() == null ? "0000" : msg.getCmd();
        String json = msg == null || msg.getJson() == null ? "" : msg.getJson();
        XindaPushMessage payload = new XindaPushMessage(cmd, json);
        byte[] raw = json.getBytes(StandardCharsets.UTF_8);
        Map<String, String> meta = new HashMap<>();
        meta.put("source", "xinda-push");
        meta.put("cmd", cmd);
        meta.put("vin", "unknown");
        meta.put("identityResolved", "false");
        meta.put("identitySource", "UNKNOWN");
        meta.put("operationalError", "true");
        meta.put("phase", "message");
        meta.put("reason", reason == null ? "" : reason);
        JsonParseResult parsed = parseForMetadata(json);
        meta.put(RawArchiveKeys.META_PARSED_JSON, parsedJson(cmd, json, parsed));
        dispatcher.dispatch(new RawFrame(
                ProtocolId.XINDA_PUSH,
                0,
                0,
                payload,
                raw,
                meta,
                Instant.now()));
    }

    // ========================================================================
    // JSON helpers
    // ========================================================================

    private static String firstNonBlank(JSONObject node, String... keys) {
        if (node == null) return "";
        for (String k : keys) {
            Object v = node.get(k);
            if (v != null && !v.toString().isBlank()) return v.toString();
        }
        return "";
    }

    private IdentityResolution resolveIdentity(JSONObject json) {
        try {
            return new IdentityResolution(identityResolver.resolve(new VehicleIdentityLookup(
                    ProtocolId.XINDA_PUSH,
                    firstNonBlank(json, "vin"),
                    phone(json),
                    deviceId(json),
                    plateNo(json))), null);
        } catch (RuntimeException e) {
            return new IdentityResolution(
                    new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN),
                    e.getMessage() == null ? "" : e.getMessage());
        }
    }

    private static String plateNo(JSONObject node) {
        return firstNonBlank(node, "plateNo", "carNo", "carName");
    }

    private static String phone(JSONObject node) {
        return firstNonBlank(node, "sim", "phone", "mobile", "simCode", "tmnKey");
    }

    private static String deviceId(JSONObject node) {
        return firstNonBlank(node, "deviceId", "terminalId", "imei", "tmnKey", "tmnCode");
    }

    private static void putRawFields(Map<String, String> meta, JSONObject json) {
        if (meta == null || json == null || json.isEmpty()) {
            return;
        }
        for (Map.Entry<String, Object> entry : json.entrySet()) {
            if (entry.getKey() == null || entry.getKey().isBlank() || entry.getValue() == null) {
                continue;
            }
            Object value = entry.getValue();
            if (value instanceof JSONObject object) {
                for (Map.Entry<String, Object> nested : object.entrySet()) {
                    if (nested.getKey() != null && !nested.getKey().isBlank() && nested.getValue() != null) {
                        meta.put("raw." + entry.getKey() + "." + nested.getKey(), nested.getValue().toString());
                    }
                }
            } else {
                String text = value.toString();
                JSONObject nestedJson = parseObjectOrNull(text);
                if (nestedJson != null) {
                    for (Map.Entry<String, Object> nested : nestedJson.entrySet()) {
                        if (nested.getKey() != null && !nested.getKey().isBlank() && nested.getValue() != null) {
                            meta.put("raw." + entry.getKey() + "." + nested.getKey(), nested.getValue().toString());
                        }
                    }
                } else {
                    meta.put("raw." + entry.getKey(), text);
                }
            }
        }
    }

    private static JSONObject parseObjectOrNull(String value) {
        if (value == null || value.isBlank() || !value.trim().startsWith("{")) {
            return null;
        }
        try {
            return JSON.parseObject(value);
        } catch (Exception ignored) {
            return null;
        }
    }

    private static JSONObject parseQuietly(String rawJson) {
        if (rawJson == null || rawJson.isBlank()) {
            return new JSONObject();
        }
        try {
            JSONObject parsed = JSON.parseObject(rawJson);
            return parsed == null ? new JSONObject() : parsed;
        } catch (Exception ignored) {
            return new JSONObject();
        }
    }

    private static JsonParseResult parseForMetadata(String rawJson) {
        if (rawJson == null || rawJson.isBlank()) {
            return new JsonParseResult(new JSONObject(), false, "");
        }
        try {
            JSONObject parsed = JSON.parseObject(rawJson);
            return new JsonParseResult(parsed == null ? new JSONObject() : parsed, false, "");
        } catch (Exception e) {
            String message = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
            return new JsonParseResult(new JSONObject(), true, message);
        }
    }

    private static String parsedJson(String command, String rawJson, JsonParseResult parsed) {
        JSONObject root = new JSONObject(true);
        root.put("command", command == null ? "" : command);
        root.put("parseError", parsed != null && parsed.parseError());
        if (parsed != null && parsed.parseError()) {
            root.put("parseErrorMessage", parsed.errorMessage() == null ? "" : parsed.errorMessage());
            root.put("rawJson", rawJson == null ? "" : rawJson);
        } else {
            root.put("body", parsed == null || parsed.json() == null ? new JSONObject() : parsed.json());
        }
        return root.toJSONString();
    }

    private record JsonParseResult(JSONObject json, boolean parseError, String errorMessage) {}

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