package tech.easyflow.ai.documentimport.task;

import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.documentimport.DocumentImportBatchDtos;
import tech.easyflow.ai.entity.DocumentImportBatch;
import tech.easyflow.ai.entity.DocumentImportBatchItem;
import tech.easyflow.ai.enums.DocumentImportBatchItemStage;
import tech.easyflow.ai.enums.DocumentImportBatchItemStatus;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.enums.DocumentImportMode;
import tech.easyflow.ai.mapper.DocumentImportBatchItemMapper;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.service.DocumentImportBatchItemService;
import tech.easyflow.ai.service.DocumentImportBatchService;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.math.BigInteger;
import java.util.Date;

/**
 * 批量导入文件项与批次汇总状态跟踪器。
 *
 * @author Codex
 * @since 2026-07-31
 */
@Service
public class DocumentImportBatchTracker {

    private final DocumentImportBatchService batchService;
    private final DocumentImportBatchItemService itemService;
    private final DocumentImportBatchMapper batchMapper;
    private final DocumentImportBatchItemMapper itemMapper;

    /**
     * 创建批次状态跟踪器。
     *
     * @param batchService 批次服务
     * @param itemService 批次项服务
     * @param batchMapper 批次 Mapper
     * @param itemMapper 批次项 Mapper
     */
    public DocumentImportBatchTracker(DocumentImportBatchService batchService,
                                      DocumentImportBatchItemService itemService,
                                      DocumentImportBatchMapper batchMapper,
                                      DocumentImportBatchItemMapper itemMapper) {
        this.batchService = batchService;
        this.itemService = itemService;
        this.batchMapper = batchMapper;
        this.itemMapper = itemMapper;
    }

    /**
     * 查询批次。
     *
     * @param batchId 批次 ID
     * @return 批次实体
     */
    public DocumentImportBatch requireBatch(BigInteger batchId) {
        DocumentImportBatch batch = batchId == null ? null : batchService.getById(batchId);
        if (batch == null) {
            throw new BusinessException("导入批次不存在");
        }
        return batch;
    }

    /**
     * 查询批次文件项。
     *
     * @param itemId 文件项 ID
     * @return 文件项实体
     */
    public DocumentImportBatchItem requireItem(BigInteger itemId) {
        DocumentImportBatchItem item = itemId == null ? null : itemService.getById(itemId);
        if (item == null) {
            throw new BusinessException("导入文件不存在");
        }
        return item;
    }

    /**
     * 判断指定批次是否采用自动导入。
     *
     * @param batchId 批次 ID
     * @return 是否自动导入
     */
    public boolean isAutoBatch(BigInteger batchId) {
        if (batchId == null) {
            return false;
        }
        return DocumentImportMode.AUTO.name().equals(requireBatch(batchId).getImportMode());
    }

    /**
     * 更新文件项阶段与状态。
     *
     * @param itemId 文件项 ID
     * @param stage 处理阶段
     * @param status 处理状态
     * @param errorSummary 错误摘要
     */
    @org.springframework.transaction.annotation.Transactional
    public boolean updateItem(BigInteger itemId,
                              DocumentImportBatchItemStage stage,
                              DocumentImportBatchItemStatus status,
                              String errorSummary) {
        if (itemId == null) {
            return false;
        }
        DocumentImportBatchItem current = requireItem(itemId);
        int attemptDelta = (status == DocumentImportBatchItemStatus.PENDING
            || status == DocumentImportBatchItemStatus.RUNNING)
            && DocumentImportBatchItemStatus.FAILED.name().equals(current.getStatus())
            ? 1
            : 0;
        return transitionItem(itemId, stage, status, errorSummary,
            status == DocumentImportBatchItemStatus.FAILED, attemptDelta, null);
    }

    /**
     * 按预期旧状态原子迁移文件项，并同步更新批次计数。
     *
     * @param itemId 文件项 ID
     * @param stage 新阶段
     * @param status 新状态
     * @param errorSummary 错误摘要
     * @param retryable 是否允许批量重试
     * @param attemptDelta 重试次数增量
     * @return 状态迁移或同状态刷新成功时返回 {@code true}
     */
    @org.springframework.transaction.annotation.Transactional
    public boolean transitionItem(BigInteger itemId,
                                  DocumentImportBatchItemStage stage,
                                  DocumentImportBatchItemStatus status,
                                  String errorSummary,
                                  boolean retryable,
                                  int attemptDelta) {
        return transitionItem(itemId, stage, status, errorSummary,
            retryable, attemptDelta, null);
    }

