package com.lingniu.ingest.inbound.xinda;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.event.AlarmPayload;
import com.lingniu.ingest.api.event.LocationPayload;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.spi.EventMapper;
import com.lingniu.ingest.identity.InMemoryVehicleIdentityService;
import com.lingniu.ingest.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityLookup;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.identity.VehicleRegistrationBinding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;

public final class XindaPushEventMapper implements EventMapper<XindaPushMessage> {

    private static final Logger log = LoggerFactory.getLogger(XindaPushEventMapper.class);
    private static final VehicleIdentityRegistry NOOP_REGISTRY = binding -> {};

    private final VehicleIdentityResolver identityResolver;
    private final VehicleIdentityRegistry identityRegistry;

    public XindaPushEventMapper() {
        this(new InMemoryVehicleIdentityService());
    }

    public XindaPushEventMapper(VehicleIdentityResolver identityResolver) {
        this.identityResolver = identityResolver == null ? new InMemoryVehicleIdentityService() : identityResolver;
        this.identityRegistry = this.identityResolver instanceof VehicleIdentityRegistry registry ? registry : NOOP_REGISTRY;
    }

    public XindaPushEventMapper(VehicleIdentityResolver identityResolver, VehicleIdentityRegistry identityRegistry) {
        this.identityResolver = identityResolver == null ? new InMemoryVehicleIdentityService() : identityResolver;
        this.identityRegistry = identityRegistry == null ? NOOP_REGISTRY : identityRegistry;
    }

