package tech.easyflow.ai.documentimport.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.entity.DocumentImportBatch;
import tech.easyflow.ai.entity.DocumentImportTask;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.enums.DocumentImportMode;
import tech.easyflow.ai.enums.DocumentImportTaskStatus;
import tech.easyflow.ai.mapper.DocumentImportBatchItemMapper;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.mapper.DocumentImportTaskMapper;

import java.math.BigInteger;
import java.sql.SQLException;
import java.sql.SQLRecoverableException;
import java.sql.SQLTimeoutException;
import java.sql.SQLTransientConnectionException;
import java.util.Date;
import java.util.concurrent.RejectedExecutionException;

/**
 * 知识库自动导入批次熔断器。
 *
 * <p>消费者或调度器发生未被业务流程收口的基础设施异常时，通过
 * 数据库 CAS 立即停止整个自动导入批次，撤销活跃任务执行令牌，
 * 并把未完成文件保留为可恢复失败。</p>
 */
@Service
public class DocumentImportBatchCircuitBreaker {

    private static final Logger LOG =
        LoggerFactory.getLogger(DocumentImportBatchCircuitBreaker.class);
    private static final String REDIS_UNAVAILABLE = "redis_unavailable";
    private static final String DATABASE_UNAVAILABLE = "database_unavailable";
    private static final String WORKER_OVERLOADED = "worker_overloaded";
    private static final String INFRASTRUCTURE_FAILURE =
        "document_import_infrastructure_failure";

    private final DocumentImportBatchMapper batchMapper;
    private final DocumentImportBatchItemMapper itemMapper;
    private final DocumentImportTaskMapper taskMapper;

    /**
     * 创建自动导入批次熔断器。
     *
     * @param batchMapper 批次 Mapper
     * @param itemMapper 批次项 Mapper
     * @param taskMapper 导入任务 Mapper
     */
    public DocumentImportBatchCircuitBreaker(
        DocumentImportBatchMapper batchMapper,
        DocumentImportBatchItemMapper itemMapper,
        DocumentImportTaskMapper taskMapper) {
        this.batchMapper = batchMapper;
        this.itemMapper = itemMapper;
        this.taskMapper = taskMapper;
    }

    /**
     * 根据异常任务强制中断所属自动导入批次。
     *
     * <p>该方法使用独立事务，保证调用方业务事务已经回滚时仍能写入
     * 可恢复终态。技术异常通过完整堆栈写入后端日志；数据库仅保存
     * 稳定错误码和面向用户的安全摘要。</p>
     *
     * @param taskId 触发异常的任务 ID
     * @param error 原始异常
     * @return 批次已经中断或已经结束时返回 {@code true}；无自动批次
     *         归属时返回 {@code false}
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean interruptTaskBatch(BigInteger taskId, Throwable error) {
        DocumentImportTask task =
            taskId == null ? null : taskMapper.selectOneById(taskId);
        if (task == null) {
            LOG.warn("忽略无法关联任务的自动导入熔断请求: taskId={}", taskId);
            return true;
        }
        if (task.getBatchId() == null) {
            return false;
        }
        if (!isActiveTask(task)) {
            LOG.info(
                "忽略已结束任务的迟到熔断请求: taskId={}, batchId={}, status={}",
                task.getId(), task.getBatchId(), task.getStatus());
            return true;
        }
        return interruptBatch(
            task.getBatchId(),
            resolveReason(error),
            task.getId(),
            task.getPhase(),
            error
        );
    }

    /**
     * 根据系统异常强制中断指定自动导入批次。
     *
     * @param batchId 批次 ID
     * @param error 原始异常
     * @return 批次已经中断、结束或不存在时返回 {@code true}
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean interruptBatch(BigInteger batchId, Throwable error) {
        return interruptBatch(
            batchId,
            resolveReason(error),
            null,
            null,
            error
        );
    }

    /**
     * 仅在恢复令牌仍由当前实例持有时中断指定批次。
     *
     * @param batchId 批次 ID
     * @param recoveryToken 当前恢复调度令牌
     * @param error 原始异常
     * @return 批次已停止时返回 {@code true}；令牌失效时返回 {@code false}
     * @throws IllegalArgumentException 恢复令牌为空时抛出
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean interruptRecoveryBatch(BigInteger batchId,
                                          String recoveryToken,
                                          Throwable error) {
        if (recoveryToken == null || recoveryToken.isBlank()) {
            throw new IllegalArgumentException("恢复调度令牌不能为空");
        }
        return interruptBatch(
            batchId,
            resolveReason(error),
            null,
            null,
            error,
            recoveryToken
        );
    }

    /**
     * 使用指定稳定原因强制中断自动导入批次。
     *
     * @param batchId 批次 ID
     * @param interruptCode 稳定中断码
     * @param interruptMessage 用户可见中断原因
     * @param error 原始异常
     * @return 批次已经中断、结束或不存在时返回 {@code true}
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean interruptBatch(BigInteger batchId,
                                  String interruptCode,
                                  String interruptMessage,
                                  Throwable error) {
        return interruptBatch(
            batchId,
            new InterruptionReason(interruptCode, interruptMessage),
            null,
            null,
            error
        );
    }

    /**
     * 加入调用方事务并使用指定原因中断批次。
     *
     * <p>供超时回收等已经持有任务行锁的事务使用，避免开启新事务
     * 等待调用方自身尚未提交的行锁。</p>
     *
     * @param batchId 批次 ID
     * @param interruptCode 稳定中断码
     * @param interruptMessage 用户可见中断原因
     * @param error 原始异常
     * @return 批次是否已经停止运行
     */
    @Transactional(propagation = Propagation.MANDATORY)
    public boolean interruptBatchInCurrentTransaction(
        BigInteger batchId,
        String interruptCode,
        String interruptMessage,
        Throwable error) {
        return interruptBatch(
            batchId,
            new InterruptionReason(interruptCode, interruptMessage),
            null,
            null,
            error
        );
    }

