package tech.easyflow.ai.documentimport.task;

import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import tech.easyflow.ai.entity.DocumentImportSnapshotCleanup;
import tech.easyflow.ai.mapper.DocumentImportSnapshotCleanupMapper;
import tech.easyflow.ai.mapper.DocumentMapper;
import tech.easyflow.common.util.StringUtil;

import javax.annotation.Resource;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Date;
import java.util.HexFormat;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/**
 * 文档导入中间快照可靠清理服务。
 *
 * <p>先持久化精确清单路径，再按“分片、清单”两个阶段删除。阶段推进先于
 * 清单删除落库，确保进程在任一位置中断后都能安全继续。</p>
 *
 * @author Codex
 * @since 2026-08-04
 */
@Service
public class DocumentImportSnapshotCleanupService {

    public static final String TYPE_CHUNK = "CHUNK";
    public static final String TYPE_CSV_TABLE = "CSV_TABLE";

    private static final Logger LOG =
        LoggerFactory.getLogger(DocumentImportSnapshotCleanupService.class);
    private static final String PHASE_PARTS_PENDING = "PARTS_PENDING";
    private static final String PHASE_MANIFEST_PENDING = "MANIFEST_PENDING";
    private static final int PROCESS_BATCH_SIZE = 50;
    private static final int MAX_PATH_CHARS = 2_048;
    private static final int MAX_ERROR_CHARS = 1_024;
    private static final Duration CLAIM_LEASE = Duration.ofHours(1);
    private static final Duration MIN_RETRY_DELAY = Duration.ofSeconds(30);
    private static final Duration MAX_RETRY_DELAY = Duration.ofHours(1);

    @Resource
    private DocumentImportSnapshotCleanupMapper cleanupMapper;

    @Resource
    private DocumentImportChunkSnapshotService chunkSnapshotService;

    @Resource
    private CsvTableSnapshotService csvTableSnapshotService;

    @Resource
    private DocumentMapper documentMapper;

    private final FlexIDKeyGenerator keyGenerator =
        new FlexIDKeyGenerator();

    /**
     * 登记并尝试清理分块快照。
     *
     * @param manifestPath 分块快照清单路径
     */
    public void scheduleChunkSnapshot(String manifestPath) {
        schedule(null, null, TYPE_CHUNK, manifestPath);
    }

    /**
     * 登记并尝试清理 CSV 表格快照。
     *
     * @param knowledgeId 知识库 ID
     * @param documentId 文档 ID
     * @param manifestPath CSV 表格清单路径
     */
    public void scheduleCsvTableSnapshot(
        BigInteger knowledgeId,
        BigInteger documentId,
        String manifestPath) {
        schedule(
            knowledgeId,
            documentId,
            TYPE_CSV_TABLE,
            manifestPath);
    }

    /**
     * 处理一批已到期的快照清理记录。
     */
    public void processPendingCleanups() {
        Date now = new Date();
        List<DocumentImportSnapshotCleanup> records =
            cleanupMapper.selectDueRecords(now, PROCESS_BATCH_SIZE);
        for (DocumentImportSnapshotCleanup record : records) {
            processRecord(record.getId());
        }
    }

    /**
     * 幂等登记清理记录，并在当前没有事务时立即尝试一次。
     *
     * @param knowledgeId 知识库 ID
     * @param documentId 文档 ID
     * @param snapshotType 快照类型
     * @param manifestPath 清单路径
     */
    private void schedule(
        BigInteger knowledgeId,
        BigInteger documentId,
        String snapshotType,
        String manifestPath) {
        if (!StringUtil.hasText(manifestPath)) {
            return;
        }
        if (manifestPath.length() > MAX_PATH_CHARS) {
            throw new IllegalArgumentException("快照清单路径超过 2048 字符");
        }
        Date now = new Date();
        String pathHash = sha256(manifestPath);
        DocumentImportSnapshotCleanup record =
            new DocumentImportSnapshotCleanup();
        record.setId(generateId(record));
        record.setKnowledgeId(knowledgeId);
        record.setDocumentId(documentId);
        record.setSnapshotType(snapshotType);
        record.setManifestPath(manifestPath);
        record.setPathHash(pathHash);
        record.setPhase(PHASE_PARTS_PENDING);
        record.setAttemptCount(0);
        record.setNextRetryAt(now);
        record.setCreated(now);
        record.setModified(now);
        cleanupMapper.insertIgnore(record);

        DocumentImportSnapshotCleanup persisted =
            cleanupMapper.selectOneByQuery(
                QueryWrapper.create()
                    .eq(
                        DocumentImportSnapshotCleanup::getSnapshotType,
                        snapshotType)
                    .eq(
                        DocumentImportSnapshotCleanup::getPathHash,
                        pathHash));
        if (persisted == null
            || !Objects.equals(
                manifestPath, persisted.getManifestPath())) {
            throw new IllegalStateException("快照清理记录登记失败");
        }
        // 事务中的登记必须随业务提交；后台调度器会在提交后处理。
        if (!TransactionSynchronizationManager
            .isActualTransactionActive()) {
            processRecord(persisted.getId());
        }
    }

