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

import com.alibaba.cola.exception.BizException;
import com.fasterxml.jackson.databind.JsonNode;
import com.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Refreshes the FindS-to-local personnel mapping cache.
 *
 * <p>The cache contains identity and terminal snapshots only. Location coordinates are deliberately
 * not persisted here and continue to come from FindS at query time.</p>
 */
@Component
@RequiredArgsConstructor
public class FindsStaffMappingSyncService {
    private static final Logger LOGGER = LoggerFactory.getLogger(FindsStaffMappingSyncService.class);
    private static final String STAFF_LIST_API = "finds.staff.list";
    private static final String SOURCE_CONFIG_CODE = "FINDS";
    private static final int PAGE_SIZE = 2000;
    private static final int MAX_PAGE_COUNT = 50;
    private static final int BATCH_CONCURRENCY = 4;

    private final FindsOpenApiClient findsOpenApiClient;
    private final PositionPersonLocalLookupService localLookupService;
    private final JdbcTemplate jdbcTemplate;

    public int syncAll() {
        // The schema uses second precision; keep the same precision for the stale-row cutoff.
        LocalDateTime syncTime = LocalDateTime.now().withNano(0);
        List<StaffSnapshot> staffRows = loadAllStaff();
        Set<String> staffNos = new LinkedHashSet<>();
        for (StaffSnapshot row : staffRows) {
            staffNos.add(row.staffNo);
        }
        Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople =
                localLookupService.findUniquePeopleByPhones(staffNos);
        for (StaffSnapshot row : staffRows) {
            upsert(row, localPeople.get(row.staffNo), syncTime);
        }
        disableMissingRows(syncTime);
        LOGGER.info("FindS staff mapping synchronization completed, total={}, matched={}",
                staffRows.size(), countMatched(staffRows, localPeople));
        return staffRows.size();
    }

    private List<StaffSnapshot> loadAllStaff() {
        List<StaffSnapshot> result = new ArrayList<>();
        JsonNode firstData = successData(
                findsOpenApiClient.postFailFast(STAFF_LIST_API, pageRequest(1)));
        int firstPageSize = appendRows(result, extractRows(firstData));
        long total = requiredTotal(firstData);
        if (total > 0 && firstPageSize == 0) {
            throw new BizException("FindS staff page is empty while total is " + total);
        }
        int pageCount = (int) ((total + PAGE_SIZE - 1) / PAGE_SIZE);
        if (pageCount > MAX_PAGE_COUNT) {
            throw new BizException("FindS staff pages exceed the configured safety limit: " + pageCount);
        }
        if (pageCount <= 1) {
            return result;
        }
        List<Map<String, Object>> requests = new ArrayList<>(pageCount - 1);
        for (int pageNo = 2; pageNo <= pageCount; pageNo++) {
            requests.add(pageRequest(pageNo));
        }
        long receivedCount = firstPageSize;
        for (JsonNode root : findsOpenApiClient.postFailFastBatch(
                STAFF_LIST_API, requests, BATCH_CONCURRENCY)) {
            receivedCount += appendRows(result, extractRows(successData(root)));
        }
        if (receivedCount < total) {
            throw new BizException("FindS staff result is incomplete: received="
                    + receivedCount + ", total=" + total);
        }
        return result;
    }

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

    private int appendRows(List<StaffSnapshot> result, JsonNode rows) {
        if (rows == null || !rows.isArray()) {
            return 0;
        }
        for (JsonNode row : rows) {
            StaffSnapshot snapshot = map(row);
            if (snapshot != null) {
                result.add(snapshot);
            }
        }
        return rows.size();
    }

    private StaffSnapshot map(JsonNode row) {
        if (row == null || row.isNull()) {
            return null;
        }
        JsonNode terminalInfo = row.path("terminalInfo");
        StaffSnapshot snapshot = new StaffSnapshot();
        snapshot.staffNo = firstText(
                text(firstPresent(row, "trackNo")),
                text(firstPresent(terminalInfo, "trackNo")),
                text(firstPresent(row, "workNo")),
                text(firstPresent(row, "employeeNo")),
                text(firstPresent(row, "staffNo")),
                text(firstPresent(row, "code")),
                text(firstPresent(terminalInfo, "staffNo")));
        if (!StringUtils.hasText(snapshot.staffNo)) {
            return null;
        }
        snapshot.staffNoType = firstText(
                text(firstPresent(row, "trackNo")),
                text(firstPresent(terminalInfo, "trackNo"))) != null
                ? "TRACK_NO" : "STAFF_NO";
        snapshot.findsTrackId = longValue(firstPresent(row, "trackId", "id"));
        snapshot.idCardNo = firstText(
                text(firstPresent(row, "idCardNo", "identityNo", "certificateNo", "cardNo")),
                text(firstPresent(terminalInfo, "idCardNo")));
        snapshot.staffName = firstText(text(firstPresent(row, "staffName", "name", "realName", "userName")));
        snapshot.corpinfoId = firstText(text(firstPresent(row,
                "corpinfoId", "companyId", "corpId", "enterpriseId")));
        snapshot.corpinfoName = firstText(text(firstPresent(row,
                "companyName", "corpName", "enterpriseName", "orgName")));
        snapshot.departmentName = firstText(text(firstPresent(row,
                "departmentName", "deptName", "orgDepartmentName")));
        snapshot.terminalNo = firstText(
                text(firstPresent(row, "terminalNo")),
                text(firstPresent(terminalInfo, "terminalNo")),
                text(firstPresent(row.path("terminal"), "terminalNo")),
                text(firstPresent(row.path("bindTerminal"), "terminalNo")),
                terminalNoFromList(row.path("takenTerminalList")));
        return snapshot;
    }

