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

import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jjb.saas.framework.auth.model.SSOUser;
import com.jjb.saas.framework.auth.utils.AuthContext;
import com.zcloud.personnel.positioning.dto.BiAreaFenceQry;
import com.zcloud.personnel.positioning.dto.BiCompanyFenceQry;
import com.zcloud.personnel.positioning.dto.BiCompanyOnlinePersonQry;
import com.zcloud.personnel.positioning.dto.BiPersonLocationQry;
import com.zcloud.personnel.positioning.dto.BiPersonPositionQry;
import com.zcloud.personnel.positioning.dto.BiPersonTraceQry;
import com.zcloud.personnel.positioning.dto.clientobject.BiAreaFenceCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiCompanyLocationCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiCompanyOnlinePersonCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiCompanyOnlinePersonStatCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiOverviewCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiPersonLocationCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiPersonPositionCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiPersonTraceCO;
import com.zcloud.personnel.positioning.dto.clientobject.BiPortStatCO;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import com.zcloud.personnel.positioning.persistence.dataobject.AreaDO;
import com.zcloud.personnel.positioning.persistence.dataobject.BiPersonPositionUserDO;
import com.zcloud.personnel.positioning.persistence.dataobject.FenceDO;
import com.zcloud.personnel.positioning.persistence.repository.AreaRepository;
import com.zcloud.personnel.positioning.persistence.repository.FenceRepository;
import com.zcloud.personnel.positioning.persistence.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * BI large-screen query executor.
 *
 * @Author fangjiakai
 * @Date 2026-07-07
 */
@Component
@AllArgsConstructor
public class PersonnelPositioningBiQueryExe {
    private static final String STAFF_LIST_API = "finds.staff.list";
    private static final String STAFF_TERMINAL_QUERY_API = "finds.staff.taken.terminal.query";
    private static final String POINT_LOCATE_API = "finds.point.locate";
    private static final String POINT_TRACE_API = "finds.point.trace";
    private static final String CORP_INFO_SELECT = "SELECT corpinfo_id, corp_name, code, address, address_business, "
            + "address_office, contacts, lr_name, corp_type_name, corp_type2_name, corp_type3_name, corp_type4_name, "
            + "longitude, latitude, port_area FROM corp_info WHERE (delete_enum IS NULL OR delete_enum = 'FALSE')";
    private static final String OPEN_ALARM_WHERE = "(delete_enum IS NULL OR delete_enum = 'FALSE') "
            + "AND (dispose_status IS NULL OR dispose_status <> 'CLOSE')";
    private static final int DEFAULT_LIST_ALL_PAGE_SIZE = 200;
    private static final int MAX_LIST_ALL_PAGE_COUNT = 50;
    private static final int MAX_LOCATE_BATCH_SIZE = 100;
    private static final long MAX_TRACE_RANGE_MS = 24 * 60 * 60 * 1000L;
    private static final String AREA_TYPE_PORT = "PORT";
    private static final String AREA_TYPE_COMPANY = "COMPANY_AREA";

    private final FindsOpenApiClient findsOpenApiClient;
    private final ObjectMapper objectMapper;
    private final JdbcTemplate jdbcTemplate;
    private final AreaRepository areaRepository;
    private final FenceRepository fenceRepository;
    private final UserRepository userRepository;
    private final BiPersonLocationProvider biPersonLocationProvider;
    private final BiOverviewCache biOverviewCache;
    private final PositionPersonLocalLookupService localLookupService;

    public BiOverviewCO queryOverview() {
        String cacheKey = overviewCacheKey();
        BiOverviewCO cached = biOverviewCache.get(cacheKey);
        if (cached != null) {
            return cached;
        }
        List<BiPersonLocationCO> personList = loadMatchedPersonLocations(new BiPersonLocationQry());
        List<BiCompanyLocationCO> companyList = listCompanyLocation(personList, null);
        List<BiPortStatCO> portList = buildPortStats(companyList);
        BiOverviewCO co = new BiOverviewCO();
        co.setCompanyList(companyList);
        co.setPortList(portList);
        co.setPortCount(portList.size());
        co.setCompanyCount(companyList.size());
        co.setFenceCount(0);
        co.setCompanyAreaCount(0);
        co.setWorkFenceCount(0);
        for (BiCompanyLocationCO company : companyList) {
            co.setFenceCount(co.getFenceCount() + safeInt(company.getFenceCount()));
            co.setCompanyAreaCount(co.getCompanyAreaCount() + safeInt(company.getCompanyAreaCount()));
            co.setWorkFenceCount(co.getWorkFenceCount() + safeInt(company.getWorkFenceCount()));
        }
        co.setOnlineCount(countOnlinePersons(personList));
        co.setAlarmCount(queryOpenAlarmCount());
        co.setAlarmPersonCount(queryOpenAlarmPersonCount());
        biOverviewCache.put(cacheKey, co);
        return co;
    }

    public List<BiCompanyLocationCO> listCompanyLocation(Integer portArea) {
        validatePortArea(portArea);
        return listCompanyLocation(loadMatchedPersonLocations(new BiPersonLocationQry()), portArea);
    }

    private List<BiCompanyLocationCO> listCompanyLocation(List<BiPersonLocationCO> personList, Integer portArea) {
        Map<String, CompanyMarkerAccumulator> accumulatorMap = new LinkedHashMap<>();
        for (BiAreaFenceCO fence : listAllAreaFence()) {
            String companyName = firstText(fence.getCorpinfoName(), fence.getOrgName(), "未归属组织");
            String companyKey = fence.getCorpinfoId() == null
                    ? "NAME:" + companyName : "ID:" + fence.getCorpinfoId();
            CompanyMarkerAccumulator accumulator = accumulatorMap.computeIfAbsent(companyKey,
                    key -> new CompanyMarkerAccumulator(companyName));
            accumulator.addFence(fence);
        }
        List<BiCompanyLocationCO> fenceCompanies = new ArrayList<>();
        for (CompanyMarkerAccumulator accumulator : accumulatorMap.values()) {
            fenceCompanies.add(accumulator.toCO());
        }
        List<BiCompanyLocationCO> result = mergeCorpInfoCompanies(fenceCompanies, portArea);
        fillCompanyAlarmCounts(result);
        fillCompanyOnlineCounts(result, personList);
        return result;
    }

    private void validatePortArea(Integer portArea) {
        if (portArea != null && (portArea < 1 || portArea > 3)) {
            throw new BizException("portArea只支持1、2、3");
        }
    }

    private List<BiCompanyLocationCO> mergeCorpInfoCompanies(List<BiCompanyLocationCO> fenceCompanies,
                                                              Integer portArea) {
        Map<String, BiCompanyLocationCO> companiesById = new LinkedHashMap<>();
        Map<String, BiCompanyLocationCO> companiesByName = new LinkedHashMap<>();
        for (BiCompanyLocationCO company : fenceCompanies) {
            if (StringUtils.hasText(company.getCorpinfoId())) {
                companiesById.putIfAbsent(company.getCorpinfoId(), company);
            }
            String companyName = firstText(company.getCorpinfoName(), company.getCompanyName());
            if (StringUtils.hasText(companyName)) {
                companiesByName.putIfAbsent(companyName, company);
            }
        }

        Set<BiCompanyLocationCO> mergedFenceCompanies = Collections.newSetFromMap(new IdentityHashMap<>());
        List<BiCompanyLocationCO> result = new ArrayList<>();
        for (CorpInfoView corpInfo : listCorpInfos(portArea)) {
            BiCompanyLocationCO company = companiesById.get(corpInfo.corpinfoId);
            if (company == null) {
                company = companiesByName.get(corpInfo.corpName);
            }
            if (company == null || mergedFenceCompanies.contains(company)) {
                company = emptyCompanyLocation();
            } else {
                mergedFenceCompanies.add(company);
            }
            applyCorpInfo(company, corpInfo);
            applyPortArea(company, corpInfo.portArea);
            result.add(company);
        }

        if (portArea == null) {
            for (BiCompanyLocationCO company : fenceCompanies) {
                if (!mergedFenceCompanies.contains(company)) {
                    result.add(company);
                }
            }
        }
        return result;
    }

    private List<CorpInfoView> listCorpInfos(Integer portArea) {
        StringBuilder sql = new StringBuilder(CORP_INFO_SELECT);
        List<Object> args = new ArrayList<>();
        if (portArea != null) {
            sql.append(" AND port_area = ?");
            args.add(portArea);
        }
        sql.append(" ORDER BY corp_name, corpinfo_id");
        return jdbcTemplate.query(sql.toString(), this::mapCorpInfo, args.toArray());
    }