    /**
     * 按预期旧状态原子迁移文件项，并记录稳定失败码。
     *
     * @param itemId 文件项 ID
     * @param stage 新阶段
     * @param status 新状态
     * @param errorSummary 错误摘要
     * @param retryable 是否允许批量重试
     * @param attemptDelta 重试次数增量
     * @param failureCode 稳定失败码
     * @return 状态迁移或同状态刷新成功时返回 {@code true}
     */
    @org.springframework.transaction.annotation.Transactional
    public boolean transitionItem(BigInteger itemId,
                                  DocumentImportBatchItemStage stage,
                                  DocumentImportBatchItemStatus status,
                                  String errorSummary,
                                  boolean retryable,
                                  int attemptDelta,
                                  String failureCode) {
        if (itemId == null) {
            return false;
        }
        for (int attempt = 0; attempt < 3; attempt++) {
            DocumentImportBatchItem current = requireItem(itemId);
            DocumentImportBatchItemStatus currentStatus =
                DocumentImportBatchItemStatus.valueOf(current.getStatus());
            if (!isAllowedTransition(currentStatus, status)) {
                return false;
            }
            String expectedStatus = current.getStatus();
            Date now = new Date();
            int updated = itemMapper.transitionStatus(
                itemId,
                expectedStatus,
                stage.name(),
                status.name(),
                errorSummary,
                failureCode,
                retryable,
                Math.max(0, attemptDelta),
                now
            );
            if (updated <= 0) {
                continue;
            }
            CounterDelta delta = CounterDelta.between(current, status, retryable);
            if (!delta.isZero()) {
                batchMapper.adjustCounters(
                    current.getBatchId(),
                    delta.completed,
                    delta.processing,
                    delta.failed,
                    delta.pending,
                    delta.uploaded,
                    delta.skipped,
                    delta.cancelled,
                    delta.retryableFailed,
                    now
                );
            }
            refreshBatch(current.getBatchId());
            return true;
        }
        return false;
    }

    /**
     * 校验文件项状态机，拒绝迟到任务覆盖终态。
     *
     * @param current 当前状态
     * @param next 目标状态
     * @return 是否允许迁移
     */
    private boolean isAllowedTransition(DocumentImportBatchItemStatus current,
                                        DocumentImportBatchItemStatus next) {
        if (current == next) {
            return true;
        }
        return switch (next) {
            case UPLOADING -> current == DocumentImportBatchItemStatus.PENDING;
            case UPLOADED -> current == DocumentImportBatchItemStatus.UPLOADING;
            case RUNNING -> current == DocumentImportBatchItemStatus.PENDING
                || current == DocumentImportBatchItemStatus.FAILED;
            case PENDING -> current == DocumentImportBatchItemStatus.RUNNING
                || current == DocumentImportBatchItemStatus.FAILED;
            case FAILED, COMPLETED -> current == DocumentImportBatchItemStatus.RUNNING
                || current == DocumentImportBatchItemStatus.PENDING;
            case SKIPPED -> current == DocumentImportBatchItemStatus.UPLOADED;
            case CANCELLED -> current == DocumentImportBatchItemStatus.PENDING
                || current == DocumentImportBatchItemStatus.UPLOADING
                || current == DocumentImportBatchItemStatus.UPLOADED;
        };
    }

    /**
     * 将已上传或缺少文档的失败项绑定到创建后的文档。
     *
     * @param itemId 文件项 ID
     * @param documentId 文档 ID
     * @throws BusinessException 文件项状态变化或绑定失败时抛出
     */
    @org.springframework.transaction.annotation.Transactional
    public void bindDocument(BigInteger itemId, BigInteger documentId) {
        DocumentImportBatchItem item = requireItem(itemId);
        if (documentId.equals(item.getDocumentId())
            && DocumentImportBatchItemStatus.PENDING.name().equals(item.getStatus())) {
            return;
        }
        Date now = new Date();
        boolean recoveringFailedItem =
            DocumentImportBatchItemStatus.FAILED.name().equals(item.getStatus())
                && item.getDocumentId() == null;
        int updated = recoveringFailedItem
            ? itemMapper.bindFailedDocument(itemId, documentId, now)
            : itemMapper.bindDocument(itemId, documentId, now);
        if (updated <= 0) {
            throw new BusinessException("导入文件状态已变化，请刷新后重试");
        }
        if (recoveringFailedItem) {
            // 防御性恢复属于极低频路径，按文件项真实状态汇总可避免手工计数漂移。
            batchMapper.refreshCountersFromItems(item.getBatchId(), now);
        } else {
            batchMapper.adjustCounters(item.getBatchId(),
                0, 0, 0, 0, -1, 0, 0, 0, now);
        }
        refreshBatch(item.getBatchId());
    }

