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.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Refreshes the persisted online/offline state for FindS personnel.
 *
 * <p>Only device status is stored. Coordinates remain outside MySQL.</p>
 */
@Component
@RequiredArgsConstructor
public class FindsStaffLocationStatusSyncService {
    private static final Logger LOGGER = LoggerFactory.getLogger(FindsStaffLocationStatusSyncService.class);

    private static final String STAFF_LIST_API = "finds.staff.list";
    private static final String TERMINAL_LIST_API = "finds.terminal.list";
    private static final String SOURCE_CONFIG_CODE = "FINDS";
    private static final int STAFF_COUNT_PAGE_SIZE = 1;
    private static final int TERMINAL_PAGE_SIZE = 2000;
    private static final int MAX_TERMINAL_PAGE_COUNT = 100;
    private static final int BATCH_CONCURRENCY = 4;
    private static final int FULL_SYNC_INTERVAL_HOURS = 24;
    private static final List<String> ONLINE_DEVICE_STATUSES =
            Arrays.asList("STATIC", "MOVING");

    private final FindsOpenApiClient findsOpenApiClient;
    private final FindsStaffMappingSyncService mappingSyncService;
    private final JdbcTemplate jdbcTemplate;

    public SyncSummary synchronize() {
        LocalDateTime syncTime = LocalDateTime.now().withNano(0);
        long remoteStaffCount = queryRemoteStaffCount();
        long localStaffCount = queryLocalStaffCount();
        boolean fullSync = localStaffCount == 0
                || remoteStaffCount != localStaffCount
                || fullSyncDue(syncTime);
        int fullSyncCount = fullSync ? mappingSyncService.syncAll() : 0;

        Map<String, OnlineTerminal> onlineTerminals = loadOnlineTerminals();
        int onlineMappings = updateLocationStatuses(onlineTerminals, syncTime);
        LOGGER.info("FindS personnel location status synchronization completed, remoteStaffCount={}, "
                        + "localStaffCount={}, fullSync={}, fullSyncCount={}, onlineTerminals={}, onlineMappings={}",
                remoteStaffCount, localStaffCount, fullSync, fullSyncCount,
                onlineTerminals.size(), onlineMappings);
        return new SyncSummary(remoteStaffCount, localStaffCount, fullSync, fullSyncCount,
                onlineTerminals.size(), onlineMappings);
    }

    private long queryRemoteStaffCount() {
        JsonNode data = successData(findsOpenApiClient.postFailFast(STAFF_LIST_API,
                staffCountRequest()));
        return requiredTotal(data, "FindS staff count response");
    }

    private Map<String, Object> staffCountRequest() {
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("pageNo", 1);
        request.put("pageSize", STAFF_COUNT_PAGE_SIZE);
        return request;
    }