    /**
     * 原子中断批次并统一收口关联任务、文档和文件项。
     *
     * @param batchId 批次 ID
     * @param reason 稳定中断原因
     * @param taskId 触发任务 ID，可为空
     * @param phase 触发任务阶段，可为空
     * @param error 原始异常
     * @return 批次是否已经停止运行
     */
    private boolean interruptBatch(BigInteger batchId,
                                   InterruptionReason reason,
                                   BigInteger taskId,
                                   String phase,
                                   Throwable error) {
        return interruptBatch(
            batchId, reason, taskId, phase, error, null);
    }

    /**
     * 按调用来源围栏原子中断批次并收口关联状态。
     *
     * @param batchId 批次 ID
     * @param reason 稳定中断原因
     * @param taskId 触发任务 ID，可为空
     * @param phase 触发任务阶段，可为空
     * @param error 原始异常
     * @param recoveryToken 恢复调度令牌，可为空
     * @return 批次是否已经停止运行；围栏失效时返回 {@code false}
     */
    private boolean interruptBatch(BigInteger batchId,
                                   InterruptionReason reason,
                                   BigInteger taskId,
                                   String phase,
                                   Throwable error,
                                   String recoveryToken) {
        if (batchId == null) {
            return false;
        }
        DocumentImportBatch batch = batchMapper.selectOneById(batchId);
        if (batch == null) {
            LOG.warn(
                "忽略无法关联批次的自动导入熔断请求: taskId={}, batchId={}",
                taskId, batchId);
            return true;
        }
        if (!DocumentImportMode.AUTO.name().equals(batch.getImportMode())) {
            return false;
        }
        if (!DocumentImportBatchStatus.RUNNING.name().equals(batch.getStatus())) {
            return true;
        }

        Date now = new Date();
        int interrupted;
        if (recoveryToken != null) {
            interrupted = batchMapper.interruptOwnedRecoveryBatch(
                batch.getId(), recoveryToken, reason.code, reason.message, now);
        } else if (taskId != null) {
            interrupted = batchMapper.interruptRunningBatchForActiveTask(
                batch.getId(), taskId, reason.code, reason.message, now);
        } else {
            interrupted = batchMapper.interruptRunningBatch(
                batch.getId(), reason.code, reason.message, now);
        }
        if (interrupted <= 0) {
            DocumentImportBatch current = batchMapper.selectOneById(batch.getId());
            if (current == null
                || !DocumentImportBatchStatus.RUNNING.name()
                    .equals(current.getStatus())) {
                return true;
            }
            if (taskId != null) {
                DocumentImportTask currentTask = taskMapper.selectOneById(taskId);
                return currentTask == null || !isActiveTask(currentTask);
            }
            return false;
        }

        // 先同步文档列表，再撤销任务令牌；两个更新处于同一事务中。
        int interruptedDocuments = taskMapper.interruptActiveDocuments(
            batch.getId(), reason.message, reason.code, now, BigInteger.ZERO);
        int interruptedTasks = taskMapper.interruptActiveTasks(
            batch.getId(), reason.message, reason.code, now, BigInteger.ZERO);
        int interruptedItems = itemMapper.interruptActiveItems(
            batch.getId(), reason.message, reason.code, now);
        batchMapper.refreshCountersFromItems(batch.getId(), now);

        LOG.error(
            "知识库自动导入批次已强制中断: batchId={}, taskId={}, phase={}, "
                + "interruptCode={}, interruptedTasks={}, interruptedItems={}, "
                + "interruptedDocuments={}",
            batch.getId(),
            taskId,
            phase,
            reason.code,
            interruptedTasks,
            interruptedItems,
            interruptedDocuments,
            error);
        return true;
    }

