package com.zcloud.personnel.positioning.command;

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.TerminalCardCompanyOptionQry;
import com.zcloud.personnel.positioning.dto.TerminalCardCompanyPageQry;
import com.zcloud.personnel.positioning.dto.TerminalCardPageQry;
import com.zcloud.personnel.positioning.dto.TerminalCardPersonOptionQry;
import com.zcloud.personnel.positioning.dto.TerminalCardSaveCmd;
import com.zcloud.personnel.positioning.dto.TerminalCardSyncCmd;
import com.zcloud.personnel.positioning.dto.TerminalCardUpdateCmd;
import com.zcloud.personnel.positioning.dto.clientobject.TerminalCardCO;
import com.zcloud.personnel.positioning.dto.clientobject.TerminalCardCompanyCO;
import com.zcloud.personnel.positioning.dto.clientobject.TerminalCardCompanyOptionCO;
import com.zcloud.personnel.positioning.dto.clientobject.TerminalCardPersonOptionCO;
import com.zcloud.personnel.positioning.dto.clientobject.TerminalCardSyncCO;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import com.zcloud.personnel.positioning.persistence.dataobject.TerminalBindDO;
import com.zcloud.personnel.positioning.persistence.dataobject.TerminalDO;
import com.zcloud.personnel.positioning.persistence.repository.TerminalBindRepository;
import com.zcloud.personnel.positioning.persistence.repository.TerminalRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@AllArgsConstructor
public class TerminalCardSyncExe {
    private static final String DEVICE_TYPE_CARD = "CARD";
    private static final String BIND_STATUS_BOUND = "BOUND";
    private static final String BIND_STATUS_UNBOUND = "UNBOUND";
    private static final String TRACK_TYPE_STAFF = "STAFF";
    private static final String SYNC_STATUS_SUCCESS = "SUCCESS";
    private static final String SYNC_STATUS_WAIT = "WAIT";
    private static final String TERMINAL_LIST_API = "finds.terminal.list";
    private static final int DEFAULT_SYNC_PAGE_SIZE = 200;
    private static final int DEFAULT_SYNC_MAX_PAGE_COUNT = 50;

    private final TerminalRepository terminalRepository;
    private final TerminalBindRepository terminalBindRepository;
    private final FindsOpenApiClient findsOpenApiClient;
    private final ObjectMapper objectMapper;
    private final FindsPendingSyncService findsPendingSyncService;
    private final TransactionTemplate transactionTemplate;
    private final FindsCorpMappingService findsCorpMappingService;

    public TerminalCardSyncCO syncCards(TerminalCardSyncCmd cmd) {
        TerminalCardSyncCmd command = cmd == null ? new TerminalCardSyncCmd() : cmd;
        int pageSize = normalizePositive(command.getPageSize(), DEFAULT_SYNC_PAGE_SIZE);
        int maxPageCount = normalizePositive(command.getMaxPageCount(), DEFAULT_SYNC_MAX_PAGE_COUNT);
        TerminalCardSyncCO result = new TerminalCardSyncCO();
        result.setPulledCount(0);
        result.setInsertedCount(0);
        result.setUpdatedCount(0);
        result.setSkippedCount(0);
        result.setBindingCount(0);
        result.setSyncTime(LocalDateTime.now());

        long total = Long.MAX_VALUE;
        int pageNo = 1;
        while (result.getPulledCount() < total && pageNo <= maxPageCount) {
            JsonNode root = findsOpenApiClient.post(TERMINAL_LIST_API, buildTerminalListRequest(command, pageNo, pageSize));
            JsonNode data = assertSuccess(root, TERMINAL_LIST_API).path("data");
            JsonNode rows = extractRows(data);
            if (!rows.isArray() || rows.size() == 0) {
                break;
            }
            total = totalValue(data, rows.size());
            transactionTemplate.execute(status -> {
                for (JsonNode row : rows) {
                    SyncRowResult rowResult = upsertSyncedCard(row, command, result.getSyncTime());
                    if (rowResult.skipped) {
                        result.setSkippedCount(result.getSkippedCount() + 1);
                        continue;
                    }
                    result.setPulledCount(result.getPulledCount() + 1);
                    if (rowResult.inserted) {
                        result.setInsertedCount(result.getInsertedCount() + 1);
                    } else {
                        result.setUpdatedCount(result.getUpdatedCount() + 1);
                    }
                    if (rowResult.bindingSynced) {
                        result.setBindingCount(result.getBindingCount() + 1);
                    }
                }
                return null;
            });
            pageNo++;
        }
        return result;
    }