    /**
     * 原子完成文件上传并增量更新批次上传数。
     *
     * @param itemId 文件项 ID
     * @param filePath 存储路径
     * @param storageLocator 可恢复存储定位符
     * @return 文件项仍处于上传中且完成成功时返回 {@code true}
     */
    @org.springframework.transaction.annotation.Transactional
    public boolean completeUpload(BigInteger itemId,
                                  String filePath,
                                  String storageLocator) {
        DocumentImportBatchItem item = requireItem(itemId);
        if (DocumentImportBatchItemStatus.UPLOADED.name().equals(item.getStatus())) {
            return filePath.equals(item.getFilePath())
                && storageLocator.equals(item.getStorageLocator());
        }
        if (!DocumentImportBatchItemStatus.UPLOADING.name().equals(item.getStatus())) {
            return false;
        }
        Date now = new Date();
        int updated = itemMapper.completeUpload(
            itemId,
            filePath,
            storageLocator,
            now
        );
        if (updated <= 0) {
            return false;
        }
        batchMapper.adjustCounters(item.getBatchId(),
            0, 0, 0, 0, 1, 0, 0, 0, now);
        refreshBatch(item.getBatchId());
        return true;
    }

    /**
     * 记录文件项成功后需要清理的历史文档。
     *
     * @param itemId 文件项 ID
     * @param replacedDocumentId 历史文档 ID
     */
    @org.springframework.transaction.annotation.Transactional
    public void markReplacement(BigInteger itemId, BigInteger replacedDocumentId) {
        Date now = new Date();
        int updated = itemMapper.markReplacement(itemId, replacedDocumentId, now);
        if (updated <= 0) {
            throw new BusinessException("重复文件状态已变化，请刷新后重试");
        }
    }

    /**
     * 清除已完成的历史文档覆盖标记。
     *
     * @param itemId 文件项 ID
     * @param replacedDocumentId 历史文档 ID
     */
    @org.springframework.transaction.annotation.Transactional
    public void clearReplacement(BigInteger itemId, BigInteger replacedDocumentId) {
        itemMapper.clearReplacement(itemId, replacedDocumentId, new Date());
    }

    /**
     * 汇总并持久化批次状态。
     *
     * @param batchId 批次 ID
     * @return 最新状态
     */
    public DocumentImportBatchDtos.StatusResponse refreshBatch(BigInteger batchId) {
        DocumentImportBatch batch = requireBatch(batchId);
        Date now = new Date();
        String nextStatus = batch.getStatus();
        Date nextFinishedAt = batch.getFinishedAt();
        if (batch.getImportMode() == null) {
            if (!DocumentImportBatchStatus.CANCELLED.name().equals(batch.getStatus())) {
                nextStatus = valueOrZero(batch.getUploadedCount()) == valueOrZero(batch.getTotalCount())
                    ? DocumentImportBatchStatus.READY.name()
                    : DocumentImportBatchStatus.UPLOADING.name();
            }
        } else if (!DocumentImportBatchStatus.CANCELLED.name().equals(batch.getStatus())
            && !DocumentImportBatchStatus.INTERRUPTED.name().equals(batch.getStatus())) {
            int terminalCount = valueOrZero(batch.getCompletedCount())
                + valueOrZero(batch.getFailedCount())
                + valueOrZero(batch.getSkippedCount())
                + valueOrZero(batch.getCancelledCount());
            if (terminalCount >= valueOrZero(batch.getTotalCount())
                && valueOrZero(batch.getProcessingCount()) == 0
                && valueOrZero(batch.getPendingCount()) == 0) {
                nextStatus = valueOrZero(batch.getFailedCount()) > 0
                    ? DocumentImportBatchStatus.PARTIAL_SUCCEEDED.name()
                    : DocumentImportBatchStatus.COMPLETED.name();
                nextFinishedAt = now;
            } else {
                nextStatus = DocumentImportBatchStatus.RUNNING.name();
            }
        }
        if (!java.util.Objects.equals(nextStatus, batch.getStatus())
            || !java.util.Objects.equals(nextFinishedAt, batch.getFinishedAt())) {
            batch.setStatus(nextStatus);
            batch.setFinishedAt(nextFinishedAt);
            batch.setModified(now);
            batchService.updateById(batch, false);
        }
        return toStatusResponse(batch);
    }

    /**
     * 将批次标记为已中断，保留已完成文件并允许批量继续。
     *
     * @param batchId 批次 ID
     */
    public void markInterrupted(BigInteger batchId) {
        markInterrupted(
            batchId,
            "execution_interrupted",
            "任务执行中断，请确认服务恢复后继续批次"
        );
    }