    private long queryLocalStaffCount() {
        Long count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM personnel_match "
                        + "WHERE source_config_code = ? "
                        + "AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                        + "AND match_status <> 'DISABLED'",
                Long.class, SOURCE_CONFIG_CODE);
        return count == null ? 0 : count;
    }

    private boolean fullSyncDue(LocalDateTime syncTime) {
        LocalDateTime lastSyncTime = jdbcTemplate.queryForObject(
                "SELECT MAX(last_sync_time) FROM personnel_match "
                        + "WHERE source_config_code = ? "
                        + "AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                        + "AND match_status <> 'DISABLED'",
                LocalDateTime.class, SOURCE_CONFIG_CODE);
        return lastSyncTime == null
                || lastSyncTime.isBefore(syncTime.minusHours(FULL_SYNC_INTERVAL_HOURS));
    }

    private Map<String, OnlineTerminal> loadOnlineTerminals() {
        Map<String, OnlineTerminal> result = new LinkedHashMap<>();
        for (String status : ONLINE_DEVICE_STATUSES) {
            loadOnlineTerminals(status, result);
        }
        return result;
    }

    private void loadOnlineTerminals(String deviceStatus, Map<String, OnlineTerminal> result) {
        JsonNode firstData = successData(findsOpenApiClient.postFailFast(
                TERMINAL_LIST_API, terminalPageRequest(deviceStatus, 1)));
        JsonNode firstRows = extractRows(firstData);
        long total = requiredTotal(firstData, "FindS " + deviceStatus + " terminal response");
        if (total > 0 && (firstRows == null || firstRows.size() == 0)) {
            throw new BizException("FindS terminal status page is empty while total is " + total);
        }
        appendOnlineTerminals(result, firstRows, deviceStatus);

        int effectivePageSize = firstRows == null || firstRows.size() == 0
                ? TERMINAL_PAGE_SIZE : firstRows.size();
        int pageCount = (int) ((total + effectivePageSize - 1) / effectivePageSize);
        if (pageCount > MAX_TERMINAL_PAGE_COUNT) {
            throw new BizException("FindS online terminal pages exceed the configured safety limit: " + pageCount);
        }
        if (pageCount <= 1) {
            return;
        }

        List<Map<String, Object>> requests = new ArrayList<>(pageCount - 1);
        for (int pageNo = 2; pageNo <= pageCount; pageNo++) {
            requests.add(terminalPageRequest(deviceStatus, pageNo));
        }
        long receivedCount = firstRows == null ? 0 : firstRows.size();
        for (JsonNode root : findsOpenApiClient.postFailFastBatch(
                TERMINAL_LIST_API, requests, BATCH_CONCURRENCY)) {
            JsonNode rows = extractRows(successData(root));
            receivedCount += rows == null ? 0 : rows.size();
            appendOnlineTerminals(result, rows, deviceStatus);
        }
        if (receivedCount < total) {
            throw new BizException("FindS online terminal result is incomplete: received="
                    + receivedCount + ", total=" + total);
        }
    }

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

    private void appendOnlineTerminals(Map<String, OnlineTerminal> result,
                                       JsonNode rows,
                                       String requestedStatus) {
        if (rows == null || !rows.isArray()) {
            return;
        }
        for (JsonNode row : rows) {
            String terminalNo = firstText(
                    text(firstPresent(row, "terminalNo", "no", "terminalCode", "imei")));
            if (!StringUtils.hasText(terminalNo)) {
                continue;
            }
            String trackType = firstText(
                    text(firstPresent(row, "trackType")),
                    text(firstPresent(row.path("track"), "trackType")));
            if (StringUtils.hasText(trackType) && !"STAFF".equalsIgnoreCase(trackType)) {
                continue;
            }
            String actualStatus = firstText(
                    text(firstPresent(row, "deviceStatus", "status", "onlineStatus")), requestedStatus);
            result.put(terminalNo, new OnlineTerminal(terminalNo, actualStatus));
        }
    }

    private int updateLocationStatuses(Map<String, OnlineTerminal> onlineTerminals,
                                       LocalDateTime syncTime) {
        String updateOnlineSql = "UPDATE personnel_match SET location_status = 'ONLINE', "
                + "finds_device_status = ?, location_status_time = ?, version = version + 1 "
                + "WHERE source_config_code = ? AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                + "AND finds_terminal_no = ?";
        List<Object[]> args = new ArrayList<>(onlineTerminals.size());
        for (OnlineTerminal terminal : onlineTerminals.values()) {
            args.add(new Object[]{terminal.deviceStatus, syncTime, SOURCE_CONFIG_CODE, terminal.terminalNo});
        }
        int matched = 0;
        if (!args.isEmpty()) {
            int[] updated = jdbcTemplate.batchUpdate(updateOnlineSql, args);
            for (int count : updated) {
                matched += count;
            }
        }

        jdbcTemplate.update("UPDATE personnel_match SET location_status = 'OFFLINE', "
                        + "finds_device_status = 'OFFLINE', location_status_time = ?, version = version + 1 "
                        + "WHERE source_config_code = ? AND (delete_enum IS NULL OR delete_enum = 'FALSE') "
                        + "AND location_status = 'ONLINE' "
                        + "AND (location_status_time IS NULL OR location_status_time < ?)",
                syncTime, SOURCE_CONFIG_CODE, syncTime);
        return matched;
    }

    private JsonNode successData(JsonNode root) {
        int code = root == null ? -1 : root.path("code").asInt(-1);
        if (code != 200 && code != 0) {
            throw new BizException("FindS personnel status 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, String responseName) {
        Long total = longValue(firstPresent(data, "total", "totalCount", "count", "totalRecords"));
        if (total == null || total < 0) {
            throw new BizException(responseName + " 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 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 OnlineTerminal {
        private final String terminalNo;
        private final String deviceStatus;

        private OnlineTerminal(String terminalNo, String deviceStatus) {
            this.terminalNo = terminalNo;
            this.deviceStatus = deviceStatus;
        }
    }

    public static class SyncSummary {
        private final long remoteStaffCount;
        private final long localStaffCount;
        private final boolean fullSync;
        private final int fullSyncCount;
        private final int onlineTerminalCount;
        private final int onlineMappingCount;

        private SyncSummary(long remoteStaffCount, long localStaffCount, boolean fullSync,
                            int fullSyncCount, int onlineTerminalCount, int onlineMappingCount) {
            this.remoteStaffCount = remoteStaffCount;
            this.localStaffCount = localStaffCount;
            this.fullSync = fullSync;
            this.fullSyncCount = fullSyncCount;
            this.onlineTerminalCount = onlineTerminalCount;
            this.onlineMappingCount = onlineMappingCount;
        }

        public long getRemoteStaffCount() {
            return remoteStaffCount;
        }

        public long getLocalStaffCount() {
            return localStaffCount;
        }

        public boolean isFullSync() {
            return fullSync;
        }

        public int getFullSyncCount() {
            return fullSyncCount;
        }

        public int getOnlineTerminalCount() {
            return onlineTerminalCount;
        }

        public int getOnlineMappingCount() {
            return onlineMappingCount;
        }
    }
}
