package com.zcloud.personnel.positioning.command.query;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zcloud.personnel.positioning.dto.clientobject.BiPersonLocationCO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Component
@RequiredArgsConstructor
public class RealtimePersonLocationMatcher {
    private static final String[] ROW_COLLECTION_FIELDS = {"records", "rows", "list", "points"};
    private static final String[] ENVELOPE_FIELDS = {"data", "payload", "message", "body"};

    private final ObjectMapper objectMapper;

    public List<BiPersonLocationCO> match(byte[] payload) {
        JsonNode root = parse(payload);
        List<LocationCandidate> candidates = new ArrayList<>();
        for (JsonNode row : extractRows(root)) {
            String staffNo = staffNo(row);
            if (!StringUtils.hasText(staffNo)) {
                continue;
            }
            candidates.add(new LocationCandidate(row, staffNo, terminalNo(row)));
        }

        List<BiPersonLocationCO> result = new ArrayList<>();
        for (LocationCandidate candidate : candidates) {
            result.add(toClientObject(candidate));
        }
        return result;
    }

    private JsonNode parse(byte[] payload) {
        if (payload == null || payload.length == 0) {
            throw new RealtimeLocationPayloadException("Kafka location payload is empty");
        }
        try {
            JsonNode root = objectMapper.readTree(payload);
            if (root == null || root.isNull()) {
                throw new RealtimeLocationPayloadException("Kafka location payload is empty JSON");
            }
            return root;
        } catch (IOException e) {
            throw new RealtimeLocationPayloadException("Kafka location payload is not valid JSON", e);
        }
    }

    private List<JsonNode> extractRows(JsonNode root) {
        if (root.isArray()) {
            return arrayValues(root);
        }
        JsonNode collection = firstArray(root, ROW_COLLECTION_FIELDS);
        if (collection != null) {
            return arrayValues(collection);
        }
        for (String envelopeField : ENVELOPE_FIELDS) {
            JsonNode nested = parsedNode(root.get(envelopeField));
            if (nested == null) {
                continue;
            }
            if (nested.isArray()) {
                return arrayValues(nested);
            }
            JsonNode nestedCollection = firstArray(nested, ROW_COLLECTION_FIELDS);
            if (nestedCollection != null) {
                return arrayValues(nestedCollection);
            }
            if (!StringUtils.hasText(staffNo(root)) && nested.isObject()) {
                return Collections.singletonList(nested);
            }
        }
        return Collections.singletonList(root);
    }