    /**
     * 领取并处理一条清理记录。
     *
     * @param recordId 清理记录 ID
     */
    private void processRecord(BigInteger recordId) {
        Date now = new Date();
        String executionToken = UUID.randomUUID().toString();
        Date leaseUntil = new Date(
            now.getTime() + CLAIM_LEASE.toMillis());
        if (cleanupMapper.claim(
            recordId, executionToken, leaseUntil, now) <= 0) {
            return;
        }

        DocumentImportSnapshotCleanup record =
            cleanupMapper.selectOneByQuery(
                QueryWrapper.create()
                    .eq(DocumentImportSnapshotCleanup::getId, recordId)
                    .eq(
                        DocumentImportSnapshotCleanup::getExecutionToken,
                        executionToken));
        if (record == null) {
            return;
        }
        try {
            if (PHASE_PARTS_PENDING.equals(record.getPhase())) {
                deleteParts(record);
                if (cleanupMapper.advanceToManifest(
                    recordId, executionToken, new Date()) <= 0) {
                    throw new IllegalStateException(
                        "快照清理阶段推进失败");
                }
                record.setPhase(PHASE_MANIFEST_PENDING);
            }
            if (!PHASE_MANIFEST_PENDING.equals(record.getPhase())) {
                throw new IllegalStateException(
                    "未知快照清理阶段: " + record.getPhase());
            }
            deleteManifest(record);
            clearCsvSnapshotPointer(record);
            if (cleanupMapper.deleteCompleted(
                recordId, executionToken) <= 0) {
                LOG.warn(
                    "快照对象已清理，但清理记录终态删除未命中: cleanupId={}, path={}",
                    recordId, record.getManifestPath());
            }
        } catch (RuntimeException error) {
            releaseForRetry(record, executionToken, error);
        }
    }

    /**
     * 删除记录指向的全部快照分片。
     *
     * @param record 清理记录
     */
    private void deleteParts(
        DocumentImportSnapshotCleanup record) {
        if (TYPE_CHUNK.equals(record.getSnapshotType())) {
            chunkSnapshotService.deleteParts(
                record.getManifestPath());
            return;
        }
        if (TYPE_CSV_TABLE.equals(record.getSnapshotType())) {
            csvTableSnapshotService.deleteParts(
                record.getManifestPath());
            return;
        }
        throw new IllegalStateException(
            "未知快照类型: " + record.getSnapshotType());
    }

    /**
     * 删除记录指向的快照清单。
     *
     * @param record 清理记录
     */
    private void deleteManifest(
        DocumentImportSnapshotCleanup record) {
        if (TYPE_CHUNK.equals(record.getSnapshotType())) {
            chunkSnapshotService.deleteManifest(
                record.getManifestPath());
            return;
        }
        if (TYPE_CSV_TABLE.equals(record.getSnapshotType())) {
            csvTableSnapshotService.deleteManifest(
                record.getManifestPath());
            return;
        }
        throw new IllegalStateException(
            "未知快照类型: " + record.getSnapshotType());
    }

    /**
     * CSV 清单删除完成后原子移除仍指向该清单的文档选项。
     *
     * @param record 清理记录
     */
    private void clearCsvSnapshotPointer(
        DocumentImportSnapshotCleanup record) {
        if (TYPE_CSV_TABLE.equals(record.getSnapshotType())
            && record.getDocumentId() != null) {
            documentMapper.clearCsvSnapshotPath(
                record.getDocumentId(),
                record.getManifestPath(),
                new Date());
        }
    }

    /**
     * 释放失败记录并按指数退避安排下一次重试。
     *
     * @param record 清理记录
     * @param executionToken 执行令牌
     * @param error 清理异常
     */
    private void releaseForRetry(
        DocumentImportSnapshotCleanup record,
        String executionToken,
        RuntimeException error) {
        Date now = new Date();
        int attempts = Math.max(
            1,
            record.getAttemptCount() == null
                ? 1
                : record.getAttemptCount());
        long multiplier = 1L << Math.min(7, attempts - 1);
        long delayMillis = Math.min(
            MAX_RETRY_DELAY.toMillis(),
            MIN_RETRY_DELAY.toMillis() * multiplier);
        String errorMessage = boundedError(error);
        int released = cleanupMapper.releaseForRetry(
            record.getId(),
            executionToken,
            new Date(now.getTime() + delayMillis),
            errorMessage,
            now);
        LOG.error(
            "文档导入快照清理失败，已安排重试: cleanupId={}, type={}, path={}, released={}",
            record.getId(),
            record.getSnapshotType(),
            record.getManifestPath(),
            released,
            error);
    }

    /**
     * 返回有界错误摘要。
     *
     * @param error 清理异常
     * @return 有界摘要
     */
    private String boundedError(RuntimeException error) {
        String message = error.getMessage();
        String summary = error.getClass().getSimpleName()
            + (StringUtil.hasText(message) ? ": " + message : "");
        return summary.length() <= MAX_ERROR_CHARS
            ? summary
            : summary.substring(0, MAX_ERROR_CHARS);
    }

    /**
     * 计算清单路径 SHA-256。
     *
     * @param value 清单路径
     * @return 十六进制摘要
     */
    private String sha256(String value) {
        try {
            return HexFormat.of().formatHex(
                MessageDigest.getInstance("SHA-256").digest(
                    value.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException error) {
            throw new IllegalStateException(
                "当前运行环境不支持 SHA-256", error);
        }
    }

    /**
     * 生成清理记录主键。
     *
     * @param entity 清理记录
     * @return 主键
     */
    private BigInteger generateId(Object entity) {
        return new BigInteger(
            String.valueOf(keyGenerator.generate(entity, null)));
    }
}
