package com.zcloud.personnel.positioning.command;

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.zcloud.personnel.positioning.integration.finds.FindsOpenApiClient;
import com.zcloud.personnel.positioning.persistence.dataobject.AlarmRecordDO;
import com.zcloud.personnel.positioning.persistence.dataobject.AlarmRecordTargetDO;
import com.zcloud.personnel.positioning.persistence.dataobject.OpenapiConfigDO;
import com.zcloud.personnel.positioning.persistence.dataobject.SyncTaskDO;
import com.zcloud.personnel.positioning.persistence.repository.AlarmRecordRepository;
import com.zcloud.personnel.positioning.persistence.repository.AlarmRecordTargetRepository;
import com.zcloud.personnel.positioning.persistence.repository.OpenapiConfigRepository;
import com.zcloud.personnel.positioning.persistence.repository.SyncTaskRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
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.math.BigDecimal;
import java.time.Instant;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * Incrementally pulls FindS alarm records into local snapshot tables.
 *
 * @Author fangjiakai
 * @Date 2026-07-08
 */
@Component
@RequiredArgsConstructor
public class AlarmRecordFindsPullService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AlarmRecordFindsPullService.class);
    private static final String LIST_API = "finds.alarm.list";
    private static final String TASK_CODE_PREFIX = "FINDS_ALARM_PULL_";
    private static final String TASK_CRON = "0 */2 * * * ?";
    private static final int PULL_PAGE_SIZE = 500;
    private static final int MAX_ROWS_PER_WINDOW = 2000;
    private static final int MAX_PULL_PAGE_COUNT = 200;
    private static final Duration MAX_QUERY_WINDOW = Duration.ofHours(24);
    private static final Duration TOTAL_PROBE_MIN_WINDOW = Duration.ofHours(6);
    private static final Duration MIN_SPLIT_WINDOW = Duration.ofMinutes(1);
    private static final Duration OVERLAP = Duration.ofMinutes(5);

    private final FindsOpenApiClient findsOpenApiClient;
    private final OpenapiConfigRepository openapiConfigRepository;
    private final AlarmRecordRepository alarmRecordRepository;
    private final AlarmRecordTargetRepository alarmRecordTargetRepository;
    private final SyncTaskRepository syncTaskRepository;
    private final ObjectMapper objectMapper;
    private final JdbcTemplate jdbcTemplate;
    private final TransactionTemplate transactionTemplate;
    private final Set<String> inFlight = ConcurrentHashMap.newKeySet();

    @Value("${personnel-positioning.finds.alarm.initial-pull-start-time:2025-04-01 00:00:00}")
    private String initialPullStartTime;

    public int syncIncremental() {
        List<OpenapiConfigDO> configs = openapiConfigRepository.list(baseConfigQuery());
        int pulledCount = 0;
        List<String> failures = new ArrayList<>();
        for (OpenapiConfigDO config : configs) {
            try {
                pulledCount += syncConfigIncremental(config);
            } catch (Exception e) {
                failures.add(configIdentity(config) + ": " + safeMessage(e));
                LOGGER.warn("Incrementally pull FindS alarms failed, config={}", configIdentity(config), e);
            }
        }
        if (!failures.isEmpty()) {
            throw new BizException("部分FindS告警增量同步失败: " + String.join("; ", failures));
        }
        return pulledCount;
    }

    private int syncConfigIncremental(OpenapiConfigDO config) {
        String lockKey = configIdentity(config);
        if (!inFlight.add(lockKey)) {
            return 0;
        }
        SyncTaskDO task = null;
        try {
            task = getOrCreateTask(config);
            if (Integer.valueOf(0).equals(task.getEnabled())) {
                return 0;
            }
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime startTime = task.getLastExecuteTime() == null
                    ? initialFullPullStartTime()
                    : task.getLastExecuteTime().minus(OVERLAP);
            int pulledCount = 0;
            while (startTime.isBefore(now)) {
                LocalDateTime endTime = min(startTime.plus(MAX_QUERY_WINDOW), now);
                int windowCount = refreshConfig(config, startTime, endTime);
                pulledCount += windowCount;
                updateTaskSuccess(task, endTime, windowCount);
                startTime = endTime;
            }
            return pulledCount;
        } catch (Exception e) {
            if (task != null) {
                updateTaskFailure(task, e);
            }
            throw e;
        } finally {
            inFlight.remove(lockKey);
        }
    }

    private int refreshConfig(OpenapiConfigDO config, LocalDateTime startTime, LocalDateTime endTime) {
        Duration window = Duration.between(startTime, endTime);
        if (window.compareTo(TOTAL_PROBE_MIN_WINDOW) > 0) {
            return splitWindow(config, startTime, endTime);
        }
        if (window.compareTo(TOTAL_PROBE_MIN_WINDOW) >= 0
                && queryWindowTotal(config, startTime, endTime) > MAX_ROWS_PER_WINDOW) {
            return splitWindow(config, startTime, endTime);
        }
        int pageNo = 1;
        long loaded = 0;
        long total = Long.MAX_VALUE;
        while (loaded < total && pageNo <= MAX_PULL_PAGE_COUNT) {
            Map<String, Object> request = new LinkedHashMap<>();
            request.put("pageNo", pageNo);
            request.put("pageSize", PULL_PAGE_SIZE);
            request.put("startTime", toEpochMilli(startTime));
            request.put("endTime", toEpochMilli(endTime));
            JsonNode root = findsOpenApiClient.post(config, LIST_API, request);
            JsonNode data = assertSuccess(root, LIST_API).path("data");
            JsonNode rows = extractRows(data);
            if (!rows.isArray() || rows.size() == 0) {
                if (loaded > 0 && total != Long.MAX_VALUE && loaded < total) {
                    throw new BizException("FindS告警分页提前结束，游标未推进");
                }
                total = loaded;
                break;
            }
            long pageTotal = totalValue(data, loaded + rows.size());
            if (pageNo == 1 && pageTotal > MAX_ROWS_PER_WINDOW
                    && Duration.between(startTime, endTime).compareTo(MIN_SPLIT_WINDOW) > 0) {
                return splitWindow(config, startTime, endTime);
            }
            transactionTemplate.execute(status -> {
                for (JsonNode row : rows) {
                    upsertRecord(config, row);
                }
                return null;
            });
            loaded += rows.size();
            total = pageTotal;
            pageNo++;
        }
        if (loaded < total) {
            return splitWindow(config, startTime, endTime);
        }
        return safeLongToInt(loaded);
    }

    private long queryWindowTotal(OpenapiConfigDO config, LocalDateTime startTime, LocalDateTime endTime) {
        Map<String, Object> request = new LinkedHashMap<>();
        request.put("pageNo", 1);
        request.put("pageSize", 1);
        request.put("startTime", toEpochMilli(startTime));
        request.put("endTime", toEpochMilli(endTime));
        JsonNode root = findsOpenApiClient.post(config, LIST_API, request);
        JsonNode data = assertSuccess(root, LIST_API).path("data");
        JsonNode rows = extractRows(data);
        return totalValue(data, rows.isArray() ? rows.size() : 0);
    }

    private int splitWindow(OpenapiConfigDO config, LocalDateTime startTime, LocalDateTime endTime) {
        Duration window = Duration.between(startTime, endTime);
        if (window.compareTo(MIN_SPLIT_WINDOW) <= 0) {
            throw new BizException("FindS alarm volume exceeds page limit within minimum one-minute window");
        }
        LocalDateTime midpoint = startTime.plusNanos(window.toNanos() / 2);
        LOGGER.info("Split FindS alarm pull window, config={}, start={}, midpoint={}, end={}",
                configIdentity(config), startTime, midpoint, endTime);
        int leftCount = refreshConfig(config, startTime, midpoint);
        int rightCount = refreshConfig(config, midpoint, endTime);
        return safeLongToInt((long) leftCount + rightCount);
    }

    @Transactional(rollbackFor = Exception.class)
    public AlarmRecordDO upsertRecord(OpenapiConfigDO config, JsonNode row) {
        Long findsAlarmId = longValue(row.path("id"));
        String alarmNo = firstText(row.path("alarmNo"), null);
        AlarmRecordDO record = queryExistingRecord(config, findsAlarmId, alarmNo);
        boolean isNew = record == null;
        if (isNew) {
            record = new AlarmRecordDO();
        }
        record.setFindsAlarmId(findsAlarmId);
        record.setAlarmNo(defaultIfBlank(alarmNo, findsAlarmId == null ? record.getAlarmNo() : "FA" + findsAlarmId));
        record.setAlarmCode(firstText(row.path("alarmCode"), record.getAlarmCode()));
        record.setAlarmName(defaultIfBlank(firstText(row.path("name"), record.getAlarmName()),
                alarmNameOf(record.getAlarmCode())));
        record.setDescription(firstText(row.path("description"), record.getDescription()));
        record.setDisposeStatus(defaultIfBlank(firstText(row.path("disposeStatus"), record.getDisposeStatus()), "INIT"));
        record.setDisposeDescription(firstText(row.path("disposeDescription"), record.getDisposeDescription()));
        record.setDuration(longValue(row.path("duration")));
        record.setStartTime(localDateTimeValue(row.path("startTime")));
        record.setEndTime(localDateTimeValue(row.path("endTime")));
        record.setStartFencesJson(jsonValue(row.path("startFences")));
        record.setEndFencesJson(jsonValue(row.path("endFences")));
        fillLocation(record, row.path("location"));
        record.setCorpinfoId(config.getCorpinfoId() == null ? "0" : config.getCorpinfoId());
        record.setCorpinfoName(defaultIfBlank(config.getCorpinfoName(), resolveCorpinfoName(config.getCorpinfoId())));
        fillFirstTrack(record, row.path("trackInfoList"));
        record.setTerminalNoListJson(jsonValue(row.path("terminalNoList")));
        record.setSyncTime(LocalDateTime.now());
        record.setRawJson(row.toString());
        if (isNew) {
            alarmRecordRepository.save(record);
        } else {
            alarmRecordRepository.updateById(record);
        }
        rebuildTargets(record, row);
        return record;
    }

    private AlarmRecordDO queryExistingRecord(OpenapiConfigDO config, Long findsAlarmId, String alarmNo) {
        QueryWrapper<AlarmRecordDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        if (findsAlarmId != null) {
            queryWrapper.eq("finds_alarm_id", findsAlarmId);
        } else if (StringUtils.hasText(alarmNo)) {
            queryWrapper.eq("alarm_no", alarmNo);
        } else {
            return null;
        }
        if (config.getCorpinfoId() != null) {
            queryWrapper.eq("corpinfo_id", config.getCorpinfoId());
        } else {
            queryWrapper.and(wrapper -> wrapper.isNull("corpinfo_id").or().eq("corpinfo_id", 0));
        }
        queryWrapper.last("LIMIT 1");
        return alarmRecordRepository.getOne(queryWrapper, false);
    }

    private void fillLocation(AlarmRecordDO record, JsonNode location) {
        if (location == null || location.isMissingNode() || location.isNull()) {
            return;
        }
        record.setLon(decimalValue(location.path("lon")));
        record.setLat(decimalValue(location.path("lat")));
        record.setAlt(decimalValue(location.path("alt")));
        record.setSpeed(decimalValue(location.path("speed")));
        record.setDirection(decimalValue(location.path("direction")));
        record.setGt(localDateTimeValue(location.path("gt")));
    }

    private void fillFirstTrack(AlarmRecordDO record, JsonNode trackInfoList) {
        JsonNode firstTrack = firstStaffTrack(trackInfoList);
        if (firstTrack == null) {
            return;
        }
        record.setFirstIdCardNo(firstText(firstTrack.path("idCardNo"), record.getFirstIdCardNo()));
        record.setFirstStaffNoSnapshot(firstText(firstTrack.path("trackNo"), record.getFirstStaffNoSnapshot()));
        record.setFirstStaffNameSnapshot(firstText(firstTrack.path("trackName"), record.getFirstStaffNameSnapshot()));
    }

    private JsonNode firstStaffTrack(JsonNode trackInfoList) {
        if (!trackInfoList.isArray() || trackInfoList.size() == 0) {
            return null;
        }
        for (JsonNode item : trackInfoList) {
            if ("STAFF".equalsIgnoreCase(item.path("trackType").asText())) {
                return item;
            }
        }
        return trackInfoList.get(0);
    }

    private void rebuildTargets(AlarmRecordDO record, JsonNode row) {
        QueryWrapper<AlarmRecordTargetDO> removeWrapper = new QueryWrapper<>();
        removeWrapper.eq("alarm_record_id", record.getId());
        alarmRecordTargetRepository.remove(removeWrapper);

        JsonNode trackInfoList = row.path("trackInfoList");
        if (!trackInfoList.isArray()) {
            return;
        }
        for (int i = 0; i < trackInfoList.size(); i++) {
            JsonNode track = trackInfoList.get(i);
            AlarmRecordTargetDO target = new AlarmRecordTargetDO();
            target.setAlarmRecordId(record.getId());
            target.setCorpinfoId(record.getCorpinfoId());
            target.setCorpinfoName(record.getCorpinfoName());
            target.setTrackId(longValue(track.path("trackId")));
            target.setTrackType(firstText(track.path("trackType"), null));
            target.setTrackNo(firstText(track.path("trackNo"), null));
            target.setTrackName(firstText(track.path("trackName"), null));
            target.setTerminalNo(terminalNoAt(row.path("terminalNoList"), i));
            target.setIdCardNo(firstText(track.path("idCardNo"), null));
            if ("STAFF".equalsIgnoreCase(target.getTrackType())) {
                target.setStaffNoSnapshot(target.getTrackNo());
            }
            alarmRecordTargetRepository.save(target);
        }
    }

    private SyncTaskDO getOrCreateTask(OpenapiConfigDO config) {
        String taskCode = TASK_CODE_PREFIX + configIdentity(config);
        QueryWrapper<SyncTaskDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("task_code", taskCode);
        queryWrapper.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        queryWrapper.last("LIMIT 1");
        SyncTaskDO task = syncTaskRepository.getOne(queryWrapper, false);
        if (task != null) {
            return task;
        }
        task = new SyncTaskDO();
        task.setTaskCode(taskCode);
        task.setTaskName("FindS告警增量拉取-" + configIdentity(config));
        task.setBizType("ALARM");
        task.setCorpinfoId(config.getCorpinfoId() == null ? "0" : config.getCorpinfoId());
        task.setCorpinfoName(config.getCorpinfoName());
        task.setSyncDirection("PULL");
        task.setCronExpr(TASK_CRON);
        task.setEnabled(1);
        task.setLastStatus("INIT");
        task.setLastMessage("等待首次执行");
        syncTaskRepository.save(task);
        return task;
    }

    private void updateTaskSuccess(SyncTaskDO task, LocalDateTime cursor, int windowCount) {
        task.setLastExecuteTime(cursor);
        task.setNextExecuteTime(LocalDateTime.now().plusMinutes(2));
        task.setLastStatus("SUCCESS");
        task.setLastMessage("游标已推进至 " + cursor + "，本窗口拉取 " + windowCount + " 条");
        syncTaskRepository.updateById(task);
    }

    private void updateTaskFailure(SyncTaskDO task, Exception e) {
        task.setNextExecuteTime(LocalDateTime.now().plusMinutes(2));
        task.setLastStatus("FAIL");
        task.setLastMessage(safeMessage(e));
        syncTaskRepository.updateById(task);
    }

    private String configIdentity(OpenapiConfigDO config) {
        if (config.getId() != null) {
            return String.valueOf(config.getId());
        }
        return StringUtils.hasText(config.getConfigCode()) ? config.getConfigCode() : "DEFAULT";
    }

    private String safeMessage(Throwable e) {
        String message = e == null ? null : e.getMessage();
        if (!StringUtils.hasText(message)) {
            message = e == null ? "FindS alarm incremental synchronization failed" : e.getClass().getSimpleName();
        }
        return message.length() > 1000 ? message.substring(0, 1000) : message;
    }

    private LocalDateTime min(LocalDateTime left, LocalDateTime right) {
        return left.isBefore(right) ? left : right;
    }

    private LocalDateTime initialFullPullStartTime() {
        if (!StringUtils.hasText(initialPullStartTime)) {
            throw new BizException("FindS initial alarm pull start time is not configured");
        }
        String value = initialPullStartTime.trim();
        for (DateTimeFormatter formatter : new DateTimeFormatter[]{
                DateTimeFormatter.ISO_LOCAL_DATE_TIME,
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
        }) {
            try {
                return LocalDateTime.parse(value, formatter);
            } catch (DateTimeParseException ignored) {
            }
        }
        throw new BizException("Invalid FindS initial alarm pull start time: " + value);
    }

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

    private QueryWrapper<OpenapiConfigDO> baseConfigQuery() {
        QueryWrapper<OpenapiConfigDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("enabled", 1);
        queryWrapper.and(wrapper -> wrapper.isNull("delete_enum").or().eq("delete_enum", "FALSE"));
        queryWrapper.orderByDesc("update_time");
        return queryWrapper;
    }

    private JsonNode assertSuccess(JsonNode root, String apiName) {
        if (root == null || !"200".equals(root.path("code").asText())) {
            String msg = root == null ? "" : root.path("msg").asText();
            String detail = root == null ? "" : root.path("detail").asText();
            throw new BizException("FindS OpenAPI call failed: " + apiName + ", " + msg + " " + detail);
        }
        return root;
    }

    private JsonNode extractRows(JsonNode data) {
        if (data == null || data.isMissingNode() || data.isNull()) {
            return objectMapper.createArrayNode();
        }
        if (data.isArray()) {
            return data;
        }
        JsonNode direct = firstArray(data.path("data"), data.path("rows"), data.path("records"), data.path("list"));
        if (direct != null) {
            return direct;
        }
        JsonNode nested = firstArray(data.path("data").path("data"), data.path("data").path("rows"));
        return nested == null ? objectMapper.createArrayNode() : nested;
    }

    private JsonNode firstArray(JsonNode... nodes) {
        for (JsonNode node : nodes) {
            if (node != null && node.isArray()) {
                return node;
            }
        }
        return null;
    }

    private long totalValue(JsonNode data, long defaultValue) {
        for (String field : new String[]{"total", "totalCount", "count"}) {
            Long value = longValue(data.path(field));
            if (value != null) {
                return value;
            }
        }
        return defaultValue;
    }

    private long toEpochMilli(LocalDateTime value) {
        return value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }

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

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

    private LocalDateTime localDateTimeValue(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull() || !StringUtils.hasText(node.asText())) {
            return null;
        }
        Long epoch = longValue(node);
        if (epoch != null) {
            long millis = epoch > 9999999999L ? epoch : epoch * 1000;
            return LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneId.systemDefault());
        }
        String value = node.asText().trim();
        try {
            return OffsetDateTime.parse(value).toLocalDateTime();
        } catch (DateTimeParseException ignored) {
        }
        try {
            return LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
        } catch (DateTimeParseException ignored) {
        }
        for (DateTimeFormatter formatter : new DateTimeFormatter[]{
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
        }) {
            try {
                return LocalDateTime.parse(value, formatter);
            } catch (DateTimeParseException ignored) {
            }
        }
        return null;
    }

    private String jsonValue(JsonNode node) {
        if (node == null || node.isMissingNode() || node.isNull()) {
            return null;
        }
        try {
            return objectMapper.writeValueAsString(node);
        } catch (IOException e) {
            return null;
        }
    }

    private String terminalNoAt(JsonNode terminalNoList, int index) {
        if (terminalNoList == null || terminalNoList.isMissingNode() || terminalNoList.isNull()) {
            return null;
        }
        if (terminalNoList.isArray()) {
            if (terminalNoList.size() == 1) {
                return terminalNoList.get(0).asText();
            }
            if (index >= 0 && index < terminalNoList.size()) {
                return terminalNoList.get(index).asText();
            }
            List<String> values = new ArrayList<>();
            for (JsonNode item : terminalNoList) {
                if (StringUtils.hasText(item.asText())) {
                    values.add(item.asText());
                }
            }
            return values.stream().collect(Collectors.joining(","));
        }
        return terminalNoList.asText();
    }

    private String resolveCorpinfoName(String corpinfoId) {
        if (corpinfoId == null) {
            return null;
        }
        try {
            List<String> names = jdbcTemplate.queryForList(
                    "SELECT corp_name FROM corp_info WHERE CAST(corpinfo_id AS CHAR) = ? "
                            + "AND (delete_enum IS NULL OR delete_enum = 'FALSE') LIMIT 1",
                    String.class, corpinfoId);
            return names.isEmpty() ? null : names.get(0);
        } catch (Exception ignored) {
            return null;
        }
    }

    private String alarmNameOf(String alarmCode) {
        if ("CROSS_IN".equals(alarmCode)) {
            return "进入围栏告警";
        }
        if ("CROSS_OUT".equals(alarmCode)) {
            return "离开围栏告警";
        }
        if ("RETENTION".equals(alarmCode)) {
            return "超时停留告警";
        }
        if ("LACKED".equals(alarmCode)) {
            return "脱岗告警";
        }
        if ("STAY".equals(alarmCode)) {
            return "停留告警";
        }
        if ("CRASH".equals(alarmCode)) {
            return "碰撞告警";
        }
        return alarmCode;
    }

    private String firstText(JsonNode node, String defaultValue) {
        return node != null && !node.isMissingNode() && !node.isNull() && StringUtils.hasText(node.asText())
                ? node.asText()
                : defaultValue;
    }

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