    private void upsert(StaffSnapshot row,
                        PositionPersonLocalLookupService.LocalPerson local,
                        LocalDateTime syncTime) {
        String matchStatus = local == null ? "UNMATCHED" : "MATCHED";
        String matchType = local == null ? null : "MOBILE";
        if (local != null && StringUtils.hasText(row.idCardNo) && StringUtils.hasText(local.getIdCardNo())
                && !row.idCardNo.equals(local.getIdCardNo().trim())) {
            matchStatus = "AMBIGUOUS";
            matchType = null;
            local = null;
        }
        String sql = "INSERT INTO personnel_match (source_system, source_config_code, finds_staff_no, "
                + "finds_staff_no_type, finds_track_id, finds_id_card_no, finds_staff_name, "
                + "finds_corpinfo_id, finds_corpinfo_name, finds_department_name, finds_terminal_no, "
                + "local_user_id, local_mobile_no, local_id_card_no, match_status, match_type, "
                + "last_seen_time, last_sync_time, sync_status, sync_error, delete_enum) "
                + "VALUES ('FINDS', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'SUCCESS', NULL, 'FALSE') "
                + "ON DUPLICATE KEY UPDATE finds_staff_no_type = VALUES(finds_staff_no_type), "
                + "finds_track_id = VALUES(finds_track_id), finds_id_card_no = VALUES(finds_id_card_no), "
                + "finds_staff_name = VALUES(finds_staff_name), finds_corpinfo_id = VALUES(finds_corpinfo_id), "
                + "finds_corpinfo_name = VALUES(finds_corpinfo_name), finds_department_name = VALUES(finds_department_name), "
                + "finds_terminal_no = VALUES(finds_terminal_no), local_user_id = VALUES(local_user_id), "
                + "local_mobile_no = VALUES(local_mobile_no), local_id_card_no = VALUES(local_id_card_no), "
                + "match_status = VALUES(match_status), match_type = VALUES(match_type), "
                + "last_seen_time = VALUES(last_seen_time), last_sync_time = VALUES(last_sync_time), "
                + "sync_status = 'SUCCESS', sync_error = NULL, delete_enum = 'FALSE', version = version + 1";
        jdbcTemplate.update(sql, SOURCE_CONFIG_CODE, row.staffNo, row.staffNoType, row.findsTrackId,
                row.idCardNo, row.staffName, row.corpinfoId, row.corpinfoName, row.departmentName,
                row.terminalNo, local == null ? null : local.getUserId(),
                local == null ? null : local.getMobileNo(), local == null ? null : local.getIdCardNo(),
                matchStatus, matchType, syncTime, syncTime);
    }

    private void disableMissingRows(LocalDateTime syncTime) {
        jdbcTemplate.update("UPDATE personnel_match SET match_status = 'DISABLED', "
                        + "sync_status = 'SUCCESS', last_sync_time = ?, version = version + 1 "
                        + "WHERE source_config_code = ? AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                        + "AND (last_seen_time IS NULL OR last_seen_time < ?)",
                syncTime, SOURCE_CONFIG_CODE, syncTime);
    }

    private int countMatched(List<StaffSnapshot> rows,
                             Map<String, PositionPersonLocalLookupService.LocalPerson> localPeople) {
        int count = 0;
        for (StaffSnapshot row : rows) {
            if (localPeople.containsKey(row.staffNo)) {
                count++;
            }
        }
        return count;
    }

    private JsonNode successData(JsonNode root) {
        int code = root == null ? -1 : root.path("code").asInt(-1);
        if (code != 200 && code != 0) {
            throw new BizException("FindS staff list synchronization failed, code=" + code);
        }
        return root.path("data");
    }

    private JsonNode extractRows(JsonNode data) {
        if (data == null || data.isNull() || data.isMissingNode()) {
            return null;
        }
        if (data.isArray()) {
            return data;
        }
        for (String field : new String[]{"data", "rows", "list", "records"}) {
            JsonNode value = data.get(field);
            if (value != null && value.isArray()) {
                return value;
            }
        }
        return null;
    }

    private long requiredTotal(JsonNode data) {
        Long total = longValue(firstPresent(data, "total", "totalCount", "count", "totalRecords"));
        if (total == null || total < 0) {
            throw new BizException("FindS staff response is missing a valid total");
        }
        return total;
    }

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

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

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

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

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

    private static class StaffSnapshot {
        private String staffNo;
        private String staffNoType;
        private Long findsTrackId;
        private String idCardNo;
        private String staffName;
        private String corpinfoId;
        private String corpinfoName;
        private String departmentName;
        private String terminalNo;
    }
}