    private BiCompanyLocationCO emptyCompanyLocation() {
        BiCompanyLocationCO company = new BiCompanyLocationCO();
        company.setFenceCount(0);
        company.setCompanyAreaCount(0);
        company.setWorkFenceCount(0);
        company.setAlarmCount(0);
        company.setOnlineCount(0);
        return company;
    }

    private void applyPortArea(BiCompanyLocationCO company, Integer portArea) {
        company.setPortArea(portArea);
        if (portArea == null) {
            return;
        }
        if (portArea == 1) {
            company.setPortCode("QHD");
            company.setPortName("秦皇岛港区");
        } else if (portArea == 2) {
            company.setPortCode("EAST");
            company.setPortName("东港区");
        } else if (portArea == 3) {
            company.setPortCode("WEST");
            company.setPortName("西港区");
        }
    }

    public PageResponse<BiAreaFenceCO> listCompanyFence(BiCompanyFenceQry qry) {
        BiCompanyFenceQry query = qry == null ? new BiCompanyFenceQry() : qry;
        List<BiAreaFenceCO> matched = new ArrayList<>();
        for (BiAreaFenceCO fence : listAllAreaFence()) {
            if (matchesCompanyFence(fence, query)) {
                matched.add(fence);
            }
        }
        int pageNo = Math.max(query.getPageIndex(), 1);
        int pageSize = Math.max(query.getPageSize(), 1);
        int fromIndex = Math.min((pageNo - 1) * pageSize, matched.size());
        int toIndex = Math.min(fromIndex + pageSize, matched.size());
        return PageResponse.of(matched.subList(fromIndex, toIndex), matched.size(), pageSize, pageNo);
    }

    public PageResponse<BiPersonLocationCO> listPersonLocation(BiPersonLocationQry qry) {
        BiPersonLocationQry query = qry == null ? new BiPersonLocationQry() : qry;
        int pageNo = Math.max(query.getPageIndex(), 1);
        int pageSize = Math.max(query.getPageSize(), 1);
        List<BiPersonLocationCO> rows = loadMatchedPersonLocations(query);
        int fromIndex = Math.min((pageNo - 1) * pageSize, rows.size());
        int toIndex = Math.min(fromIndex + pageSize, rows.size());
        return PageResponse.of(rows.subList(fromIndex, toIndex), rows.size(), pageSize, pageNo);
    }

    public List<BiPersonPositionCO> listPersonPosition(BiPersonPositionQry qry) {
        if (qry == null || qry.getCorpinfoId() == null) {
            throw new BizException("企业id不能为空");
        }
        String corpinfoId = String.valueOf(qry.getCorpinfoId());
        List<BiPersonPositionArea> areas = loadBiPersonPositionAreas(corpinfoId);
        Map<String, BiCompanyOnlinePersonCO> locationMap = loadBiPersonPositionLocationMap(corpinfoId);
        List<BiPersonPositionCO> result = new ArrayList<>();
        for (BiPersonPositionUserDO user : userRepository.listBiPersonPositionUsers(corpinfoId)) {
            BiCompanyOnlinePersonCO location = findBiPersonPositionLocation(user, locationMap);
            result.add(buildBiPersonPositionRow(user, location, areas));
        }
        return result;
    }

    private List<BiPersonPositionArea> loadBiPersonPositionAreas(String corpinfoId) {
        List<AreaDO> rows = areaRepository.listBiPersonPositionAreas(corpinfoId);
        List<BiPersonPositionArea> result = new ArrayList<>();
        for (AreaDO row : rows) {
            result.add(new BiPersonPositionArea(row, parseBiPersonPositionPolygon(row.getPolygonJson())));
        }
        return result;
    }

    private Map<String, BiCompanyOnlinePersonCO> loadBiPersonPositionLocationMap(String corpinfoId) {
        List<BiCompanyOnlinePersonCO> locations =
                biPersonLocationProvider.listCompanyOnlinePersons(corpinfoId, null, null);
        Map<String, BiCompanyOnlinePersonCO> result = new LinkedHashMap<>();
        for (BiCompanyOnlinePersonCO location : locations) {
            putBiPersonPositionLocation(result, location.getUserId() == null ? null : String.valueOf(location.getUserId()), location);
            putBiPersonPositionLocation(result, location.getStaffNo(), location);
            putBiPersonPositionLocation(result, location.getMobileNo(), location);
        }
        return result;
    }

    private void putBiPersonPositionLocation(Map<String, BiCompanyOnlinePersonCO> locationMap,
                                             String key,
                                             BiCompanyOnlinePersonCO location) {
        if (StringUtils.hasText(key)) {
            locationMap.putIfAbsent(key.trim(), location);
        }
    }

    private BiCompanyOnlinePersonCO findBiPersonPositionLocation(BiPersonPositionUserDO user,
                                                                  Map<String, BiCompanyOnlinePersonCO> locationMap) {
        if (user == null || locationMap == null || locationMap.isEmpty()) {
            return null;
        }
        BiCompanyOnlinePersonCO location = locationMap.get(String.valueOf(user.getUserId()));
        if (location == null) {
            location = locationMap.get(user.getUsername());
        }
        if (location == null) {
            location = locationMap.get(user.getMobileNo());
        }
        if (location == null) {
            location = locationMap.get(user.getIdCardNo());
        }
        return location;
    }

    private BiPersonPositionCO buildBiPersonPositionRow(BiPersonPositionUserDO user,
                                                        BiCompanyOnlinePersonCO location,
                                                        List<BiPersonPositionArea> areas) {
        BiPersonPositionCO co = new BiPersonPositionCO();
        co.setUserId(user.getUserId());
        co.setPersonName(user.getPersonName());
        co.setCorpinfoId(user.getCorpinfoId());
        co.setCorpinfoName(user.getCorpinfoName());
        if (location == null) {
            co.setOnlineStatus("OFFLINE");
            co.setOnlineStatusName("离线");
            return co;
        }
        co.setOnlineStatus("ONLINE");
        co.setOnlineStatusName("在线");
        co.setLon(location.getLon());
        co.setLat(location.getLat());
        co.setLastLocationTime(location.getLastLocationTime());
        fillBiPersonPositionArea(co, location, areas);
        return co;
    }

    private void fillBiPersonPositionArea(BiPersonPositionCO co,
                                          BiCompanyOnlinePersonCO location,
                                          List<BiPersonPositionArea> areas) {
        if (location == null || location.getLon() == null || location.getLat() == null) {
            return;
        }
        for (BiPersonPositionArea area : areas) {
            if (isBiPersonPositionInArea(location.getLon(), location.getLat(), area.points)) {
                co.setAreaId(area.area.getId());
                co.setAreaName(area.area.getAreaName());
                return;
            }
        }
    }

    private List<BigDecimal[]> parseBiPersonPositionPolygon(String polygonJson) {
        List<BigDecimal[]> points = new ArrayList<>();
        if (!StringUtils.hasText(polygonJson)) {
            return points;
        }
        try {
            collectBiPersonPositionPoints(objectMapper.readTree(polygonJson), points);
        } catch (IOException e) {
            throw new BizException("区域边界坐标JSON格式错误");
        }
        return points;
    }

    private void collectBiPersonPositionPoints(JsonNode node, List<BigDecimal[]> points) {
        if (node == null || node.isNull() || node.isMissingNode()) {
            return;
        }
        if (node.isObject()) {
            node.elements().forEachRemaining(child -> collectBiPersonPositionPoints(child, points));
            return;
        }
        if (!node.isArray()) {
            return;
        }
        if (node.size() >= 2 && node.get(0).isNumber() && node.get(1).isNumber()) {
            points.add(new BigDecimal[]{node.get(0).decimalValue(), node.get(1).decimalValue()});
            return;
        }
        for (JsonNode child : node) {
            collectBiPersonPositionPoints(child, points);
        }
    }

    private boolean isBiPersonPositionInArea(BigDecimal lon, BigDecimal lat, List<BigDecimal[]> points) {
        if (lon == null || lat == null || points == null || points.size() < 3) {
            return false;
        }
        boolean inside = false;
        double x = lon.doubleValue();
        double y = lat.doubleValue();
        for (int i = 0, j = points.size() - 1; i < points.size(); j = i++) {
            double xi = points.get(i)[0].doubleValue();
            double yi = points.get(i)[1].doubleValue();
            double xj = points.get(j)[0].doubleValue();
            double yj = points.get(j)[1].doubleValue();
            if ((yi > y) != (yj > y)
                    && x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
                inside = !inside;
            }
        }
        return inside;
    }

