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

import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.exception.BizException;
import com.fasterxml.jackson.databind.JsonNode;
import com.zcloud.personnel.positioning.dto.PositionPersonPageQry;
import com.zcloud.personnel.positioning.dto.clientobject.PositionPersonCO;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class PositionPersonQueryExe {
    static final String STATUS_ONLINE = "ONLINE";
    static final String STATUS_OFFLINE = "OFFLINE";

    private static final String STAFF_LIST_API = "finds.staff.list";
    private static final String POINT_LOCATE_API = "finds.point.locate";
    private static final int FINDS_PAGE_SIZE = 2000;
    private static final int MAX_FINDS_PAGE_COUNT = 50;
    private static final int FINDS_BATCH_CONCURRENCY = 4;
    private static final int LOCATE_BATCH_SIZE = 100;

    private final FindsOpenApiClient findsOpenApiClient;
    private final PositionPersonLocalLookupService localLookupService;

    public PageResponse<PositionPersonCO> list(PositionPersonPageQry qry) {
        PositionPersonPageQry query = qry == null ? new PositionPersonPageQry() : qry;
        boolean persistedMappings = localLookupService.hasPersonnelMappings();
        List<MatchedPerson> matched = loadMatchedPeople(query, persistedMappings);
        Set<String> idCardNos = new LinkedHashSet<>();
        for (MatchedPerson person : matched) {
            if (StringUtils.hasText(person.local.getIdCardNo())) {
                idCardNos.add(person.local.getIdCardNo());
            }
        }

        Map<String, String> localTerminalNos = localLookupService.findTerminalNosByIdCardNos(idCardNos);
        if (localTerminalNos == null) {
            localTerminalNos = Collections.emptyMap();
        }
        Set<String> terminalNos = new LinkedHashSet<>();
        for (MatchedPerson person : matched) {
            if (!StringUtils.hasText(person.finds.terminalNo)) {
                person.finds.terminalNo = localTerminalNos.get(normalize(person.local.getIdCardNo()));
            }
            if (StringUtils.hasText(person.finds.terminalNo)) {
                terminalNos.add(person.finds.terminalNo);
            }
        }

        Map<String, JsonNode> currentLocations = persistedMappings
                ? Collections.emptyMap() : locateTerminals(terminalNos);
        Map<String, PositionPersonLocalLookupService.TerminalSnapshot> localTerminals =
                localLookupService.findTerminals(terminalNos);
        List<PositionPersonCO> rows = new ArrayList<>();
        for (MatchedPerson person : matched) {
            JsonNode currentLocation = currentLocations.get(person.finds.terminalNo);
            PositionPersonLocalLookupService.TerminalSnapshot terminal =
                    localTerminals.get(person.finds.terminalNo);
            boolean online = persistedMappings
                    ? isStoredOnline(person.finds, terminal) : hasLocation(currentLocation);
            boolean locatedBefore = persistedMappings || online || hasHistoricalLocation(person.finds.lastLocation)
                    || person.finds.lastLocationTime != null
                    || hasHistoricalLocation(terminal);
            if (!locatedBefore) {
                continue;
            }
            String locationStatus = online ? STATUS_ONLINE : STATUS_OFFLINE;
            String positionSource = positionSource(terminal);
            if (!matchesText(query.getLocationStatus(), locationStatus)
                    || !matchesText(query.getPositionSource(), positionSource)) {
                continue;
            }
            rows.add(toClientObject(person, terminal, currentLocation, online, locationStatus, positionSource));
        }

        int pageIndex = Math.max(query.getPageIndex(), 1);
        int pageSize = Math.max(query.getPageSize(), 1);
        int fromIndex = Math.min((pageIndex - 1) * pageSize, rows.size());
        int toIndex = Math.min(fromIndex + pageSize, rows.size());
        return PageResponse.of(rows.subList(fromIndex, toIndex), rows.size(), pageSize, pageIndex);
    }

    private List<MatchedPerson> loadMatchedPeople(PositionPersonPageQry query, boolean persistedMappings) {
        if (persistedMappings) {
            return loadPersistedMatches(query);
        }
        List<FindsStaff> findsStaff = loadAllFindsStaff();
        Set<String> phones = new LinkedHashSet<>();
        for (FindsStaff staff : findsStaff) {
            if (StringUtils.hasText(staff.staffNo)) {
                phones.add(staff.staffNo);
            }
        }
        Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
                localLookupService.findUniquePeopleByPhones(phones);

        List<MatchedPerson> matched = new ArrayList<>();
        Set<Long> matchedUserIds = new LinkedHashSet<>();
        for (FindsStaff staff : findsStaff) {
            PositionPersonLocalLookupService.LocalPerson local = localPeople.get(staff.staffNo);
            if (local == null || !matchesLocalPerson(local, query)) {
                continue;
            }
            if (local.getUserId() != null && !matchedUserIds.add(local.getUserId())) {
                continue;
            }
            matched.add(new MatchedPerson(staff, local));
        }
        return matched;
    }

    private List<MatchedPerson> loadPersistedMatches(PositionPersonPageQry query) {
        List<MatchedPerson> result = new ArrayList<>();
        Set<Long> matchedUserIds = new LinkedHashSet<>();
        for (PositionPersonLocalLookupService.PersonMapping mapping
                : localLookupService.findMatchedPersonnel(query)) {
            PositionPersonLocalLookupService.LocalPerson local = mapping.getLocal();
            if (local == null || (local.getUserId() != null && !matchedUserIds.add(local.getUserId()))) {
                continue;
            }
            FindsStaff staff = new FindsStaff();
            staff.staffNo = mapping.getFindsStaffNo();
            staff.sourceCompanyName = mapping.getFindsCorpinfoName();
            staff.sourceDepartmentName = mapping.getFindsDepartmentName();
            staff.terminalNo = mapping.getFindsTerminalNo();
            staff.locationStatus = mapping.getLocationStatus();
            staff.deviceStatus = mapping.getFindsDeviceStatus();
            result.add(new MatchedPerson(staff, local));
        }
        return result;
    }

    public PositionPersonCO queryByUserId(Long userId) {
        if (userId == null) {
            throw new BizException("本平台用户ID不能为空");
        }
        PositionPersonPageQry qry = new PositionPersonPageQry();
        qry.setUserId(userId);
        qry.setPageIndex(1);
        qry.setPageSize(1);
        PageResponse<PositionPersonCO> response = list(qry);
        if (response.isEmpty()) {
            throw new BizException("定位人员不存在、未匹配本平台人员或从未产生定位记录");
        }
        return response.getData().get(0);
    }

    private List<FindsStaff> loadAllFindsStaff() {
        List<FindsStaff> result = new ArrayList<>();
        JsonNode firstRoot = findsOpenApiClient.postFailFast(STAFF_LIST_API, staffPageRequest(1));
        JsonNode firstData = assertSuccess(firstRoot, STAFF_LIST_API).path("data");
        int firstPageSize = appendFindsStaff(result, extractRows(firstData));
        if (firstPageSize == 0) {
            return result;
        }

        long total = totalValue(firstData, firstPageSize);
        int pageCount = (int) Math.min((total + FINDS_PAGE_SIZE - 1) / FINDS_PAGE_SIZE,
                MAX_FINDS_PAGE_COUNT);
        if (pageCount <= 1) {
            return result;
        }

        List<Map<String, Object>> requests = new ArrayList<>(pageCount - 1);
        for (int pageNo = 2; pageNo <= pageCount; pageNo++) {
            requests.add(staffPageRequest(pageNo));
        }
        List<JsonNode> roots = findsOpenApiClient.postFailFastBatch(
                STAFF_LIST_API, requests, FINDS_BATCH_CONCURRENCY);
        for (JsonNode root : roots) {
            JsonNode data = assertSuccess(root, STAFF_LIST_API).path("data");
            appendFindsStaff(result, extractRows(data));
        }
        return result;
    }

    private Map<String, Object> staffPageRequest(int pageNo) {
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("pageNo", pageNo);
        request.put("pageSize", FINDS_PAGE_SIZE);
        return request;
    }

    private int appendFindsStaff(List<FindsStaff> result, JsonNode rows) {
        if (rows == null || !rows.isArray() || rows.size() == 0) {
            return 0;
        }
        for (JsonNode row : rows) {
            FindsStaff staff = mapFindsStaff(row);
            if (StringUtils.hasText(staff.staffNo)) {
                result.add(staff);
            }
        }
        return rows.size();
    }

    private FindsStaff mapFindsStaff(JsonNode row) {
        FindsStaff staff = new FindsStaff();
        JsonNode terminalInfo = row.path("terminalInfo");
        staff.staffNo = normalize(firstText(
                text(firstPresent(row, "staffNo", "code", "employeeNo", "workNo", "trackNo")),
                text(firstPresent(terminalInfo, "trackNo", "staffNo"))));
        staff.sourceCompanyName = text(firstPresent(row, "companyName", "corpName", "enterpriseName", "orgName"));
        staff.sourceDepartmentName = text(firstPresent(row, "departmentName", "deptName", "orgDepartmentName"));
        staff.lastLocation = firstPresent(row, "lastLocation", "location", "point");
        staff.lastLocationTime = firstLong(locationTime(staff.lastLocation),
                timeMillis(firstPresent(row, "lastReportTime", "lastLocationTime", "lastOnlineTime")));
        staff.terminalNo = firstText(
                text(firstPresent(row, "terminalNo")),
                text(firstPresent(terminalInfo, "terminalNo")),
                text(firstPresent(staff.lastLocation, "terminalNo")),
                text(firstPresent(row.path("terminal"), "terminalNo")),
                text(firstPresent(row.path("bindTerminal"), "terminalNo")),
                terminalNoFromList(row.path("takenTerminalList")));
        return staff;
    }

    private Map<String, JsonNode> locateTerminals(Set<String> terminalNos) {
        if (terminalNos == null || terminalNos.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, JsonNode> result = new LinkedHashMap<>();
        List<String> batch = new ArrayList<>(LOCATE_BATCH_SIZE);
        for (String terminalNo : terminalNos) {
            batch.add(terminalNo);
            if (batch.size() == LOCATE_BATCH_SIZE) {
                locateTerminalBatch(batch, result);
                batch.clear();
            }
        }
        if (!batch.isEmpty()) {
            locateTerminalBatch(batch, result);
        }
        return result;
    }

    private void locateTerminalBatch(List<String> terminalNos, Map<String, JsonNode> result) {
        Map<String, Object> request = Collections.singletonMap("terminalNoList", new ArrayList<>(terminalNos));
        JsonNode root = findsOpenApiClient.postFailFast(POINT_LOCATE_API, request);
        JsonNode data = assertSuccess(root, POINT_LOCATE_API).path("data");
        JsonNode rows = extractRows(data);
        if (rows != null && rows.isArray()) {
            for (JsonNode row : rows) {
                String terminalNo = text(firstPresent(row, "terminalNo"));
                if (!StringUtils.hasText(terminalNo) && terminalNos.size() == 1) {
                    terminalNo = terminalNos.get(0);
                }
                if (StringUtils.hasText(terminalNo)) {
                    result.put(terminalNo, row);
                }
            }
        }
    }

    private PositionPersonCO toClientObject(MatchedPerson person,
                                             PositionPersonLocalLookupService.TerminalSnapshot terminal,
                                             JsonNode currentLocation,
                                             boolean online,
                                             String locationStatus,
                                             String positionSource) {
        PositionPersonCO co = new PositionPersonCO();
        PositionPersonLocalLookupService.LocalPerson local = person.local;
        co.setUserId(local.getUserId());
        co.setStaffName(local.getStaffName());
        co.setLocalStaffNo(local.getLocalStaffNo());
        co.setMobileNo(local.getMobileNo());
        co.setIdCardNo(local.getIdCardNo());
        co.setCorpinfoId(local.getCorpinfoId());
        co.setCorpinfoName(local.getCorpinfoName());
        co.setDepartmentId(local.getDepartmentId());
        co.setDepartmentName(local.getDepartmentName());
        co.setPositionSource(positionSource);
        co.setPositionSourceName(positionSourceName(positionSource));
        co.setSourceCompanyName(person.finds.sourceCompanyName);
        co.setSourceDepartmentName(person.finds.sourceDepartmentName);
        co.setTerminalNo(person.finds.terminalNo);
        co.setLocationStatus(locationStatus);
        co.setLocationStatusName(online ? "\u5728\u7ebf" : "\u79bb\u7ebf");

        JsonNode latest = online ? currentLocation : person.finds.lastLocation;
        co.setLastLocationTime(firstLong(locationTime(latest), person.finds.lastLocationTime,
                terminal == null ? null : terminal.getLastLocationTime()));
        co.setLastLocationName(firstText(locationName(latest),
                terminal == null ? null : terminal.getLastLocationName()));
        if (online) {
            co.setCurrentLocation(firstText(locationName(currentLocation), locationName(person.finds.lastLocation)));
            co.setLon(decimal(firstPresent(currentLocation, "lon", "lng", "longitude")));
            co.setLat(decimal(firstPresent(currentLocation, "lat", "latitude")));
            co.setAlt(decimal(firstPresent(currentLocation, "alt", "altitude")));
            co.setSpeed(decimal(firstPresent(currentLocation, "spd", "speed")));
            co.setDirection(decimal(firstPresent(currentLocation, "dir", "direction")));
        }
        co.setMatchDescription("FindS\u5de5\u53f7\u4e0e\u672c\u5e73\u53f0\u624b\u673a\u53f7\u5339\u914d");
        return co;
    }

    private boolean matchesLocalPerson(PositionPersonLocalLookupService.LocalPerson person,
                                       PositionPersonPageQry query) {
        if (query.getUserId() != null && !query.getUserId().equals(person.getUserId())) {
            return false;
        }
        if (StringUtils.hasText(query.getStaffName())
                && !contains(person.getStaffName(), query.getStaffName())) {
            return false;
        }
        if (query.getCorpinfoId() != null && !query.getCorpinfoId().equals(person.getCorpinfoId())) {
            return false;
        }
        if (StringUtils.hasText(query.getCorpinfoName())
                && !contains(person.getCorpinfoName(), query.getCorpinfoName())) {
            return false;
        }
        if (query.getDepartmentId() != null && !query.getDepartmentId().equals(person.getDepartmentId())) {
            return false;
        }
        return !StringUtils.hasText(query.getDepartmentName())
                || contains(person.getDepartmentName(), query.getDepartmentName());
    }

    private boolean hasLocation(JsonNode location) {
        return decimal(firstPresent(location, "lon", "lng", "longitude")) != null
                && decimal(firstPresent(location, "lat", "latitude")) != null;
    }

    private boolean hasHistoricalLocation(JsonNode location) {
        return hasLocation(location) || locationTime(location) != null || StringUtils.hasText(locationName(location));
    }

    private boolean hasHistoricalLocation(PositionPersonLocalLookupService.TerminalSnapshot terminal) {
        return terminal != null && (terminal.getLastLocationTime() != null
                || StringUtils.hasText(terminal.getLastLocationName()));
    }

    private boolean isStoredOnline(FindsStaff staff,
                                   PositionPersonLocalLookupService.TerminalSnapshot terminal) {
        if (staff != null && StringUtils.hasText(staff.locationStatus)) {
            return STATUS_ONLINE.equalsIgnoreCase(staff.locationStatus);
        }
        String deviceStatus = firstText(
                staff == null ? null : staff.deviceStatus,
                terminal == null ? null : terminal.getDeviceStatus());
        return STATUS_ONLINE.equalsIgnoreCase(deviceStatus)
                || "STATIC".equalsIgnoreCase(deviceStatus)
                || "MOVING".equalsIgnoreCase(deviceStatus);
    }

    private String positionSource(PositionPersonLocalLookupService.TerminalSnapshot terminal) {
        String type = terminal == null ? null : normalize(terminal.getDeviceType());
        if ("CARD".equalsIgnoreCase(type)) {
            return "CARD";
        }
        if ("APP".equalsIgnoreCase(type)) {
            return "APP";
        }
        return "FINDS";
    }

    private String positionSourceName(String source) {
        if ("CARD".equals(source)) {
            return "\u5b9a\u4f4d\u5361\u7cfb\u7edf";
        }
        if ("APP".equals(source)) {
            return "\u624b\u673aAPP\u5b9a\u4f4d";
        }
        return "\u7b2c\u4e09\u65b9\u4eba\u5458\u7cfb\u7edf";
    }

    private String locationName(JsonNode location) {
        return firstText(text(firstPresent(location, "fenceName")),
                text(firstPresent(location, "areaName")),
                text(firstPresent(location, "positionName")),
                text(firstPresent(location, "location")),
                text(firstPresent(location, "address")));
    }

    private Long locationTime(JsonNode location) {
        return timeMillis(firstPresent(location, "gt", "time", "locateTime", "locationTime", "timestamp"));
    }

    private Long timeMillis(JsonNode node) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        if (node.isNumber()) {
            return normalizeEpoch(node.asLong());
        }
        String value = node.asText();
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return normalizeEpoch(Long.parseLong(value.trim()));
        } catch (NumberFormatException ignored) {
            // 数字时间戳解析失败后，继续尝试解析格式化日期。
        }
        String normalized = value.trim().replace(' ', 'T');
        try {
            return OffsetDateTime.parse(normalized).toInstant().toEpochMilli();
        } catch (Exception ignored) {
            try {
                return LocalDateTime.parse(normalized).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            } catch (Exception ignoredAgain) {
                try {
                    return Instant.parse(normalized).toEpochMilli();
                } catch (Exception ignoredFinally) {
                    return null;
                }
            }
        }
    }

    private Long normalizeEpoch(long epoch) {
        return epoch < 100000000000L ? epoch * 1000L : epoch;
    }

    private JsonNode assertSuccess(JsonNode root, String apiName) {
        int code = root == null ? -1 : root.path("code").asInt(-1);
        if (code != 200 && code != 0) {
            String message = root == null ? "" : root.path("msg").asText("");
            throw new BizException("FindS接口调用失败：" + apiName + "，状态码=" + code + "，错误信息=" + message);
        }
        return root;
    }

    private JsonNode extractRows(JsonNode data) {
        if (data == null || data.isNull() || data.isMissingNode()) {
            return null;
        }
        if (data.isArray()) {
            return data;
        }
        return firstArray(data, "data", "rows", "list", "records");
    }

    private JsonNode firstArray(JsonNode node, String... fields) {
        if (node == null) {
            return null;
        }
        for (String field : fields) {
            JsonNode value = node.get(field);
            if (value != null && value.isArray()) {
                return value;
            }
        }
        return null;
    }

    private JsonNode firstPresent(JsonNode node, String... fields) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        for (String field : fields) {
            JsonNode value = node.get(field);
            if (value != null && !value.isNull() && !value.isMissingNode()) {
                return value;
            }
        }
        return null;
    }

    private String terminalNoFromList(JsonNode terminals) {
        if (terminals != null && terminals.isArray()) {
            for (JsonNode terminal : terminals) {
                String terminalNo = text(firstPresent(terminal, "terminalNo"));
                if (StringUtils.hasText(terminalNo)) {
                    return terminalNo;
                }
            }
        }
        return null;
    }

    private long totalValue(JsonNode data, long defaultValue) {
        Long total = longValue(firstPresent(data, "total", "totalCount", "count", "totalRecords"));
        return total == null ? defaultValue : total;
    }

    private String text(JsonNode node) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        return node.isValueNode() ? node.asText() : null;
    }

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

    private BigDecimal decimal(JsonNode node) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return null;
        }
        try {
            return new BigDecimal(node.asText());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }

    private boolean contains(String value, String expected) {
        return value != null && expected != null
                && value.toLowerCase().contains(expected.trim().toLowerCase());
    }

    private boolean matchesText(String expected, String actual) {
        return !StringUtils.hasText(expected) || expected.trim().equalsIgnoreCase(actual);
    }

    private String normalize(String value) {
        return value == null ? null : value.trim();
    }

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

    private Long firstLong(Long... values) {
        for (Long value : values) {
            if (value != null) {
                return value;
            }
        }
        return null;
    }

    private static class FindsStaff {
        private String staffNo;
        private String sourceCompanyName;
        private String sourceDepartmentName;
        private String terminalNo;
        private String locationStatus;
        private String deviceStatus;
        private Long lastLocationTime;
        private JsonNode lastLocation;
    }

    private static class MatchedPerson {
        private final FindsStaff finds;
        private final PositionPersonLocalLookupService.LocalPerson local;

        private MatchedPerson(FindsStaff finds, PositionPersonLocalLookupService.LocalPerson local) {
            this.finds = finds;
            this.local = local;
        }
    }
}