    /**
     * 将异常链归类为稳定中断原因。
     *
     * @param error 原始异常
     * @return 稳定错误码与用户可见摘要
     */
    private InterruptionReason resolveReason(Throwable error) {
        if (containsRedisFailure(error)) {
            return new InterruptionReason(
                REDIS_UNAVAILABLE,
                "缓存与消息服务异常，自动导入已中断，请确认服务恢复后继续"
            );
        }
        if (containsWorkerOverload(error)) {
            return new InterruptionReason(
                WORKER_OVERLOADED,
                "导入任务处理资源已满，批次已中断，请稍后继续"
            );
        }
        if (containsDataAccessFailure(error)) {
            return new InterruptionReason(
                DATABASE_UNAVAILABLE,
                "数据库服务异常，自动导入已中断，请确认服务恢复后继续"
            );
        }
        return new InterruptionReason(
            INFRASTRUCTURE_FAILURE,
            "自动导入发生系统异常，批次已中断，请联系管理员排查后继续"
        );
    }

    /**
     * 判断异常链是否来自 Redis/Lettuce。
     *
     * @param error 原始异常
     * @return 是否是 Redis 故障
     */
    private boolean containsRedisFailure(Throwable error) {
        Throwable current = error;
        while (current != null) {
            String className = current.getClass().getName();
            if (className.startsWith("io.lettuce.")
                || className.contains(".redis.")
                || className.contains("RedisCommand")) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }

    /**
     * 判断异常链是否包含线程池拒绝。
     *
     * @param error 原始异常
     * @return 是否是任务资源过载
     */
    private boolean containsWorkerOverload(Throwable error) {
        Throwable current = error;
        while (current != null) {
            if (current instanceof RejectedExecutionException
                || current.getClass().getSimpleName()
                    .contains("TaskRejectedException")) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }

    /**
     * 判断异常链是否包含数据库连接、资源或超时异常。
     *
     * @param error 原始异常
     * @return 是否是数据库故障
     */
    private boolean containsDataAccessFailure(Throwable error) {
        Throwable current = error;
        while (current != null) {
            if (current instanceof DataAccessResourceFailureException
                || current instanceof TransientDataAccessResourceException
                || current instanceof QueryTimeoutException
                || current instanceof SQLTransientConnectionException
                || current instanceof SQLRecoverableException
                || current instanceof SQLTimeoutException
                || isSqlConnectionFailure(current)
                || "org.hibernate.exception.JDBCConnectionException"
                    .equals(current.getClass().getName())) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }

    /**
     * 根据 SQLState 判断是否属于数据库连接类故障。
     *
     * @param error 当前异常
     * @return SQLState 使用连接异常类别 08 时返回 {@code true}
     */
    private boolean isSqlConnectionFailure(Throwable error) {
        if (!(error instanceof SQLException sqlError)) {
            return false;
        }
        String sqlState = sqlError.getSQLState();
        return sqlState != null && sqlState.startsWith("08");
    }

    /**
     * 判断任务是否仍可能代表当前批次的活跃执行。
     *
     * @param task 导入任务
     * @return 待处理或运行中返回 {@code true}
     */
    private boolean isActiveTask(DocumentImportTask task) {
        return task != null
            && (DocumentImportTaskStatus.PENDING.name().equals(task.getStatus())
                || DocumentImportTaskStatus.RUNNING.name().equals(task.getStatus()));
    }

    /**
     * 稳定中断码与安全展示文案。
     */
    private static final class InterruptionReason {
        private final String code;
        private final String message;

        /**
         * 创建中断原因。
         *
         * @param code 稳定错误码
         * @param message 用户可见摘要
         */
        private InterruptionReason(String code, String message) {
            this.code = code;
            this.message = message;
        }
    }
}