    public BiCompanyOnlinePersonStatCO queryCompanyOnlinePersonStat(BiCompanyOnlinePersonQry qry) {
        BiCompanyOnlinePersonQry query = qry == null ? new BiCompanyOnlinePersonQry() : qry;
        validatePortArea(query.getPortArea());
        List<CorpInfoView> corpInfos = queryCompanyOnlineCorpInfos(query);
        List<BiCompanyOnlinePersonCO> personList = new ArrayList<>();
        for (CorpInfoView corpInfo : corpInfos) {
            personList.addAll(biPersonLocationProvider.listCompanyOnlinePersons(
                    corpInfo.corpinfoId, corpInfo.corpName, corpInfo.code));
        }
        personList.sort((left, right) -> {
            long leftTime = left.getLastLocationTime() == null ? 0L : left.getLastLocationTime();
            long rightTime = right.getLastLocationTime() == null ? 0L : right.getLastLocationTime();
            return Long.compare(rightTime, leftTime);
        });
        BiCompanyOnlinePersonStatCO co = new BiCompanyOnlinePersonStatCO();
        co.setPersonList(personList);
        co.setOnlineCount(personList.size());
        int internalCount = 0;
        for (BiCompanyOnlinePersonCO person : personList) {
            if (Boolean.TRUE.equals(person.getInternalEmployee())) {
                internalCount++;
            }
        }
        co.setInternalCount(internalCount);
        co.setTemporaryCount(personList.size() - internalCount);
        return co;
    }

    private List<CorpInfoView> queryCompanyOnlineCorpInfos(BiCompanyOnlinePersonQry qry) {
        if (qry.getCorpinfoId() != null) {
            CorpInfoView corpInfo = queryCorpInfoById(String.valueOf(qry.getCorpinfoId()));
            if (corpInfo == null) {
                throw new BizException("未找到对应企业信息");
            }
            if (qry.getPortArea() != null && !qry.getPortArea().equals(corpInfo.portArea)) {
                return Collections.emptyList();
            }
            return Collections.singletonList(corpInfo);
        }
        return listCorpInfos(qry.getPortArea());
    }

    private List<BiPersonLocationCO> loadMatchedPersonLocations(BiPersonLocationQry query) {
        List<StaffSnapshot> staffRows = loadAllStaffSnapshots();
        Set<String> phones = new LinkedHashSet<>();
        for (StaffSnapshot staff : staffRows) {
            if (StringUtils.hasText(staff.staffNo)) {
                phones.add(staff.staffNo);
            }
        }
        Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
                localLookupService.findUniquePeopleByPhones(phones);

        List<MatchedStaffSnapshot> matchedStaff = new ArrayList<>();
        Set<Long> matchedUserIds = new HashSet<>();
        Set<String> terminalNos = new LinkedHashSet<>();
        for (StaffSnapshot staff : staffRows) {
            PositionPersonLocalLookupService.LocalPerson local = localPeople.get(staff.staffNo);
            if (local == null) {
                continue;
            }
            if (local.getUserId() != null && !matchedUserIds.add(local.getUserId())) {
                continue;
            }
            if (!StringUtils.hasText(staff.terminalNo)) {
                staff.terminalNo = resolveTerminalNo(staff.staffNo, staff.idCardNo, staff.mobileNo, false);
            }
            if (StringUtils.hasText(query.getTerminalNo())
                    && !query.getTerminalNo().equals(staff.terminalNo)) {
                continue;
            }
            if (StringUtils.hasText(staff.terminalNo)) {
                terminalNos.add(staff.terminalNo);
            }
            matchedStaff.add(new MatchedStaffSnapshot(staff, local));
        }

        List<StaffSnapshot> matchedSnapshots = new ArrayList<>();
        for (MatchedStaffSnapshot matched : matchedStaff) {
            matchedSnapshots.add(matched.staff);
        }
        Map<String, JsonNode> locationMap = locateStaffRows(matchedSnapshots, query.getTerminalNo());
        Map<String, PositionPersonLocalLookupService.TerminalSnapshot> localTerminals =
                localLookupService.findTerminals(terminalNos);
        List<BiPersonLocationCO> rows = new ArrayList<>();
        List<BiAreaFenceCO> fences = listAllAreaFence();
        for (MatchedStaffSnapshot matched : matchedStaff) {
            StaffSnapshot staff = matched.staff;
            String terminalNo = staff.terminalNo;
            JsonNode location = locationMap.get(terminalNo);
            BiPersonLocationCO co = mapPersonLocation(staff, matched.local, terminalNo,
                    localTerminals.get(terminalNo), location, fences);
            if (matchesPersonLocation(co, query)) {
                rows.add(co);
            }
        }
        return rows;
    }

    private List<StaffSnapshot> loadAllStaffSnapshots() {
        List<StaffSnapshot> result = new ArrayList<>();
        long total = Long.MAX_VALUE;
        int loaded = 0;
        for (int pageNo = 1; pageNo <= MAX_LIST_ALL_PAGE_COUNT && loaded < total; pageNo++) {
            BiPersonLocationQry query = new BiPersonLocationQry();
            JsonNode root = findsOpenApiClient.post(STAFF_LIST_API,
                    buildStaffListRequest(query, pageNo, DEFAULT_LIST_ALL_PAGE_SIZE));
            JsonNode data = assertSuccess(root, STAFF_LIST_API).path("data");
            List<StaffSnapshot> rows = mapStaffSnapshots(data);
            if (rows.isEmpty()) {
                break;
            }
            result.addAll(rows);
            loaded += rows.size();
            total = totalValue(data, loaded);
        }
        return result;
    }

    public BiPersonTraceCO queryPersonTrace(BiPersonTraceQry qry) {
        validateTraceQuery(qry);
        String terminalNo = resolveTerminalNo(qry);
        Map<String, Object> request = new HashMap<>();
        request.put("terminalNo", terminalNo);
        request.put("startTime", qry.getStartTime());
        request.put("endTime", qry.getEndTime());
        JsonNode root = findsOpenApiClient.post(POINT_TRACE_API, request);
        JsonNode data = assertSuccess(root, POINT_TRACE_API).path("data");
        BiPersonTraceCO co = new BiPersonTraceCO();
        co.setTerminalNo(terminalNo);
        co.setStaffNo(qry.getStaffNo());
        co.setIdCardNo(qry.getIdCardNo());
        co.setMobileNo(qry.getMobileNo());
        co.setStartTime(qry.getStartTime());
        co.setEndTime(qry.getEndTime());
        co.setPoints(mapTracePoints(data));
        co.setTotal(data.path("total").isNumber() ? data.path("total").asInt() : co.getPoints().size());
        co.setRawJson(jsonValue(root));
        return co;
    }

    public PageResponse<BiAreaFenceCO> listAreaFence(BiAreaFenceQry qry) {
        BiAreaFenceQry query = qry == null ? new BiAreaFenceQry() : qry;
        int pageNo = Math.max(query.getPageIndex(), 1);
        int pageSize = Math.max(query.getPageSize(), 1);
        QueryWrapper<FenceDO> wrapper = localFenceQuery(query.getId(), query.getNameLike());
        long total = fenceRepository.count(wrapper);
        wrapper.orderByDesc("update_time", "id");
        wrapper.last("LIMIT " + pageSize + " OFFSET " + ((pageNo - 1) * pageSize));
        List<BiAreaFenceCO> rows = mapLocalFences(fenceRepository.list(wrapper));
        return PageResponse.of(rows, safeLongToInt(total), pageSize, pageNo);
    }

    public List<BiAreaFenceCO> listAllAreaFence() {
        QueryWrapper<FenceDO> wrapper = localFenceQuery(null, null);
        wrapper.orderByDesc("update_time", "id");
        return mapLocalFences(fenceRepository.list(wrapper));
    }

    public BiAreaFenceCO queryAreaFenceById(Long id) {
        if (id == null) {
            throw new BizException("围栏ID不能为空");
        }
        QueryWrapper<FenceDO> wrapper = localFenceQuery(id, null);
        wrapper.last("LIMIT 1");
        return mapLocalFence(fenceRepository.getOne(wrapper, false), loadAreaSnapshots());
    }