    /**
     * 将批次标记为已中断并记录可展示的稳定原因。
     *
     * @param batchId 批次 ID
     * @param interruptCode 稳定中断码
     * @param interruptMessage 用户可见中断原因
     */
    public void markInterrupted(BigInteger batchId,
                                String interruptCode,
                                String interruptMessage) {
        if (batchId == null) {
            return;
        }
        Date now = new Date();
        DocumentImportBatch update = new DocumentImportBatch();
        update.setStatus(DocumentImportBatchStatus.INTERRUPTED.name());
        update.setInterruptCode(interruptCode);
        update.setInterruptMessage(interruptMessage);
        update.setInterruptedAt(now);
        update.setRecoveryPending(false);
        update.setModified(now);
        batchMapper.updateByQuery(update,
            QueryWrapper.create()
                .eq(DocumentImportBatch::getId, batchId)
                .eq(DocumentImportBatch::getStatus, DocumentImportBatchStatus.RUNNING.name()));
    }

    /**
     * 将批次转换为状态响应。
     *
     * @param batch 批次实体
     * @return 状态响应
     */
    public DocumentImportBatchDtos.StatusResponse toStatusResponse(DocumentImportBatch batch) {
        DocumentImportBatchDtos.StatusResponse response = new DocumentImportBatchDtos.StatusResponse();
        response.setBatchId(batch.getId());
        response.setImportMode(batch.getImportMode());
        response.setStatus(batch.getStatus());
        response.setTotalCount(valueOrZero(batch.getTotalCount()));
        response.setTotalBytes(batch.getTotalBytes() == null ? 0L : batch.getTotalBytes());
        response.setCompletedCount(valueOrZero(batch.getCompletedCount()));
        response.setProcessingCount(valueOrZero(batch.getProcessingCount()));
        response.setFailedCount(valueOrZero(batch.getFailedCount()));
        response.setPendingCount(valueOrZero(batch.getPendingCount()));
        response.setSkippedCount(valueOrZero(batch.getSkippedCount()));
        response.setCancelledCount(valueOrZero(batch.getCancelledCount()));
        // 人工继续统一覆盖全部失败项，兼容字段返回相同数量，避免旧计数影响前端判断。
        response.setRetryableFailedCount(valueOrZero(batch.getFailedCount()));
        int total = Math.max(1, valueOrZero(batch.getTotalCount()));
        int terminalCount = valueOrZero(batch.getCompletedCount())
            + valueOrZero(batch.getFailedCount())
            + valueOrZero(batch.getSkippedCount())
            + valueOrZero(batch.getCancelledCount());
        response.setProgressPercent(Math.min(100, terminalCount * 100 / total));
        response.setInterruptCode(batch.getInterruptCode());
        response.setInterruptMessage(batch.getInterruptMessage());
        response.setInterruptedAt(batch.getInterruptedAt());
        response.setStartedAt(batch.getStartedAt());
        response.setFinishedAt(batch.getFinishedAt());
        return response;
    }

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

    /**
     * 文件项状态变化对应的批次计数增量。
     */
    private static final class CounterDelta {
        private int completed;
        private int processing;
        private int failed;
        private int pending;
        private int uploaded;
        private int skipped;
        private int cancelled;
        private int retryableFailed;

        /**
         * 计算文件项状态迁移前后的计数差值。
         *
         * @param current 当前文件项
         * @param nextStatus 新状态
         * @param nextRetryable 新状态是否允许重试
         * @return 计数差值
         */
        private static CounterDelta between(DocumentImportBatchItem current,
                                            DocumentImportBatchItemStatus nextStatus,
                                            boolean nextRetryable) {
            CounterDelta delta = new CounterDelta();
            apply(delta, DocumentImportBatchItemStatus.valueOf(current.getStatus()),
                Boolean.TRUE.equals(current.getRetryable()), -1);
            apply(delta, nextStatus, nextRetryable, 1);
            return delta;
        }

        /**
         * 将一个状态映射到对应计数桶。
         *
         * @param delta 待修改的差值
         * @param status 文件项状态
         * @param retryable 是否允许重试
         * @param direction 增加或减少方向
         */
        private static void apply(CounterDelta delta,
                                  DocumentImportBatchItemStatus status,
                                  boolean retryable,
                                  int direction) {
            switch (status) {
                case COMPLETED -> delta.completed += direction;
                case RUNNING -> delta.processing += direction;
                case FAILED -> {
                    delta.failed += direction;
                    if (retryable) {
                        delta.retryableFailed += direction;
                    }
                }
                case SKIPPED -> delta.skipped += direction;
                case CANCELLED -> delta.cancelled += direction;
                case PENDING, UPLOADING, UPLOADED -> delta.pending += direction;
                default -> throw new IllegalStateException("未知批次文件状态: " + status);
            }
            if (status == DocumentImportBatchItemStatus.UPLOADED) {
                delta.uploaded += direction;
            }
        }

        /**
         * 判断所有增量是否均为零。
         *
         * @return 是否没有计数变化
         */
        private boolean isZero() {
            return completed == 0
                && processing == 0
                && failed == 0
                && pending == 0
                && uploaded == 0
                && skipped == 0
                && cancelled == 0
                && retryableFailed == 0;
        }
    }
}