    private Map<String, Object> buildTerminalListRequest(TerminalCardSyncCmd cmd, int pageNo, int pageSize) {
        Map<String, Object> request = new HashMap<>();
        putIfText(request, "terminalNo", cmd.getTerminalNo());
        putIfText(request, "orgCode", cmd.getOrgCode());
        putIfText(request, "orgName", firstText(cmd.getOrgName(), cmd.getCorpinfoName()));
        request.put("pageNo", pageNo);
        request.put("pageSize", pageSize);
        return request;
    }

    private SyncRowResult upsertSyncedCard(JsonNode row, TerminalCardSyncCmd cmd, LocalDateTime syncTime) {
        String terminalNo = firstText(textValue(child(row, "terminalNo")), textValue(child(row, "no")),
                textValue(child(row, "terminalCode")), textValue(child(row, "imei")));
        SyncRowResult result = new SyncRowResult();
        if (!StringUtils.hasText(terminalNo)) {
            result.skipped = true;
            return result;
        }
        if (!looksLikeCardTerminal(row)) {
            result.skipped = true;
            return result;
        }
        TerminalDO terminal = queryTerminalByNo(terminalNo);
        if (terminal != null && isPendingLocalSync(terminal.getSyncStatus())) {
            result.skipped = true;
            return result;
        }
        result.inserted = terminal == null;
        if (terminal == null) {
            terminal = new TerminalDO();
            terminal.setTerminalNo(terminalNo);
        }
        JsonNode track = firstPresentAny(child(row, "track"), child(row, "bindTrack"), child(row, "staff"));
        String remoteOrgName = firstText(textValue(child(row, "corpinfoName")),
                textValue(child(row, "corpName")), textValue(child(row, "orgName")), cmd.getCorpinfoName());
        String remoteOrgCode = firstText(textValue(child(row, "orgCode")), cmd.getOrgCode());
        FindsCorpMappingService.CorpMatch mappedCorp = findsCorpMappingService.resolve("FINDS",
                textValue(child(row, "orgId")), remoteOrgName,
                firstText(textValue(child(row, "departmentName")), textValue(child(row, "deptName"))),
                remoteOrgCode);
        CorpInfo corpInfo = mappedCorp == null
                ? resolveCorpInfo(cmd.getCorpinfoId(), cmd.getCorpinfoName(), remoteOrgCode, cmd.getOrgName())
                : toCorpInfo(mappedCorp);
        boolean keepExistingCorp = shouldKeepExistingCorp(terminal, remoteOrgName, remoteOrgCode);

        terminal.setTerminalName(firstText(textValue(child(row, "terminalName")), textValue(child(row, "name")),
                terminal.getTerminalName(), terminalNo));
        terminal.setTerminalModel(firstText(textValue(child(row, "terminalModel")), textValue(child(row, "model")),
                terminal.getTerminalModel()));
        terminal.setDeviceType(DEVICE_TYPE_CARD);
        terminal.setSimCardNo(firstText(textValue(child(row, "simCardNo")), textValue(child(row, "simNo")),
                terminal.getSimCardNo()));
        terminal.setCorpinfoId(corpInfo == null
                ? firstText(cmd.getCorpinfoId(), keepExistingCorp ? terminal.getCorpinfoId() : null)
                : corpInfo.corpinfoId);
        terminal.setCorpinfoName(firstText(cmd.getCorpinfoName(), corpInfo == null ? null : corpInfo.corpName,
                keepExistingCorp && StringUtils.hasText(terminal.getCorpinfoId()) ? terminal.getCorpinfoName() : null,
                textValue(child(row, "corpinfoName")), textValue(child(row, "corpName")),
                textValue(child(row, "orgName")), terminal.getCorpinfoName()));
        terminal.setOrgCode(firstText(textValue(child(row, "orgCode")), textValue(child(row, "orgId")),
                cmd.getOrgCode(), terminal.getOrgCode()));
        terminal.setOrgName(firstText(textValue(child(row, "orgName")), cmd.getOrgName(), terminal.getOrgName(),
                terminal.getCorpinfoName()));
        terminal.setEnable(firstInteger(integerValue(firstPresentAny(child(row, "enable"), child(row, "enabled"))),
                terminal.getEnable(), 1));
        terminal.setDeviceStatus(firstText(textValue(child(row, "deviceStatus")), textValue(child(row, "status")),
                textValue(child(row, "onlineStatus")), terminal.getDeviceStatus()));
        terminal.setFindsTerminalId(firstLong(longValue(firstPresentAny(child(row, "id"), child(row, "terminalId"),
                child(row, "findsTerminalId"))), terminal.getFindsTerminalId()));
        terminal.setLastLocationTime(firstTime(timeValue(firstPresentAny(child(row, "lastReportTime"),
                child(row, "lastLocationTime"), child(row, "lastOnlineTime"),
                child(child(row, "lastLocation"), "time"), child(track, "lastReportTime"))),
                terminal.getLastLocationTime()));
        terminal.setLastLocationName(firstText(locationName(row), locationName(child(row, "lastLocation")),
                locationName(track), terminal.getLastLocationName()));
        terminal.setSyncStatus(SYNC_STATUS_SUCCESS);
        terminal.setLastSyncTime(syncTime);
        terminal.setRawJson(jsonValue(row));
        if (result.inserted) {
            terminalRepository.save(terminal);
        } else {
            terminalRepository.updateById(terminal);
        }

        PersonSnapshot person = personFromTrack(track);
        if (!hasPerson(person)) {
            person = personFromTrack(row);
        }
        if (hasPerson(person)) {
            result.bindingSynced = upsertActiveBinding(terminal, person, false, syncTime);
        }
        return result;
    }