    private QueryWrapper<FenceDO> localFenceQuery(Long id, String nameLike) {
        QueryWrapper<FenceDO> wrapper = new QueryWrapper<>();
        wrapper.and(value -> value.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        if (id != null) {
            wrapper.and(value -> value.eq("id", id).or().eq("finds_fence_id", id));
        }
        if (StringUtils.hasText(nameLike)) {
            wrapper.like("fence_name", nameLike.trim());
        }
        return wrapper;
    }

    private List<BiAreaFenceCO> mapLocalFences(List<FenceDO> fences) {
        List<BiAreaFenceCO> result = new ArrayList<>();
        if (fences == null || fences.isEmpty()) {
            return result;
        }
        Map<Long, AreaSnapshot> areas = loadAreaSnapshots();
        for (FenceDO fence : fences) {
            result.add(mapLocalFence(fence, areas));
        }
        return result;
    }

    private BiAreaFenceCO mapLocalFence(FenceDO fence, Map<Long, AreaSnapshot> areas) {
        if (fence == null) {
            return null;
        }
        BiAreaFenceCO co = new BiAreaFenceCO();
        co.setId(fence.getFindsFenceId() == null ? fence.getId() : fence.getFindsFenceId());
        co.setLocalFenceId(fence.getId());
        co.setFindsFenceId(fence.getFindsFenceId());
        co.setName(fence.getFenceName());
        co.setCode(firstText(fence.getFindsFenceCode(), fence.getFenceCode()));
        co.setFenceScope(fence.getFenceScope());
        co.setStatus(fence.getStatus());
        co.setCorpinfoId(fence.getCorpinfoId());
        co.setCorpinfoName(fence.getCorpinfoName());
        co.setAreaId(fence.getAreaId());
        co.setAreaName(fence.getAreaName());
        BiAreaFenceCO.FenceBiz fenceBiz = new BiAreaFenceCO.FenceBiz();
        fenceBiz.setName("COMPANY_AREA".equalsIgnoreCase(fence.getFenceScope())
                ? "公司区域" : fence.getFenceBizName());
        fenceBiz.setBelongType("USER");
        fenceBiz.setEnable("ENABLED".equalsIgnoreCase(fence.getStatus()));
        co.setFenceBiz(fenceBiz);
        co.setSilence(fence.getSilence() != null && fence.getSilence() == 1);
        co.setSpaceType(fence.getSpaceType());
        co.setEffectPeriodJson(fence.getEffectPeriodJson());
        co.setEffectPeriod(parseJsonValue(fence.getEffectPeriodJson()));
        co.setPolygonJson(fence.getPolygonJson());
        co.setPolygon(parseJsonValue(fence.getPolygonJson()));
        co.setCenterLon(fence.getCenterLon());
        co.setCenterLat(fence.getCenterLat());
        co.setBuildingId(fence.getBuildingId());
        co.setFloor(fence.getFloor());
        co.setOrgName(firstText(fence.getOrgName(), fence.getCorpinfoName()));
        co.setOrgCode(fence.getOrgCode());
        co.setCreditCode(fence.getOrgCode());
        co.setRawJson(fence.getRawJson());
        AreaSnapshot port = resolvePortArea(fence, areas);
        if (port != null) {
            co.setPortId(port.id);
            co.setPortCode(port.areaCode);
            co.setPortName(port.areaName);
        }
        return co;
    }

    private Map<Long, AreaSnapshot> loadAreaSnapshots() {
        Map<Long, AreaSnapshot> result = new LinkedHashMap<>();
        jdbcTemplate.query("SELECT id, area_code, area_name, area_type, parent_id, parent_code, "
                        + "corpinfo_id, corpinfo_name, center_lon, center_lat, polygon_json FROM area "
                        + "WHERE (delete_enum IS NULL OR delete_enum = 'FALSE')",
                rs -> {
                    AreaSnapshot area = new AreaSnapshot();
                    long id = rs.getLong("id");
                    area.id = rs.wasNull() ? null : id;
                    area.areaCode = rs.getString("area_code");
                    area.areaName = rs.getString("area_name");
                    area.areaType = rs.getString("area_type");
                    long parentId = rs.getLong("parent_id");
                    area.parentId = rs.wasNull() ? null : parentId;
                    area.parentCode = rs.getString("parent_code");
                    String corpinfoId = rs.getString("corpinfo_id");
                    area.corpinfoId = rs.wasNull() ? null : corpinfoId;
                    area.corpinfoName = rs.getString("corpinfo_name");
                    area.centerLon = rs.getBigDecimal("center_lon");
                    area.centerLat = rs.getBigDecimal("center_lat");
                    area.polygonJson = rs.getString("polygon_json");
                    if (area.id != null) {
                        result.put(area.id, area);
                    }
                });
        return result;
    }

    private AreaSnapshot resolvePortArea(FenceDO fence, Map<Long, AreaSnapshot> areas) {
        AreaSnapshot port = findPortAncestor(areas.get(fence.getAreaId()), areas);
        if (port != null) {
            return port;
        }
        for (AreaSnapshot area : areas.values()) {
            if (!AREA_TYPE_COMPANY.equalsIgnoreCase(area.areaType)) {
                continue;
            }
            boolean matchesId = fence.getCorpinfoId() != null && fence.getCorpinfoId().equals(area.corpinfoId);
            boolean matchesName = StringUtils.hasText(fence.getCorpinfoName())
                    && fence.getCorpinfoName().equals(area.corpinfoName);
            if (matchesId || matchesName) {
                port = findPortAncestor(area, areas);
                if (port != null) {
                    return port;
                }
            }
        }
        if (fence.getCenterLon() == null || fence.getCenterLat() == null) {
            return null;
        }
        for (AreaSnapshot area : areas.values()) {
            if (!AREA_TYPE_PORT.equalsIgnoreCase(area.areaType) || !StringUtils.hasText(area.polygonJson)) {
                continue;
            }
            try {
                List<BigDecimal[]> points = new ArrayList<>();
                collectPoints(objectMapper.readTree(area.polygonJson), points);
                if (pointInPolygon(fence.getCenterLon(), fence.getCenterLat(), points)) {
                    return area;
                }
            } catch (IOException ignored) {
                // Invalid port polygon data is ignored; the port remains unassigned.
            }
        }
        return null;
    }

    private AreaSnapshot findPortAncestor(AreaSnapshot area, Map<Long, AreaSnapshot> areas) {
        Set<Long> visited = new HashSet<>();
        AreaSnapshot current = area;
        while (current != null && current.id != null && visited.add(current.id)) {
            if (AREA_TYPE_PORT.equalsIgnoreCase(current.areaType)) {
                return current;
            }
            current = current.parentId == null
                    ? findAreaByCode(current.parentCode, areas) : areas.get(current.parentId);
        }
        return null;
    }

    private AreaSnapshot findAreaByCode(String areaCode, Map<Long, AreaSnapshot> areas) {
        if (!StringUtils.hasText(areaCode)) {
            return null;
        }
        for (AreaSnapshot area : areas.values()) {
            if (areaCode.equals(area.areaCode)) {
                return area;
            }
        }
        return null;
    }

    private Object parseJsonValue(String json) {
        if (!StringUtils.hasText(json)) {
            return null;
        }
        try {
            return objectMapper.readValue(json, Object.class);
        } catch (IOException ignored) {
            return null;
        }
    }

    private String overviewCacheKey() {
        SSOUser user = AuthContext.getCurrentUser();
        return user == null || user.getCompanyId() == null ? "ALL" : String.valueOf(user.getCompanyId());
    }

    private Map<String, Object> buildStaffListRequest(BiPersonLocationQry qry, int pageNo, int pageSize) {
        Map<String, Object> request = new HashMap<>();
        CorpInfoView corpInfo = queryCorpInfo(qry.getCreditCode(), qry.getOrgCode(),
                firstText(qry.getCompanyName(), qry.getOrgName()));
        putIfText(request, "orgCode", qry.getOrgCode());
        putIfText(request, "orgName", firstText(qry.getOrgName(), qry.getCompanyName(),
                corpInfo == null ? null : corpInfo.corpName));
        putIfText(request, "staffName", qry.getStaffName());
        putIfText(request, "nameLike", qry.getStaffName());
        putIfText(request, "staffNo", qry.getStaffNo());
        putIfText(request, "idCardNo", qry.getIdCardNo());
        putIfText(request, "mobileNo", qry.getMobileNo());
        request.put("pageNo", pageNo);
        request.put("pageSize", pageSize);
        return request;
    }

    private List<StaffSnapshot> mapStaffSnapshots(JsonNode data) {
        JsonNode rows = extractRows(data);
        List<StaffSnapshot> result = new ArrayList<>();
        if (!rows.isArray()) {
            return result;
        }
        for (JsonNode row : rows) {
            result.add(mapStaffSnapshot(row));
        }
        return result;
    }

    private StaffSnapshot mapStaffSnapshot(JsonNode row) {
        StaffSnapshot snapshot = new StaffSnapshot();
        snapshot.rawNode = row;
        JsonNode terminalInfo = child(row, "terminalInfo");
        snapshot.staffName = textValue(firstPresentAny(child(row, "staffName"), child(row, "name"),
                child(row, "realName"), child(row, "userName")));
        snapshot.staffNo = textValue(firstPresentAny(child(row, "staffNo"), child(row, "code"),
                child(row, "employeeNo"), child(row, "workNo"), child(row, "trackNo"),
                child(terminalInfo, "trackNo"), child(terminalInfo, "staffNo")));
        snapshot.idCardNo = textValue(firstPresentAny(child(row, "idCardNo"), child(row, "identityNo"),
                child(row, "certificateNo"), child(row, "cardNo")));
        snapshot.mobileNo = textValue(firstPresentAny(child(row, "mobileNo"), child(row, "phone"),
                child(row, "phoneNo"), child(row, "tel")));
        snapshot.orgName = textValue(firstPresentAny(child(row, "orgName"), child(row, "companyName"),
                child(row, "corpName"), child(row, "enterpriseName"), child(row, "organizationName")));
        snapshot.orgCode = textValue(firstPresentAny(child(row, "orgCode"), child(row, "companyCode"),
                child(row, "corpCode"), child(row, "enterpriseCode")));
        snapshot.creditCode = textValue(firstPresentAny(child(row, "creditCode"), child(row, "socialCreditCode"),
                child(row, "unifiedSocialCreditCode"), child(row, "unifiedCreditCode"), child(row, "creditNo")));
        snapshot.lastLocation = firstPresentAny(child(row, "lastLocation"), child(row, "location"), child(row, "point"));
        snapshot.terminalNo = firstText(textValue(child(row, "terminalNo")),
                textValue(child(terminalInfo, "terminalNo")),
                textValue(child(snapshot.lastLocation, "terminalNo")),
                textValue(child(child(row, "bindTerminal"), "terminalNo")),
                textValue(child(child(row, "terminal"), "terminalNo")),
                findTerminalNo(child(row, "takenTerminalList")));
        return snapshot;
    }

    private Map<String, JsonNode> locateStaffRows(List<StaffSnapshot> staffRows, String overrideTerminalNo) {
        Map<String, JsonNode> fallbackLocationMap = new HashMap<>();
        Set<String> terminalNos = new LinkedHashSet<>();
        for (StaffSnapshot staff : staffRows) {
            String terminalNo = firstText(overrideTerminalNo, staff.terminalNo);
            if (!StringUtils.hasText(terminalNo)) {
                terminalNo = resolveTerminalNo(staff.staffNo, staff.idCardNo, staff.mobileNo, false);
                staff.terminalNo = terminalNo;
            }
            if (!StringUtils.hasText(terminalNo)) {
                continue;
            }
            terminalNos.add(terminalNo);
            if (hasLocation(staff.lastLocation)) {
                fallbackLocationMap.put(terminalNo, staff.lastLocation);
            }
        }
        Map<String, JsonNode> locationMap = new HashMap<>(locateTerminals(terminalNos));
        fallbackLocationMap.forEach(locationMap::putIfAbsent);
        return locationMap;
    }

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

    private Map<String, JsonNode> locateTerminalBatch(List<String> terminalNos) {
        Map<String, Object> request = new HashMap<>();
        request.put("terminalNoList", terminalNos);
        JsonNode root = findsOpenApiClient.post(POINT_LOCATE_API, request);
        JsonNode data = assertSuccess(root, POINT_LOCATE_API).path("data");
        Map<String, JsonNode> result = new HashMap<>();
        JsonNode rows = extractRows(data);
        if (rows.isArray()) {
            for (JsonNode row : rows) {
                String terminalNo = textValue(child(row, "terminalNo"));
                if (!StringUtils.hasText(terminalNo) && terminalNos.size() == 1) {
                    terminalNo = terminalNos.get(0);
                }
                if (StringUtils.hasText(terminalNo)) {
                    result.put(terminalNo, row);
                }
            }
            return result;
        }
        if (!isEmpty(data) && terminalNos.size() == 1) {
            result.put(terminalNos.get(0), data);
        }
        return result;
    }

    private BiPersonLocationCO mapPersonLocation(StaffSnapshot staff,
                                                 PositionPersonLocalLookupService.LocalPerson local,
                                                 String terminalNo,
                                                 PositionPersonLocalLookupService.TerminalSnapshot terminal,
                                                 JsonNode location, List<BiAreaFenceCO> fences) {
        BiPersonLocationCO co = new BiPersonLocationCO();
        co.setUserId(local.getUserId());
        co.setStaffName(local.getStaffName());
        co.setLocalStaffNo(local.getLocalStaffNo());
        co.setStaffNo(staff.staffNo);
        co.setIdCardNo(local.getIdCardNo());
        co.setMobileNo(local.getMobileNo());
        co.setDepartmentId(local.getDepartmentId());
        co.setDepartmentName(local.getDepartmentName());
        co.setOrgName(staff.orgName);
        co.setOrgCode(staff.orgCode);
        co.setTerminalNo(terminalNo);
        co.setCompanyName(local.getCorpinfoName());
        co.setCorpinfoName(local.getCorpinfoName());
        co.setCorpinfoId(local.getCorpinfoId() == null ? null : String.valueOf(local.getCorpinfoId()));
        co.setPortArea(local.getPortArea());
        co.setCreditCode(local.getCreditCode());
        CorpInfoView corpInfo = queryCorpInfoById(local.getCorpinfoId());
        if (corpInfo != null) {
            co.setCreditCode(corpInfo.code);
            co.setPortArea(corpInfo.portArea);
        }
        co.setPositionMode(positionMode(terminal, terminalNo));
        co.setLon(decimalValue(firstPresentAny(child(location, "lon"), child(location, "lng"),
                child(location, "longitude"))));
        co.setLat(decimalValue(firstPresentAny(child(location, "lat"), child(location, "latitude"))));
        co.setAlt(decimalValue(firstPresentAny(child(location, "alt"), child(location, "altitude"))));
        co.setSpeed(decimalValue(firstPresentAny(child(location, "spd"), child(location, "speed"))));
        co.setDirection(decimalValue(firstPresentAny(child(location, "dir"), child(location, "direction"))));
        co.setLastLocationTime(timeValue(firstPresentAny(child(location, "gt"), child(location, "time"),
                child(location, "locateTime"), child(location, "locationTime"), child(location, "timestamp"))));
        co.setCurrentLocation(firstText(locationName(location), findContainingFenceName(co.getLon(), co.getLat(), fences)));
        co.setOnline(co.getLon() != null && co.getLat() != null);
        int alarmCount = queryOpenAlarmCountByPerson(co.getIdCardNo(), terminalNo);
        co.setAlarmCount(alarmCount);
        co.setAlarmStatus(alarmCount > 0);
        co.setStaffRawJson(jsonValue(staff.rawNode));
        co.setLocationRawJson(jsonValue(location));
        return co;
    }

    private boolean matchesPersonLocation(BiPersonLocationCO co, BiPersonLocationQry qry) {
        if (qry.getCorpinfoId() != null
                && !String.valueOf(qry.getCorpinfoId()).equals(co.getCorpinfoId())) {
            return false;
        }
        if (StringUtils.hasText(qry.getTerminalNo()) && !qry.getTerminalNo().equals(co.getTerminalNo())) {
            return false;
        }
        if (StringUtils.hasText(qry.getStaffName())
                && !containsText(co.getStaffName(), qry.getStaffName())) {
            return false;
        }
        if (StringUtils.hasText(qry.getStaffNo()) && !qry.getStaffNo().equals(co.getStaffNo())) {
            return false;
        }
        if (StringUtils.hasText(qry.getIdCardNo()) && !qry.getIdCardNo().equals(co.getIdCardNo())) {
            return false;
        }
        if (StringUtils.hasText(qry.getMobileNo()) && !qry.getMobileNo().equals(co.getMobileNo())) {
            return false;
        }
        if (StringUtils.hasText(qry.getOrgCode()) && !qry.getOrgCode().equals(co.getOrgCode())) {
            return false;
        }
        if (StringUtils.hasText(qry.getCreditCode()) && !qry.getCreditCode().equals(co.getCreditCode())) {
            return false;
        }
        if (StringUtils.hasText(qry.getOrgName()) && !containsText(co.getOrgName(), qry.getOrgName())) {
            return false;
        }
        return !StringUtils.hasText(qry.getCompanyName())
                || containsText(firstText(co.getCompanyName(), co.getOrgName()), qry.getCompanyName());
    }

    private JsonNode assertSuccess(JsonNode root, String apiName) {
        int code = root.path("code").asInt(-1);
        if (code != 200 && code != 0) {
            String msg = root.path("msg").asText("");
            throw new BizException("FindS接口调用失败：" + apiName + "，code=" + code + "，msg=" + msg);
        }
        return root;
    }

    private void validateTraceQuery(BiPersonTraceQry qry) {
        if (qry == null) {
            throw new BizException("人员轨迹查询参数不能为空");
        }
        if (!StringUtils.hasText(qry.getTerminalNo())
                && !StringUtils.hasText(qry.getStaffNo())
                && !StringUtils.hasText(qry.getIdCardNo())
                && !StringUtils.hasText(qry.getMobileNo())) {
            throw new BizException("terminalNo、staffNo、idCardNo、mobileNo至少传一个");
        }
        if (qry.getEndTime() == null) {
            qry.setEndTime(System.currentTimeMillis());
        }
        if (qry.getStartTime() == null) {
            qry.setStartTime(qry.getEndTime() - MAX_TRACE_RANGE_MS);
        }
        if (qry.getStartTime() > qry.getEndTime()) {
            throw new BizException("轨迹开始时间不能大于结束时间");
        }
        if (qry.getEndTime() - qry.getStartTime() > MAX_TRACE_RANGE_MS) {
            throw new BizException("FindS轨迹查询时间范围不能超过24小时");
        }
    }

    private String resolveTerminalNo(BiPersonTraceQry qry) {
        if (StringUtils.hasText(qry.getTerminalNo())) {
            return qry.getTerminalNo();
        }
        return resolveTerminalNo(qry.getStaffNo(), qry.getIdCardNo(), qry.getMobileNo(), true);
    }

    private String resolveTerminalNo(String staffNo, String idCardNo, String mobileNo, boolean required) {
        Map<String, Object> request = new HashMap<>();
        if (StringUtils.hasText(staffNo)) {
            request.put("staffNo", staffNo);
        }
        if (StringUtils.hasText(idCardNo)) {
            request.put("idCardNo", idCardNo);
        }
        if (StringUtils.hasText(mobileNo)) {
            request.put("mobileNo", mobileNo);
        }
        if (request.isEmpty()) {
            if (required) {
                throw new BizException("terminalNo, staffNo, idCardNo or mobileNo is required");
            }
            return null;
        }
        JsonNode root = findsOpenApiClient.post(STAFF_TERMINAL_QUERY_API, request);
        JsonNode data = assertSuccess(root, STAFF_TERMINAL_QUERY_API).path("data");
        String terminalNo = findTerminalNo(data);
        if (!StringUtils.hasText(terminalNo) && required) {
            throw new BizException("未查询到人员绑定的定位终端");
        }
        return terminalNo;
    }

    private String findTerminalNo(JsonNode data) {
        if (isEmpty(data)) {
            return null;
        }
        JsonNode rows = data.path("data");
        if (!rows.isArray() && data.isArray()) {
            rows = data;
        }
        if (!rows.isArray() || rows.size() == 0) {
            return null;
        }
        JsonNode first = rows.get(0);
        String terminalNo = textValue(first.get("terminalNo"));
        if (StringUtils.hasText(terminalNo)) {
            return terminalNo;
        }
        String bindTerminalNo = textValue(first.path("bindTerminal").get("terminalNo"));
        if (StringUtils.hasText(bindTerminalNo)) {
            return bindTerminalNo;
        }
        JsonNode takenTerminalList = first.path("takenTerminalList");
        if (takenTerminalList.isArray()) {
            for (JsonNode terminal : takenTerminalList) {
                String takenTerminalNo = textValue(terminal.get("terminalNo"));
                if (StringUtils.hasText(takenTerminalNo)) {
                    return takenTerminalNo;
                }
            }
        }
        return null;
    }

    private List<BiPersonTraceCO.Point> mapTracePoints(JsonNode data) {
        JsonNode rows = data.path("data");
        if (!rows.isArray() && data.isArray()) {
            rows = data;
        }
        List<BiPersonTraceCO.Point> result = new ArrayList<>();
        if (!rows.isArray()) {
            return result;
        }
        for (JsonNode row : rows) {
            BiPersonTraceCO.Point point = new BiPersonTraceCO.Point();
            point.setTerminalNo(textValue(row.get("terminalNo")));
            point.setGt(longValue(row.get("gt")));
            point.setLon(decimalValue(row.get("lon")));
            point.setLat(decimalValue(row.get("lat")));
            point.setAlt(decimalValue(row.get("alt")));
            point.setSpeed(decimalValue(firstPresent(row.get("spd"), row.get("speed"))));
            point.setDirection(decimalValue(firstPresent(row.get("dir"), row.get("direction"))));
            point.setGs(integerValue(row.get("gs")));
            point.setLm(integerValue(row.get("lm")));
            point.setSn(integerValue(row.get("sn")));
            point.setRawJson(jsonValue(row));
            result.add(point);
        }
        return result;
    }

    private int countOnlinePersons(List<BiPersonLocationCO> personList) {
        int result = 0;
        for (BiPersonLocationCO person : personList) {
            if (Boolean.TRUE.equals(person.getOnline())) {
                result++;
            }
        }
        return result;
    }

    private void fillCompanyOnlineCounts(List<BiCompanyLocationCO> companies,
                                         List<BiPersonLocationCO> personList) {
        Map<String, Integer> countsByCompanyId = new HashMap<>();
        Map<String, Integer> countsByCompanyName = new HashMap<>();
        for (BiPersonLocationCO person : personList) {
            if (!Boolean.TRUE.equals(person.getOnline())) {
                continue;
            }
            if (StringUtils.hasText(person.getCorpinfoId())) {
                countsByCompanyId.merge(person.getCorpinfoId(), 1, Integer::sum);
            }
            String companyName = firstText(person.getCorpinfoName(), person.getCompanyName());
            if (StringUtils.hasText(companyName)) {
                countsByCompanyName.merge(companyName, 1, Integer::sum);
            }
        }
        for (BiCompanyLocationCO company : companies) {
            Integer count = countsByCompanyId.get(company.getCorpinfoId());
            if (count == null) {
                count = countsByCompanyName.get(firstText(company.getCorpinfoName(), company.getCompanyName()));
            }
            company.setOnlineCount(count == null ? 0 : count);
        }
    }

    private List<BiPortStatCO> buildPortStats(List<BiCompanyLocationCO> companyList) {
        Map<String, BiPortStatCO> portMap = new LinkedHashMap<>();
        for (BiCompanyLocationCO company : companyList) {
            if (company.getPortArea() == null && company.getPortId() == null
                    && !StringUtils.hasText(company.getPortCode())
                    && !StringUtils.hasText(company.getPortName())) {
                continue;
            }
            String portKey = company.getPortArea() != null ? "AREA:" + company.getPortArea()
                    : (company.getPortId() != null ? "ID:" + company.getPortId()
                    : (StringUtils.hasText(company.getPortCode())
                    ? "CODE:" + company.getPortCode() : "NAME:" + company.getPortName()));
            BiPortStatCO port = portMap.computeIfAbsent(portKey, key -> {
                BiPortStatCO value = new BiPortStatCO();
                value.setPortArea(company.getPortArea());
                value.setPortId(company.getPortId());
                value.setPortCode(company.getPortCode());
                value.setPortName(company.getPortName());
                value.setCompanyCount(0);
                value.setCompanyAreaCount(0);
                value.setWorkFenceCount(0);
                value.setOnlineCount(0);
                value.setAlarmCount(0);
                value.setCompanyList(new ArrayList<>());
                return value;
            });
            if (port.getPortId() == null && company.getPortId() != null) {
                port.setPortId(company.getPortId());
            }
            port.setCompanyCount(port.getCompanyCount() + 1);
            port.setCompanyAreaCount(port.getCompanyAreaCount() + safeInt(company.getCompanyAreaCount()));
            port.setWorkFenceCount(port.getWorkFenceCount() + safeInt(company.getWorkFenceCount()));
            port.setOnlineCount(port.getOnlineCount() + safeInt(company.getOnlineCount()));
            port.setAlarmCount(port.getAlarmCount() + safeInt(company.getAlarmCount()));
            port.getCompanyList().add(company);
        }
        return new ArrayList<>(portMap.values());
    }

    private void fillCompanyAlarmCounts(List<BiCompanyLocationCO> rows) {
        Map<String, Integer> countsByCompanyId = new HashMap<>();
        Map<String, Integer> countsByCompanyName = new HashMap<>();
        jdbcTemplate.query("SELECT corpinfo_id, corpinfo_name, COUNT(1) AS alarm_count "
                        + "FROM alarm_record WHERE " + OPEN_ALARM_WHERE
                        + " GROUP BY corpinfo_id, corpinfo_name",
                rs -> {
                    String companyId = rs.getString("corpinfo_id");
                    String companyName = rs.getString("corpinfo_name");
                    int count = rs.getInt("alarm_count");
                    if (StringUtils.hasText(companyId)) {
                        countsByCompanyId.merge(companyId, count, Integer::sum);
                    }
                    if (StringUtils.hasText(companyName)) {
                        countsByCompanyName.merge(companyName, count, Integer::sum);
                    }
                });
        for (BiCompanyLocationCO row : rows) {
            Integer count = countsByCompanyId.get(row.getCorpinfoId());
            if (count == null) {
                count = countsByCompanyName.get(firstText(row.getCorpinfoName(), row.getCompanyName()));
            }
            row.setAlarmCount(count == null ? 0 : count);
        }
    }

    private int queryOpenAlarmCount() {
        return queryCount("SELECT COUNT(1) FROM alarm_record WHERE " + OPEN_ALARM_WHERE);
    }

    private int queryOpenAlarmPersonCount() {
        return queryCount("SELECT COUNT(DISTINCT first_id_card_no) FROM alarm_record WHERE "
                + OPEN_ALARM_WHERE + " AND first_id_card_no IS NOT NULL AND first_id_card_no <> ''");
    }

    private int queryOpenAlarmCountByPerson(String idCardNo, String terminalNo) {
        List<Object> params = new ArrayList<>();
        List<String> conditions = new ArrayList<>();
        if (StringUtils.hasText(idCardNo)) {
            conditions.add("first_id_card_no = ?");
            params.add(idCardNo);
        }
        if (StringUtils.hasText(terminalNo)) {
            conditions.add("CAST(terminal_no_list_json AS CHAR) LIKE ?");
            params.add("%" + terminalNo + "%");
        }
        if (conditions.isEmpty()) {
            return 0;
        }
        return queryCount("SELECT COUNT(1) FROM alarm_record WHERE " + OPEN_ALARM_WHERE
                + " AND (" + String.join(" OR ", conditions) + ")", params.toArray());
    }

    private int queryCount(String sql, Object... args) {
        Integer count = jdbcTemplate.queryForObject(sql, args, Integer.class);
        return count == null ? 0 : count;
    }

    private String queryPositionMode(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return null;
        }
        List<String> rows = jdbcTemplate.queryForList("SELECT device_type FROM terminal "
                + "WHERE terminal_no = ? AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                + "ORDER BY update_time DESC LIMIT 1", String.class, terminalNo);
        if (rows.isEmpty()) {
            return "定位终端";
        }
        return positionModeOf(rows.get(0));
    }

    private String positionMode(PositionPersonLocalLookupService.TerminalSnapshot terminal, String terminalNo) {
        return terminal == null || !StringUtils.hasText(terminal.getDeviceType())
                ? queryPositionMode(terminalNo) : positionModeOf(terminal.getDeviceType());
    }

    private String positionModeOf(String deviceType) {
        if ("CARD".equalsIgnoreCase(deviceType)) {
            return "定位卡";
        }
        if ("APP".equalsIgnoreCase(deviceType)) {
            return "手机APP";
        }
        if ("TERMINAL".equalsIgnoreCase(deviceType)) {
            return "定位终端";
        }
        if ("OTHER".equalsIgnoreCase(deviceType)) {
            return "其他";
        }
        return StringUtils.hasText(deviceType) ? deviceType : "定位终端";
    }

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

    private String findContainingFenceName(BigDecimal lon, BigDecimal lat, List<BiAreaFenceCO> fences) {
        if (lon == null || lat == null || fences == null) {
            return null;
        }
        for (BiAreaFenceCO fence : fences) {
            if (!StringUtils.hasText(fence.getPolygonJson())) {
                continue;
            }
            try {
                JsonNode polygon = objectMapper.readTree(fence.getPolygonJson());
                List<BigDecimal[]> points = new ArrayList<>();
                collectPoints(polygon, points);
                if (pointInPolygon(lon, lat, points)) {
                    return fence.getName();
                }
            } catch (IOException ignored) {
                // 第三方围栏数据格式错误时忽略，并使用备用展示位置。
            }
        }
        return null;
    }

    private boolean pointInPolygon(BigDecimal lon, BigDecimal lat, List<BigDecimal[]> points) {
        if (points == null || points.size() < 3) {
            return false;
        }
        boolean inside = false;
        double x = lon.doubleValue();
        double y = lat.doubleValue();
        for (int i = 0, j = points.size() - 1; i < points.size(); j = i++) {
            double xi = points.get(i)[0].doubleValue();
            double yi = points.get(i)[1].doubleValue();
            double xj = points.get(j)[0].doubleValue();
            double yj = points.get(j)[1].doubleValue();
            if ((yi > y) != (yj > y)
                    && x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
                inside = !inside;
            }
        }
        return inside;
    }

    private boolean matchesCompanyFence(BiAreaFenceCO fence, BiCompanyFenceQry qry) {
        if (qry.getId() != null && !qry.getId().equals(fence.getId())) {
            return false;
        }
        if (qry.getCorpinfoId() != null && !qry.getCorpinfoId().equals(fence.getCorpinfoId())) {
            return false;
        }
        if (StringUtils.hasText(qry.getCompanyName())
                && !matchesFenceCompanyName(fence, qry.getCompanyName())) {
            return false;
        }
        if (StringUtils.hasText(qry.getOrgName())
                && !matchesFenceCompanyName(fence, qry.getOrgName())) {
            return false;
        }
        return !StringUtils.hasText(qry.getNameLike())
                || (StringUtils.hasText(fence.getName()) && fence.getName().contains(qry.getNameLike()));
    }

    private boolean isCompanyAreaFence(BiAreaFenceCO fence) {
        if (AREA_TYPE_COMPANY.equalsIgnoreCase(fence.getFenceScope())) {
            return true;
        }
        String fenceName = fence.getName();
        if (StringUtils.hasText(fenceName)
                && (fenceName.contains("公司区域")
                || fenceName.contains("封闭区")
                || fenceName.contains("封闭区域"))) {
            return true;
        }
        BiAreaFenceCO.FenceBiz fenceBiz = fence.getFenceBiz();
        return fenceBiz != null
                && StringUtils.hasText(fenceBiz.getName())
                && fenceBiz.getName().contains("公司");
    }

    private boolean matchesFenceCompanyName(BiAreaFenceCO fence, String companyName) {
        return companyName.equals(fence.getCorpinfoName()) || companyName.equals(fence.getOrgName());
    }

    private String firstText(String value, String defaultValue) {
        return StringUtils.hasText(value) ? value : defaultValue;
    }

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

    private void putIfText(Map<String, Object> request, String key, String value) {
        if (StringUtils.hasText(value)) {
            request.put(key, value);
        }
    }

    private JsonNode extractRows(JsonNode data) {
        if (isEmpty(data)) {
            return objectMapper.createArrayNode();
        }
        if (data.isArray()) {
            return data;
        }
        JsonNode direct = firstPresentAny(child(data, "data"), child(data, "rows"),
                child(data, "list"), child(data, "records"));
        if (direct != null && direct.isArray()) {
            return direct;
        }
        JsonNode nested = firstPresentAny(child(child(data, "data"), "data"),
                child(child(data, "data"), "rows"),
                child(child(data, "data"), "list"),
                child(child(data, "data"), "records"));
        if (nested != null && nested.isArray()) {
            return nested;
        }
        return objectMapper.createArrayNode();
    }

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

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

    private Long timeValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        if (node.isNumber()) {
            return node.asLong();
        }
        String value = node.asText();
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.valueOf(value);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private boolean containsText(String source, String target) {
        return StringUtils.hasText(source) && StringUtils.hasText(target) && source.contains(target);
    }

    private int safeInt(Integer value) {
        return value == null ? 0 : value;
    }

    private int safeLongToInt(long value) {
        return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(value, 0);
    }

    private CorpInfoView queryCorpInfo(String creditCode, String orgCode, String companyName) {
        CorpInfoView exactByCreditCode = queryCorpInfoByCreditCode(creditCode);
        if (exactByCreditCode != null) {
            return exactByCreditCode;
        }
        CorpInfoView exactByOrgCode = queryCorpInfoByCreditCode(orgCode);
        if (exactByOrgCode != null) {
            return exactByOrgCode;
        }
        if (!StringUtils.hasText(companyName)) {
            return null;
        }
        CorpInfoView exact = queryFirstCorpInfo(CORP_INFO_SELECT + " AND corp_name = ? LIMIT 1", companyName);
        if (exact != null) {
            return exact;
        }
        return queryFirstCorpInfo(CORP_INFO_SELECT
                        + " AND (corp_name LIKE CONCAT('%', ?, '%') OR ? LIKE CONCAT('%', corp_name, '%'))"
                        + " ORDER BY CHAR_LENGTH(corp_name) DESC LIMIT 1",
                companyName, companyName);
    }

    private CorpInfoView queryCorpInfoByCreditCode(String creditCode) {
        if (!StringUtils.hasText(creditCode)) {
            return null;
        }
        return queryFirstCorpInfo(CORP_INFO_SELECT + " AND code = ? LIMIT 1", creditCode);
    }

    private CorpInfoView queryCorpInfoById(String corpinfoId) {
        if (corpinfoId == null) {
            return null;
        }
        return queryFirstCorpInfo(CORP_INFO_SELECT
                + " AND CAST(id AS CHAR) = CAST(? AS CHAR) LIMIT 1", corpinfoId);
    }

    private CorpInfoView queryFirstCorpInfo(String sql, Object... args) {
        List<CorpInfoView> rows = jdbcTemplate.query(sql, this::mapCorpInfo, args);
        return rows.isEmpty() ? null : rows.get(0);
    }

    private CorpInfoView mapCorpInfo(ResultSet rs, int rowNum) throws SQLException {
        CorpInfoView corpInfo = new CorpInfoView();
        corpInfo.corpinfoId = rs.getString("corpinfo_id");
        corpInfo.corpName = rs.getString("corp_name");
        corpInfo.code = rs.getString("code");
        corpInfo.address = rs.getString("address");
        corpInfo.addressBusiness = rs.getString("address_business");
        corpInfo.addressOffice = rs.getString("address_office");
        corpInfo.contacts = rs.getString("contacts");
        corpInfo.lrName = rs.getString("lr_name");
        corpInfo.corpTypeName = rs.getString("corp_type_name");
        corpInfo.corpType2Name = rs.getString("corp_type2_name");
        corpInfo.corpType3Name = rs.getString("corp_type3_name");
        corpInfo.corpType4Name = rs.getString("corp_type4_name");
        corpInfo.longitude = rs.getString("longitude");
        corpInfo.latitude = rs.getString("latitude");
        int portArea = rs.getInt("port_area");
        corpInfo.portArea = rs.wasNull() ? null : portArea;
        return corpInfo;
    }

    private void applyCorpInfo(BiCompanyLocationCO co, CorpInfoView corpInfo) {
        co.setCompanyName(firstText(corpInfo.corpName, co.getCompanyName()));
        co.setCorpinfoName(firstText(corpInfo.corpName, co.getCorpinfoName()));
        co.setCorpinfoId(corpInfo.corpinfoId);
        co.setCreditCode(corpInfo.code);
        co.setLegalPerson(corpInfo.lrName);
        co.setPrincipal(corpInfo.contacts);
        co.setIndustry(firstText(corpInfo.corpType4Name, corpInfo.corpType3Name,
                corpInfo.corpType2Name, corpInfo.corpTypeName));
        co.setAddress(firstText(corpInfo.address, corpInfo.addressBusiness, corpInfo.addressOffice));
        fillFallbackCenter(co, corpInfo);
    }

    private void fillFallbackCenter(BiCompanyLocationCO co, CorpInfoView corpInfo) {
        if (co.getCenterLon() != null && co.getCenterLat() != null) {
            return;
        }
        BigDecimal lon = decimalFromText(corpInfo.longitude);
        BigDecimal lat = decimalFromText(corpInfo.latitude);
        if (lon == null || lat == null) {
            return;
        }
        if (lon.abs().compareTo(BigDecimal.valueOf(90)) <= 0
                && lat.abs().compareTo(BigDecimal.valueOf(90)) > 0) {
            BigDecimal tmp = lon;
            lon = lat;
            lat = tmp;
        }
        co.setCenterLon(lon);
        co.setCenterLat(lat);
        co.setLocationSource("CORP_INFO_LOCATION");
    }

    private BigDecimal decimalFromText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private void collectPoints(JsonNode node, List<BigDecimal[]> points) {
        if (isEmpty(node)) {
            return;
        }
        if (node.isObject()) {
            node.elements().forEachRemaining(child -> collectPoints(child, points));
            return;
        }
        if (!node.isArray()) {
            return;
        }
        if (node.size() >= 2 && node.get(0).isNumber() && node.get(1).isNumber()) {
            points.add(new BigDecimal[]{node.get(0).decimalValue(), node.get(1).decimalValue()});
            return;
        }
        for (JsonNode child : node) {
            collectPoints(child, points);
        }
    }

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

    private JsonNode firstPresent(JsonNode first, JsonNode second) {
        return isEmpty(first) ? second : first;
    }

    private JsonNode firstPresentAny(JsonNode... nodes) {
        if (nodes == null) {
            return null;
        }
        for (JsonNode node : nodes) {
            if (!isEmpty(node)) {
                return node;
            }
        }
        return null;
    }

    private Long longValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        if (node.isNumber()) {
            return node.asLong();
        }
        try {
            return Long.valueOf(node.asText());
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private BigDecimal decimalValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        if (node.isNumber()) {
            return node.decimalValue();
        }
        try {
            return new BigDecimal(node.asText());
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private Integer integerValue(JsonNode node) {
        Long value = longValue(node);
        return value == null ? null : value.intValue();
    }

    private String textValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        String value = node.asText();
        return StringUtils.hasText(value) ? value : null;
    }

    private String jsonValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(node);
        } catch (IOException e) {
            throw new BizException("JSON转换失败");
        }
    }