    private JsonNode parsedNode(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        }
        if (!node.isTextual()) {
            return node;
        }
        String value = node.asText();
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return objectMapper.readTree(value);
        } catch (IOException ignored) {
            return null;
        }
    }

    private JsonNode firstArray(JsonNode node, String... fieldNames) {
        if (node == null || !node.isObject()) {
            return null;
        }
        for (String fieldName : fieldNames) {
            JsonNode value = node.get(fieldName);
            if (value != null && value.isArray()) {
                return value;
            }
        }
        return null;
    }

    private List<JsonNode> arrayValues(JsonNode array) {
        List<JsonNode> result = new ArrayList<>();
        array.forEach(result::add);
        return result;
    }

    private String staffNo(JsonNode row) {
        return firstText(
                directText(row, "staffNo", "code", "employeeNo", "workNo", "trackNo"),
                directText(child(row, "terminalInfo"), "trackNo", "staffNo"),
                directText(child(row, "staff"), "staffNo", "code", "employeeNo", "workNo", "trackNo"),
                directText(child(row, "person"), "staffNo", "code", "employeeNo", "workNo", "trackNo"),
                directText(child(row, "personInfo"), "staffNo", "code", "employeeNo", "workNo", "trackNo"),
                directText(locationNode(row), "staffNo", "trackNo"));
    }

    private String terminalNo(JsonNode row) {
        return firstText(
                directText(row, "terminalNo"),
                directText(child(row, "terminalInfo"), "terminalNo"),
                directText(child(row, "terminal"), "terminalNo"),
                directText(child(row, "bindTerminal"), "terminalNo"),
                directText(locationNode(row), "terminalNo"));
    }

    private BiPersonLocationCO toClientObject(LocationCandidate candidate) {
        JsonNode row = candidate.row;
        JsonNode location = locationNode(row);
        JsonNode staff = firstObject(row, "staff", "person", "personInfo", "staffInfo");
        String orgName = firstText(
                directText(row, "orgName", "organizationName", "companyName", "corpName", "enterpriseName"),
                directText(staff, "orgName", "organizationName", "companyName", "corpName", "enterpriseName"));
        String orgCode = firstText(
                directText(row, "orgCode", "companyCode", "corpCode", "enterpriseCode"),
                directText(staff, "orgCode", "companyCode", "corpCode", "enterpriseCode"));
        BiPersonLocationCO co = new BiPersonLocationCO();
        co.setStaffName(firstText(
                directText(row, "staffName", "name", "realName", "userName"),
                directText(staff, "staffName", "name", "realName", "userName")));
        co.setStaffNo(candidate.staffNo);
        co.setIdCardNo(firstText(
                directText(row, "idCardNo", "identityNo", "certificateNo", "cardNo"),
                directText(staff, "idCardNo", "identityNo", "certificateNo", "cardNo")));
        co.setMobileNo(firstText(
                directText(row, "mobileNo", "phone", "phoneNo", "tel"),
                directText(staff, "mobileNo", "phone", "phoneNo", "tel")));
        co.setDepartmentName(firstText(
                directText(row, "departmentName", "deptName"),
                directText(child(row, "department"), "name", "departmentName"),
                directText(child(staff, "department"), "name", "departmentName")));
        co.setCompanyName(firstText(
                directText(row, "companyName", "corpName", "enterpriseName"),
                directText(staff, "companyName", "corpName", "enterpriseName"),
                orgName));
        co.setCorpinfoId(firstText(
                directText(row, "corpinfoId", "companyId", "corpId", "enterpriseId"),
                directText(staff, "corpinfoId", "companyId", "corpId", "enterpriseId")));
        co.setCorpinfoName(firstText(co.getCompanyName(), orgName));
        co.setOrgName(orgName);
        co.setOrgCode(orgCode);
        co.setCreditCode(firstText(
                directText(row, "creditCode", "socialCreditCode", "unifiedSocialCreditCode",
                        "unifiedCreditCode", "creditNo"),
                directText(staff, "creditCode", "socialCreditCode", "unifiedSocialCreditCode",
                        "unifiedCreditCode", "creditNo")));
        co.setPortArea(integerValue(firstValue(row, staff, "portArea", "portAreaCode")));
        co.setTerminalNo(candidate.terminalNo);
        co.setPositionMode(positionMode(row));
        co.setLon(decimalValue(firstValue(location, row, "lon", "lng", "longitude")));
        co.setLat(decimalValue(firstValue(location, row, "lat", "latitude")));
        co.setAlt(decimalValue(firstValue(location, row, "alt", "altitude")));
        co.setSpeed(decimalValue(firstValue(location, row, "spd", "speed")));
        co.setDirection(decimalValue(firstValue(location, row, "dir", "direction")));
        co.setLastLocationTime(longValue(firstValue(location, row, "gt", "time", "locateTime",
                "locationTime", "timestamp")));
        co.setCurrentLocation(firstText(
                directText(location, "fenceName", "areaName", "positionName", "location", "address"),
                directText(row, "fenceName", "areaName", "positionName", "locationName", "address")));
        Boolean online = booleanValue(firstValue(location, row, "online", "isOnline"));
        co.setOnline(online == null ? co.getLon() != null && co.getLat() != null : online);
        co.setAlarmStatus(booleanValue(firstValue(location, row, "alarmStatus", "isAlarm")));
        co.setAlarmCount(integerValue(firstValue(location, row, "alarmCount", "unclosedAlarmCount")));
        co.setStaffRawJson(row.toString());
        co.setLocationRawJson(location.toString());
        return co;
    }

    private JsonNode locationNode(JsonNode row) {
        JsonNode nested = firstObject(row, "location", "point", "position", "locationInfo", "data");
        return nested == null ? row : nested;
    }

    private JsonNode firstObject(JsonNode node, String... fieldNames) {
        if (node == null || !node.isObject()) {
            return null;
        }
        for (String fieldName : fieldNames) {
            JsonNode value = node.get(fieldName);
            if (value != null && value.isObject()) {
                return value;
            }
        }
        return null;
    }

    private JsonNode firstValue(JsonNode first, JsonNode second, String... fieldNames) {
        JsonNode value = directValue(first, fieldNames);
        return value == null ? directValue(second, fieldNames) : value;
    }

    private JsonNode directValue(JsonNode node, String... fieldNames) {
        if (node == null || !node.isObject()) {
            return null;
        }
        for (String fieldName : fieldNames) {
            JsonNode value = node.get(fieldName);
            if (value != null && !value.isNull()) {
                return value;
            }
        }
        return null;
    }

    private String directText(JsonNode node, String... fieldNames) {
        JsonNode value = directValue(node, fieldNames);
        if (value == null || value.isContainerNode()) {
            return null;
        }
        String text = value.asText();
        return StringUtils.hasText(text) ? text.trim() : null;
    }

    private BigDecimal decimalValue(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        }
        try {
            return node.isNumber() ? node.decimalValue() : new BigDecimal(node.asText());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private Long longValue(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        }
        try {
            return node.isNumber() ? node.asLong() : Long.valueOf(node.asText());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private String positionMode(JsonNode row) {
        JsonNode terminal = firstObject(row, "terminalInfo", "terminal", "bindTerminal");
        String deviceType = firstText(
                directText(row, "positionMode", "deviceType", "terminalType"),
                directText(terminal, "positionMode", "deviceType", "terminalType", "type"));
        if ("CARD".equalsIgnoreCase(deviceType)) {
            return "\u5b9a\u4f4d\u5361";
        }
        if ("APP".equalsIgnoreCase(deviceType)) {
            return "\u624b\u673aAPP";
        }
        if ("OTHER".equalsIgnoreCase(deviceType)) {
            return "\u5176\u4ed6";
        }
        return StringUtils.hasText(deviceType) ? deviceType : "\u5b9a\u4f4d\u7ec8\u7aef";
    }

    private Integer integerValue(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        }
        try {
            return node.isNumber() ? node.asInt() : Integer.valueOf(node.asText());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private Boolean booleanValue(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        }
        if (node.isBoolean()) {
            return node.asBoolean();
        }
        String value = node.asText();
        if (!StringUtils.hasText(value)) {
            return null;
        }
        if ("1".equals(value) || "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value)) {
            return Boolean.TRUE;
        }
        if ("0".equals(value) || "false".equalsIgnoreCase(value) || "no".equalsIgnoreCase(value)) {
            return Boolean.FALSE;
        }
        return null;
    }

    private JsonNode child(JsonNode node, String fieldName) {
        return node == null || !node.isObject() ? null : node.get(fieldName);
    }

    private String firstText(String... values) {
        for (String value : values) {
            if (StringUtils.hasText(value)) {
                return value;
            }
        }
        return null;
    }

    private static class LocationCandidate {
        private final JsonNode row;
        private final String staffNo;
        private final String terminalNo;

        private LocationCandidate(JsonNode row, String staffNo, String terminalNo) {
            this.row = row;
            this.staffNo = staffNo;
            this.terminalNo = terminalNo;
        }
    }
}