    private boolean upsertActiveBinding(TerminalDO terminal, PersonSnapshot person, boolean callFinds, LocalDateTime now) {
        if (!hasPerson(person)) {
            return false;
        }
        if (!callFinds && hasPendingBinding(terminal.getTerminalNo())) {
            return false;
        }
        PersonSnapshot normalized = mergePerson(person, resolvePerson(person.idCardNo, person.staffNo, person.staffName));
        if (StringUtils.hasText(normalized.corpinfoId)) {
            terminal.setCorpinfoId(normalized.corpinfoId);
            terminal.setCorpinfoName(normalized.corpinfoName);
            terminal.setOrgName(firstText(normalized.corpinfoName, terminal.getOrgName()));
            terminalRepository.updateById(terminal);
        }
        TerminalBindDO active = queryActiveBinding(terminal.getTerminalNo());
        if (active != null && samePerson(active, normalized)) {
            active.setIdCardNo(firstText(normalized.idCardNo, active.getIdCardNo()));
            active.setStaffNoSnapshot(firstText(normalized.staffNo, active.getStaffNoSnapshot()));
            active.setStaffNameSnapshot(firstText(normalized.staffName, active.getStaffNameSnapshot()));
            active.setCorpinfoId(terminal.getCorpinfoId());
            active.setCorpinfoName(terminal.getCorpinfoName());
            active.setFindsTrackId(firstLong(normalized.findsTrackId, active.getFindsTrackId()));
            active.setFindsTerminalId(firstLong(terminal.getFindsTerminalId(), active.getFindsTerminalId()));
            markBindingSyncStatus(active, callFinds, now);
            terminalBindRepository.updateById(active);
            updateTerminalBindIdCardNo(terminal, active.getIdCardNo());
            return true;
        }
        if (active != null) {
            active.setBindStatus(BIND_STATUS_UNBOUND);
            active.setUnbindTime(now);
            markBindingSyncStatus(active, callFinds, now);
            terminalBindRepository.updateById(active);
        }
        TerminalBindDO bind = new TerminalBindDO();
        bind.setIdCardNo(normalized.idCardNo);
        bind.setStaffNoSnapshot(normalized.staffNo);
        bind.setStaffNameSnapshot(normalized.staffName);
        bind.setTerminalId(terminal.getId());
        bind.setTerminalNo(terminal.getTerminalNo());
        bind.setDeviceType(DEVICE_TYPE_CARD);
        bind.setCorpinfoId(terminal.getCorpinfoId());
        bind.setCorpinfoName(terminal.getCorpinfoName());
        bind.setTrackType(TRACK_TYPE_STAFF);
        bind.setCheckCode(terminal.getCheckCode());
        bind.setBindStatus(BIND_STATUS_BOUND);
        bind.setBindTime(now);
        bind.setFindsTrackId(normalized.findsTrackId);
        bind.setFindsTerminalId(terminal.getFindsTerminalId());
        markBindingSyncStatus(bind, callFinds, now);
        terminalBindRepository.save(bind);
        updateTerminalBindIdCardNo(terminal, normalized.idCardNo);
        return true;
    }