    private boolean isEmpty(JsonNode node) {
        return node == null || node.isMissingNode() || node.isNull();
    }

    private static class MatchedStaffSnapshot {
        private final StaffSnapshot staff;
        private final PositionPersonLocalLookupService.LocalPerson local;

        private MatchedStaffSnapshot(StaffSnapshot staff,
                                     PositionPersonLocalLookupService.LocalPerson local) {
            this.staff = staff;
            this.local = local;
        }
    }

    private static class BiPersonPositionArea {
        private final AreaDO area;
        private final List<BigDecimal[]> points;

        private BiPersonPositionArea(AreaDO area, List<BigDecimal[]> points) {
            this.area = area;
            this.points = points;
        }
    }

    private static class StaffSnapshot {
        private JsonNode rawNode;
        private String staffName;
        private String staffNo;
        private String idCardNo;
        private String mobileNo;
        private String orgName;
        private String orgCode;
        private String creditCode;
        private String terminalNo;
        private JsonNode lastLocation;
    }

    private static class AreaSnapshot {
        private Long id;
        private String areaCode;
        private String areaName;
        private String areaType;
        private Long parentId;
        private String parentCode;
        private String corpinfoId;
        private String corpinfoName;
        private BigDecimal centerLon;
        private BigDecimal centerLat;
        private String polygonJson;
    }