    @Override
    public List<VehicleEvent> toEvents(XindaPushMessage message) {
        if (message == null || message.command().isBlank()) {
            return List.of();
        }
        JSONObject json;
        try {
            json = parse(message.json());
        } catch (Exception e) {
            // JSON 解析失败不丢弃，转 passthrough 保留 rawJson，后续可通过历史库回查。
            return List.of(passthrough(message, new JSONObject(), true, false,
                    e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
        }
        if (json.getBooleanValue("operationalError")) {
            // 接入侧运行错误也统一作为 passthrough 事件进入管线，便于监控和审计。
            return List.of(passthrough(message, json, false, false));
        }
        return switch (message.command()) {
            case "0200" -> List.of(location(message, json));
            case "0300" -> List.of(alarm(message, json));
            case "0401" -> List.of(passthrough(message, json, false, false));
            default -> List.of(passthrough(message, json, false, true));
        };
    }

    private VehicleEvent.Location location(XindaPushMessage message, JSONObject json) {
        IdentityResolution identity = resolveIdentity(json);
        double lon = normalizeCoord(asDouble(json, "lng", "lon", "longitude"));
        double lat = normalizeCoord(asDouble(json, "lat", "latitude"));
        double speed = asDouble(json, "speed");
        Double totalMileageKm = optionalDouble(json,
                "totalMileage", "totalMileageKm", "mileage", "mileageKm", "total_mileage");
        double direction = asDouble(json, "drct", "direction");
        long alarmFlag = (long) asDouble(json, "alarmStts", "alarmFlag");
        long statusFlag = (long) asDouble(json, "statusStts", "statusFlag");
        Instant now = Instant.now();
        return new VehicleEvent.Location(
                UUID.randomUUID().toString(),
                identity.vin(),
                ProtocolId.XINDA_PUSH,
                now,
                now,
                null,
                metadata(message, identity, false, false),
                new LocationPayload(lon, lat, 0.0, speed, direction, alarmFlag, statusFlag, totalMileageKm));
    }

    private VehicleEvent.Alarm alarm(XindaPushMessage message, JSONObject json) {
        IdentityResolution identity = resolveIdentity(json);
        double lon = normalizeCoord(asDouble(json, "lng", "lon", "longitude"));
        double lat = normalizeCoord(asDouble(json, "lat", "latitude"));
        String alarmType = firstNonBlank(json, "alarmType", "alarmCode", "type", "warnType");
        String alarmName = firstNonBlank(json, "alarmName", "alarmDesc", "alarmContent", "content", "warnName");
        int alarmTypeCode = alarmTypeCode(alarmType);
        boolean hydrogenLeak = isHydrogenLeak(alarmType, alarmName, firstNonBlank(json, "safetyCategory"));
        AlarmPayload.AlarmLevel level = alarmLevel(asInt(json, "alarmLevel", "level"), hydrogenLeak);
        Instant now = Instant.now();
        return new VehicleEvent.Alarm(
                UUID.randomUUID().toString(),
                identity.vin(),
                ProtocolId.XINDA_PUSH,
                now,
                now,
                null,
                alarmMetadata(message, identity, hydrogenLeak),
                new AlarmPayload(
                        level,
                        alarmTypeCode,
                        alarmType.isBlank() ? alarmName : alarmType,
                        faultCodes(json, alarmType, alarmName),
                        activeBits(json, alarmType, alarmName, hydrogenLeak),
                        lon,
                        lat,
                        hydrogenLeak ? AlarmPayload.SafetyCategory.HYDROGEN_LEAK : AlarmPayload.SafetyCategory.GENERAL,
                        hydrogenLeak,
                        hydrogenLeak ? AlarmPayload.HydrogenLeakLevel.CRITICAL : AlarmPayload.HydrogenLeakLevel.NONE,
                        hydrogenLeak));
    }

    private VehicleEvent.Passthrough passthrough(XindaPushMessage message,
                                                 JSONObject json,
                                                 boolean parseError,
                                                 boolean unknownCommand) {
        return passthrough(message, json, parseError, unknownCommand, "");
    }

    private VehicleEvent.Passthrough passthrough(XindaPushMessage message,
                                                 JSONObject json,
                                                 boolean parseError,
                                                 boolean unknownCommand,
                                                 String parseErrorMessage) {
        IdentityResolution identity = resolveIdentity(json);
        byte[] raw = message.json().getBytes(StandardCharsets.UTF_8);
        Instant now = Instant.now();
        return new VehicleEvent.Passthrough(
                UUID.randomUUID().toString(),
                identity.vin(),
                ProtocolId.XINDA_PUSH,
                now,
                now,
                null,
                metadata(message, identity, parseError, unknownCommand, parseErrorMessage),
                message.commandCode(),
                raw);
    }

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

    private static Map<String, String> metadata(XindaPushMessage message,
                                                IdentityResolution identity,
                                                boolean parseError,
                                                boolean unknownCommand) {
        return metadata(message, identity, parseError, unknownCommand, "");
    }

    private static Map<String, String> metadata(XindaPushMessage message,
                                                IdentityResolution identity,
                                                boolean parseError,
                                                boolean unknownCommand,
                                                String parseErrorMessage) {
        JSONObject json = parseQuietly(message.json());
        Map<String, String> out = new java.util.LinkedHashMap<>();
        out.put("source", "xinda-push");
        out.put("cmd", message.command());
        out.put("vin", normalizedVin(identity));
        out.put("externalVin", firstNonBlank(json, "vin"));
        out.put("phone", phone(json));
        out.put("deviceId", deviceId(json));
        out.put("plateNo", plateNo(json));
        out.put("identityResolved", Boolean.toString(identity.identity().resolved()));
        out.put("identitySource", identity.identity().source().name());
        if (identity.errorMessage() != null) {
            out.put("identityError", "true");
            out.put("identityErrorMessage", identity.errorMessage());
        }
        out.put("parseError", Boolean.toString(parseError));
        if (parseError) {
            out.put("parseErrorMessage", parseErrorMessage == null ? "" : parseErrorMessage);
        }
        out.put("unknownCommand", Boolean.toString(unknownCommand));
        out.put("malformedCommand", Boolean.toString(message.malformedCommand()));
        if (json.getBooleanValue("operationalError")) {
            out.put("operationalError", "true");
            out.put("phase", firstNonBlank(json, "phase"));
            out.put("reason", firstNonBlank(json, "reason"));
        }
        return Map.copyOf(out);
    }

    private static String normalizedVin(IdentityResolution identity) {
        if (identity == null || identity.vin() == null || identity.vin().isBlank()) {
            return "unknown";
        }
        return identity.vin();
    }

    private static Map<String, String> alarmMetadata(XindaPushMessage message,
                                                    IdentityResolution identity,
                                                    boolean hydrogenLeak) {
        JSONObject json = parseQuietly(message.json());
        Map<String, String> out = new java.util.LinkedHashMap<>(metadata(message, identity, false, false));
        out.put("alarmType", firstNonBlank(json, "alarmType", "alarmCode", "type", "warnType"));
        out.put("alarmName", firstNonBlank(json, "alarmName", "alarmDesc", "alarmContent", "content", "warnName"));
        out.put("alarmLevel", firstNonBlank(json, "alarmLevel", "level"));
        out.put("hydrogenLeakDetected", Boolean.toString(hydrogenLeak));
        if (hydrogenLeak) {
            out.put("safetyCategory", AlarmPayload.SafetyCategory.HYDROGEN_LEAK.name());
            out.put("hydrogenLeakLevel", AlarmPayload.HydrogenLeakLevel.CRITICAL.name());
            out.put("hydrogenLeakActionRequired", "true");
        }
        return Map.copyOf(out);
    }

    private static JSONObject parse(String raw) {
        if (raw == null || raw.isBlank()) {
            return new JSONObject();
        }
        return JSON.parseObject(raw);
    }

    private static JSONObject parseQuietly(String raw) {
        try {
            return parse(raw);
        } catch (Exception ignored) {
            return new JSONObject();
        }
    }

    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 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 void registerIdentity(JSONObject json, IdentityResolution identity) {
        String phone = phone(json);
        String deviceId = deviceId(json);
        String plate = plateNo(json);
        if (phone.isBlank() && deviceId.isBlank() && plate.isBlank()) {
            return;
        }
        String vin = identity != null && identity.identity().resolved() ? identity.identity().vin() : "unknown";
        try {
            identityRegistry.register(new VehicleRegistrationBinding(
                    ProtocolId.XINDA_PUSH,
                    vin,
                    phone,
                    deviceId,
                    plate,
                    null,
                    null,
                    "",
                    "",
                    optionalInteger(json, "plateColor", "carColor")));
        } catch (RuntimeException e) {
            log.warn("[xinda-push] vehicle identity registration failed plate={} phone={} deviceId={}",
                    plate, phone, deviceId, e);
        }
    }

    private static double asDouble(JSONObject node, String... keys) {
        Double value = optionalDouble(node, keys);
        return value == null ? 0.0 : value;
    }

    private static Double optionalDouble(JSONObject node, String... keys) {
        if (node == null) return null;
        for (String k : keys) {
            Object v = node.get(k);
            if (v instanceof Number n) return n.doubleValue();
            if (v != null) {
                try {
                    return Double.parseDouble(v.toString());
                } catch (NumberFormatException ignored) {
                }
            }
        }
        return null;
    }

    private static int asInt(JSONObject node, String... keys) {
        return (int) asDouble(node, keys);
    }

    private static Integer optionalInteger(JSONObject node, String... keys) {
        Double value = optionalDouble(node, keys);
        return value == null ? null : value.intValue();
    }

    private static double normalizeCoord(double value) {
        if (Math.abs(value) > 1000) {
            return value / 1_000_000.0;
        }
        return value;
    }

    private static boolean isHydrogenLeak(String... values) {
        for (String value : values) {
            if (value == null || value.isBlank()) {
                continue;
            }
            String v = value.trim().toLowerCase(java.util.Locale.ROOT);
            if (v.contains("hydrogen_leak")
                    || v.contains("h2_leak")
                    || v.contains("hydrogenleak")
                    || v.contains("氢气泄")
                    || v.contains("氢泄")
                    || v.contains("漏氢")) {
                return true;
            }
        }
        return false;
    }

    private static AlarmPayload.AlarmLevel alarmLevel(int code, boolean hydrogenLeak) {
        if (hydrogenLeak || code >= 3) {
            return AlarmPayload.AlarmLevel.CRITICAL;
        }
        if (code == 2) {
            return AlarmPayload.AlarmLevel.MAJOR;
        }
        if (code == 1) {
            return AlarmPayload.AlarmLevel.MINOR;
        }
        return AlarmPayload.AlarmLevel.INFO;
    }

    private static int alarmTypeCode(String alarmType) {
        if (alarmType == null || alarmType.isBlank()) {
            return 0;
        }
        try {
            return Integer.parseInt(alarmType.trim());
        } catch (NumberFormatException ignored) {
            return Math.abs(alarmType.trim().hashCode());
        }
    }

    private static List<String> faultCodes(JSONObject json, String alarmType, String alarmName) {
        LinkedHashSet<String> codes = new LinkedHashSet<>();
        addIfPresent(codes, alarmType);
        addIfPresent(codes, firstNonBlank(json, "faultCode", "faultCodes", "alarmCode"));
        addIfPresent(codes, alarmName);
        return List.copyOf(codes);
    }

    private static java.util.Set<String> activeBits(JSONObject json,
                                                   String alarmType,
                                                   String alarmName,
                                                   boolean hydrogenLeak) {
        LinkedHashSet<String> bits = new LinkedHashSet<>();
        addIfPresent(bits, alarmType);
        addIfPresent(bits, alarmName);
        if (hydrogenLeak) {
            bits.add("HYDROGEN_LEAK");
        }
        return java.util.Set.copyOf(bits);
    }

    private static void addIfPresent(java.util.Set<String> out, String value) {
        if (value != null && !value.isBlank()) {
            out.add(value.trim());
        }
    }

    private record IdentityResolution(VehicleIdentity identity, String errorMessage) {
        private static IdentityResolution ok(VehicleIdentity identity) {
            return new IdentityResolution(identity, null);
        }

        private String vin() {
            return identity.vin();
        }
    }
}