    private void markBindingSyncStatus(TerminalBindDO binding, boolean localChange, LocalDateTime now) {
        binding.setSyncStatus(localChange ? SYNC_STATUS_WAIT : SYNC_STATUS_SUCCESS);
        binding.setSyncToken(localChange ? FindsSyncToken.next() : null);
        binding.setLastSyncTime(localChange ? null : now);
    }

    private boolean hasPendingBinding(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return false;
        }
        QueryWrapper<TerminalBindDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo);
        wrapper.in("sync_status", FindsPendingSyncService.STATUS_WAIT, FindsPendingSyncService.STATUS_FAIL,
                FindsPendingSyncService.STATUS_DELETE_WAIT, FindsPendingSyncService.STATUS_DELETE_FAIL);
        wrapper.last("LIMIT 1");
        return terminalBindRepository.getOne(wrapper, false) != null;
    }

    private boolean isPendingLocalSync(String syncStatus) {
        return FindsPendingSyncService.STATUS_WAIT.equals(syncStatus)
                || FindsPendingSyncService.STATUS_FAIL.equals(syncStatus)
                || FindsPendingSyncService.STATUS_DELETE_WAIT.equals(syncStatus)
                || FindsPendingSyncService.STATUS_DELETE_FAIL.equals(syncStatus);
    }

    private void updateTerminalBindIdCardNo(TerminalDO terminal, String idCardNo) {
        terminalRepository.updateBindIdCardNo(terminal.getId(), idCardNo);
        terminal.setBindIdCardNo(idCardNo);
    }

    private TerminalBindDO queryActiveBinding(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return null;
        }
        QueryWrapper<TerminalBindDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo);
        wrapper.eq("bind_status", BIND_STATUS_BOUND);
        wrapper.eq("delete_enum", "FALSE");
        wrapper.orderByDesc("bind_time", "id");
        wrapper.last("LIMIT 1");
        return terminalBindRepository.getOne(wrapper, false);
    }

    private TerminalDO queryTerminalByNo(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return null;
        }
        QueryWrapper<TerminalDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo.trim());
        wrapper.eq("delete_enum", "FALSE");
        wrapper.last("LIMIT 1");
        return terminalRepository.getOne(wrapper, false);
    }

    private PersonSnapshot resolvePerson(String idCardNo, String staffNo, String staffName) {
        Map<String, Object> params = new HashMap<>();
        if (StringUtils.hasText(idCardNo)) {
            params.put("idCardNo", idCardNo.trim());
        } else if (StringUtils.hasText(staffNo)) {
            params.put("staffNo", staffNo.trim());
        } else if (StringUtils.hasText(staffName)) {
            params.put("staffName", staffName.trim());
        } else {
            return null;
        }
        com.zcloud.personnel.positioning.persistence.dataobject.TerminalPersonSnapshotDO data =
                terminalRepository.findPerson(params);
        PersonSnapshot fallback = new PersonSnapshot();
        fallback.idCardNo = clean(idCardNo);
        fallback.staffNo = clean(staffNo);
        fallback.staffName = clean(staffName);
        if (data == null) {
            return fallback;
        }
        PersonSnapshot person = new PersonSnapshot();
        person.staffNo = data.getStaffNo();
        person.staffName = data.getStaffName();
        person.idCardNo = data.getIdCardNo();
        person.mobileNo = data.getMobileNo();
        person.corpinfoId = data.getCorpinfoId();
        person.corpinfoName = data.getCorpinfoName();
        person.findsTrackId = data.getFindsTrackId();
        return mergePerson(person, fallback);
    }

    private CorpInfo resolveCorpInfo(String corpinfoId, String corpinfoName, String orgCode, String orgName) {
        Map<String, Object> params = new HashMap<>();
        if (corpinfoId != null) {
            params.put("corpinfoId", corpinfoId);
        } else if (StringUtils.hasText(orgCode)) {
            params.put("orgCode", orgCode.trim());
        } else if (StringUtils.hasText(corpinfoName) || StringUtils.hasText(orgName)) {
            params.put("corpinfoName", firstText(corpinfoName, orgName).trim());
        } else {
            return null;
        }
        com.zcloud.personnel.positioning.persistence.dataobject.CorpInfoSnapshotDO data =
                terminalRepository.findCorpInfo(params);
        if (data == null) {
            return null;
        }
        CorpInfo corpInfo = new CorpInfo();
        corpInfo.corpinfoId = data.getCorpinfoId();
        corpInfo.corpName = data.getCorpName();
        corpInfo.creditCode = data.getCreditCode();
        corpInfo.contactName = data.getContactName();
        return corpInfo;
    }

    private CorpInfo toCorpInfo(FindsCorpMappingService.CorpMatch match) {
        CorpInfo corpInfo = new CorpInfo();
        corpInfo.corpinfoId = match.getCorpinfoId();
        corpInfo.corpName = match.getCorpinfoName();
        return corpInfo;
    }

    private boolean shouldKeepExistingCorp(TerminalDO terminal, String remoteOrgName, String remoteOrgCode) {
        if (!StringUtils.hasText(terminal.getCorpinfoId())) {
            return false;
        }
        if (!StringUtils.hasText(remoteOrgName) && !StringUtils.hasText(remoteOrgCode)) {
            return true;
        }
        return companyNamesCompatible(terminal.getCorpinfoName(), remoteOrgName);
    }

    private boolean companyNamesCompatible(String localName, String remoteName) {
        String local = clean(localName);
        String remote = clean(remoteName);
        return local != null && remote != null
                && (local.equals(remote) || local.endsWith(remote) || remote.endsWith(local));
    }

    private PersonSnapshot personFromTrack(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        PersonSnapshot person = new PersonSnapshot();
        person.findsTrackId = longValue(firstPresentAny(child(node, "id"), child(node, "trackId"),
                child(node, "staffId"), child(node, "findsTrackId")));
        person.idCardNo = textValue(firstPresentAny(child(node, "idCardNo"), child(node, "identityNo"),
                child(node, "certificateNo"), child(node, "cardNo")));
        person.staffNo = textValue(firstPresentAny(child(node, "staffNo"), child(node, "code"),
                child(node, "employeeNo"), child(node, "workNo")));
        person.staffName = textValue(firstPresentAny(child(node, "staffName"), child(node, "name"),
                child(node, "realName"), child(node, "userName")));
        person.mobileNo = textValue(firstPresentAny(child(node, "mobileNo"), child(node, "phone"),
                child(node, "phoneNo")));
        return person;
    }

    private boolean looksLikeCardTerminal(JsonNode row) {
        String strongType = firstText(textValue(child(row, "deviceType")),
                textValue(child(row, "terminalType")), textValue(child(row, "type")));
        if (StringUtils.hasText(strongType)) {
            return containsCardKeyword(strongType);
        }
        JsonNode terminalBiz = firstPresentAny(child(row, "terminalBiz"), child(row, "biz"));
        String bizText = firstText(textValue(child(row, "bizCode")), textValue(child(row, "bizName")),
                textValue(child(terminalBiz, "code")), textValue(child(terminalBiz, "name")),
                textValue(child(terminalBiz, "bizCode")), textValue(child(terminalBiz, "bizName")));
        if (!StringUtils.hasText(bizText)) {
            return true;
        }
        if (containsNonCardKeyword(bizText)) {
            return false;
        }
        return true;
    }

    private boolean containsCardKeyword(String value) {
        if (!StringUtils.hasText(value)) {
            return false;
        }
        String upper = value.trim().toUpperCase();
        return upper.contains("CARD") || upper.contains("DWK") || value.contains("定位卡");
    }

    private boolean containsNonCardKeyword(String value) {
        String upper = value.trim().toUpperCase();
        return upper.contains("APP") || upper.contains("PHONE") || upper.contains("MOBILE")
                || upper.contains("CAR") || upper.contains("VEHICLE")
                || value.contains("手机") || value.contains("车辆") || value.contains("摄像")
                || value.contains("门禁") || value.contains("闸机");
    }

    private boolean samePerson(TerminalBindDO active, PersonSnapshot person) {
        if (StringUtils.hasText(active.getIdCardNo()) && StringUtils.hasText(person.idCardNo)) {
            return active.getIdCardNo().equals(person.idCardNo);
        }
        if (StringUtils.hasText(active.getStaffNoSnapshot()) && StringUtils.hasText(person.staffNo)) {
            return active.getStaffNoSnapshot().equals(person.staffNo);
        }
        return StringUtils.hasText(active.getStaffNameSnapshot()) && StringUtils.hasText(person.staffName)
                && active.getStaffNameSnapshot().equals(person.staffName);
    }

    private boolean hasPerson(PersonSnapshot person) {
        return person != null && (StringUtils.hasText(person.idCardNo)
                || StringUtils.hasText(person.staffNo) || StringUtils.hasText(person.staffName));
    }

    private PersonSnapshot mergePerson(PersonSnapshot first, PersonSnapshot second) {
        if (first == null) {
            return second;
        }
        if (second == null) {
            return first;
        }
        first.idCardNo = firstText(first.idCardNo, second.idCardNo);
        first.staffNo = firstText(first.staffNo, second.staffNo);
        first.staffName = firstText(first.staffName, second.staffName);
        first.mobileNo = firstText(first.mobileNo, second.mobileNo);
        first.corpinfoId = firstText(first.corpinfoId, second.corpinfoId);
        first.corpinfoName = firstText(first.corpinfoName, second.corpinfoName);
        first.findsTrackId = firstLong(first.findsTrackId, second.findsTrackId);
        return first;
    }

    private JsonNode assertSuccess(JsonNode root, String apiName) {
        if (isEmpty(root)) {
            throw new BizException("FindS response is empty: " + apiName);
        }
        JsonNode codeNode = root.get("code");
        if (!isEmpty(codeNode)) {
            int code = codeNode.asInt(-1);
            if (code != 200 && code != 0) {
                String msg = firstText(root.path("msg").asText(null), root.path("message").asText(null));
                throw new BizException("FindS API failed: " + apiName + ", code=" + code + ", msg=" + msg);
            }
        }
        JsonNode successNode = root.get("success");
        if (!isEmpty(successNode) && successNode.isBoolean() && !successNode.asBoolean()) {
            String msg = firstText(root.path("msg").asText(null), root.path("message").asText(null));
            throw new BizException("FindS API failed: " + apiName + ", msg=" + msg);
        }
        return root;
    }

    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 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 Integer integerValue(JsonNode node) {
        Long value = longValue(node);
        return value == null ? null : value.intValue();
    }

    private LocalDateTime timeValue(JsonNode node) {
        if (isEmpty(node)) {
            return null;
        }
        if (node.isNumber()) {
            long epoch = node.asLong();
            if (epoch < 100000000000L) {
                epoch = epoch * 1000L;
            }
            return LocalDateTime.ofInstant(Instant.ofEpochMilli(epoch), ZoneId.systemDefault());
        }
        String value = node.asText();
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            long epoch = Long.parseLong(value);
            if (epoch < 100000000000L) {
                epoch = epoch * 1000L;
            }
            return LocalDateTime.ofInstant(Instant.ofEpochMilli(epoch), ZoneId.systemDefault());
        } catch (NumberFormatException ignored) {
            // 数字时间戳解析失败后，继续尝试解析格式化日期。
        }
        String normalized = value.trim().replace(' ', 'T');
        try {
            return LocalDateTime.parse(normalized);
        } catch (Exception ignored) {
            try {
                return OffsetDateTime.parse(normalized).toLocalDateTime();
            } catch (Exception ignoredAgain) {
                return null;
            }
        }
    }

    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 serialize failed");
        }
    }

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

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

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

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

    private int normalizePositive(Integer value, int defaultValue) {
        return value == null || value < 1 ? defaultValue : value;
    }

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

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

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

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

    private String clean(String value) {
        return StringUtils.hasText(value) ? value.trim() : null;
    }

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

    private static class SyncRowResult {
        private boolean inserted;
        private boolean skipped;
        private boolean bindingSynced;
    }

    private static class PersonSnapshot {
        private String idCardNo;
        private String staffNo;
        private String staffName;
        private String mobileNo;
        private String corpinfoId;
        private String corpinfoName;
        private Long findsTrackId;
    }

    private static class CorpInfo {
        private String corpinfoId;
        private String corpName;
        private String creditCode;
        private String contactName;
    }
}