    private static class CorpInfoView {
        private String corpinfoId;
        private String corpName;
        private String code;
        private String address;
        private String addressBusiness;
        private String addressOffice;
        private String contacts;
        private String lrName;
        private String corpTypeName;
        private String corpType2Name;
        private String corpType3Name;
        private String corpType4Name;
        private String longitude;
        private String latitude;
        private Integer portArea;
    }

    private class CompanyMarkerAccumulator {
        private final String companyName;
        private String orgCode;
        private String creditCode;
        private String corpinfoId;
        private Long portId;
        private String portCode;
        private String portName;
        private BigDecimal lonSum = BigDecimal.ZERO;
        private BigDecimal latSum = BigDecimal.ZERO;
        private int centerCount = 0;
        private int fenceCount = 0;
        private int companyAreaCount = 0;
        private int workFenceCount = 0;

        private CompanyMarkerAccumulator(String companyName) {
            this.companyName = companyName;
        }

        private void addFence(BiAreaFenceCO fence) {
            fenceCount++;
            if (!StringUtils.hasText(orgCode)) {
                orgCode = fence.getOrgCode();
            }
            if (!StringUtils.hasText(creditCode)) {
                creditCode = fence.getCreditCode();
            }
            if (!StringUtils.hasText(corpinfoId) && fence.getCorpinfoId() != null) {
                corpinfoId = String.valueOf(fence.getCorpinfoId());
            }
            if (portId == null && fence.getPortId() != null) {
                portId = fence.getPortId();
                portCode = fence.getPortCode();
                portName = fence.getPortName();
            }
            if (isCompanyAreaFence(fence)) {
                companyAreaCount++;
            } else {
                workFenceCount++;
            }
            if (fence.getCenterLon() != null && fence.getCenterLat() != null) {
                lonSum = lonSum.add(fence.getCenterLon());
                latSum = latSum.add(fence.getCenterLat());
                centerCount++;
            }
        }

        private BiCompanyLocationCO toCO() {
            BiCompanyLocationCO co = new BiCompanyLocationCO();
            co.setCompanyName(companyName);
            co.setOrgName(companyName);
            co.setOrgCode(orgCode);
            co.setCorpinfoId(corpinfoId);
            co.setCorpinfoName(companyName);
            co.setCreditCode(creditCode);
            co.setPortId(portId);
            co.setPortCode(portCode);
            co.setPortName(portName);
            co.setFenceCount(fenceCount);
            co.setCompanyAreaCount(companyAreaCount);
            co.setWorkFenceCount(workFenceCount);
            co.setAlarmCount(0);
            co.setOnlineCount(0);
            co.setLocationSource("LOCAL_FENCE_POLYGON_CENTER");
            if (centerCount > 0) {
                BigDecimal count = BigDecimal.valueOf(centerCount);
                co.setCenterLon(lonSum.divide(count, 10, RoundingMode.HALF_UP));
                co.setCenterLat(latSum.divide(count, 10, RoundingMode.HALF_UP));
            }
            return co;
        }
    }
}
