package tech.easyflow.ai.easyagentsflow.upload;

import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.chain.repository.ChainStateRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
import tech.easyflow.common.filestorage.FileStorageWriteResult;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

/**
 * Public Workflow API multipart 临时文件生命周期服务。
 *
 * <p>上传文件在请求线程中写入统一存储，工作流运行或挂起期间保留，
 * 工作流终态、启动失败或状态丢失后由幂等清理流程删除。</p>
 */
@Service
public class WorkflowApiUploadLifecycleService {

    private static final Logger LOG = LoggerFactory.getLogger(
            WorkflowApiUploadLifecycleService.class);
    private static final Duration STAGED_RETENTION = Duration.ofHours(1);
    private static final Duration TERMINAL_RETENTION = Duration.ofHours(1);
    private static final Duration ACTIVE_RECHECK = Duration.ofHours(24);
    private static final Duration CLEANUP_LOCK_WAIT = Duration.ZERO;
    private static final Duration CLEANUP_LOCK_LEASE = Duration.ofMinutes(5);
    private static final Duration CLEANUP_RETRY_DELAY =
            Duration.ofMinutes(5);
    private static final String STORAGE_PATH_PREFIX =
            "workflow-api-upload/";
    private static final String CLEANUP_LOCK_PREFIX =
            "easyflow:workflow:api-upload:cleanup-lock:";

    private final WorkflowRunningParameterResolver parameterResolver;
    private final FileStorageService fileStorageService;
    private final WorkflowApiMultipartFileNormalizer fileNormalizer;
    private final WorkflowApiUploadStore uploadStore;
    private final ChainStateRepository chainStateRepository;
    private final RedisLockExecutor redisLockExecutor;

    /**
     * 创建工作流 API 临时文件生命周期服务。
     *
     * @param parameterResolver 工作流运行参数解析器
     * @param fileStorageService 文件存储服务
     * @param fileNormalizer Multipart 文件元数据归一化器
     * @param uploadStore 临时上传记录存储
     * @param chainStateRepository 工作流状态仓储
     * @param redisLockExecutor Redis 分布式锁执行器
     */
    public WorkflowApiUploadLifecycleService(
            WorkflowRunningParameterResolver parameterResolver,
            @Qualifier("default") FileStorageService fileStorageService,
            WorkflowApiMultipartFileNormalizer fileNormalizer,
            WorkflowApiUploadStore uploadStore,
            ChainStateRepository chainStateRepository,
            RedisLockExecutor redisLockExecutor) {
        this.parameterResolver = parameterResolver;
        this.fileStorageService = fileStorageService;
        this.fileNormalizer = fileNormalizer;
        this.uploadStore = uploadStore;
        this.chainStateRepository = chainStateRepository;
        this.redisLockExecutor = redisLockExecutor;
    }

    /**
     * 校验、存储 multipart 文件并注入工作流变量。
     *
     * @param requestId HTTP 请求关联标识
     * @param workflowContent 已发布工作流内容
     * @param variables 普通运行变量
     * @param fileParts 以工作流文件参数名分组的 multipart 文件
     * @return 临时上传准备结果
     */
    public WorkflowApiPreparedUpload prepare(
            String requestId,
            String workflowContent,
            Map<String, Object> variables,
            Map<String, List<MultipartFile>> fileParts) {
        if (fileParts == null || fileParts.isEmpty()) {
            throw new BusinessException(
                    400,
                    40016,
                    "multipart 请求至少需要上传一个 files.<开始节点参数名> 文件 Part");
        }

        Map<String, List<MultipartFile>> normalizedFileParts =
                normalizeFileParts(fileParts);

        Set<String> fileParameterNames =
                parameterResolver.resolveFileParameterNames(workflowContent);
        Set<String> requiredFileParameterNames =
                parameterResolver.resolveRequiredFileParameterNames(
                        workflowContent);
        Map<String, Object> baseVariables = new LinkedHashMap<>();
        if (variables != null) {
            baseVariables.putAll(variables);
        }
        validateFileParts(
                workflowContent,
                baseVariables,
                normalizedFileParts,
                fileParameterNames,
                requiredFileParameterNames);

        WorkflowApiUploadRecord record = new WorkflowApiUploadRecord();
        long now = System.currentTimeMillis();
        record.setUploadId(UUID.randomUUID().toString().replace("-", ""));
        record.setRequestId(StringUtils.hasText(requestId)
                ? requestId
                : record.getUploadId());
        record.setCreatedAt(now);
        record.setCleanupAt(now + STAGED_RETENTION.toMillis());

        try {
            uploadStore.create(record);
            Map<String, Object> resolvedVariables =
                    storeFiles(
                            baseVariables,
                            normalizedFileParts,
                            record);
            Map<String, Object> normalized =
                    parameterResolver.normalizeRuntimeVariables(
                            workflowContent,
                            resolvedVariables);
            return new WorkflowApiPreparedUpload(
                    record.getUploadId(),
                    normalized);
        } catch (RuntimeException | Error error) {
            try {
                cleanupPreparationFailure(record);
            } catch (RuntimeException cleanupError) {
                error.addSuppressed(cleanupError);
            }
            throw error;
        }
    }

    /**
     * 在工作流首个节点启动前绑定执行 ID。
     *
     * @param uploadId 内部临时上传 ID
     * @param executeId 工作流执行 ID
     */
    public void bindExecution(String uploadId, String executeId) {
        uploadStore.bindExecution(uploadId, executeId);
        WorkflowApiUploadRecord record = uploadStore.find(uploadId)
                .orElseThrow(() -> new IllegalStateException(
                        "工作流临时上传记录不存在: " + uploadId));
        uploadStore.schedule(
                record,
                System.currentTimeMillis() + ACTIVE_RECHECK.toMillis());
    }

    /**
     * 标记工作流执行已进入终态，交由清理任务删除文件。
     *
     * @param executeId 工作流执行 ID
     */
    public void markExecutionTerminal(String executeId) {
        uploadStore.findByExecutionId(executeId).ifPresent(record ->
                uploadStore.schedule(
                        record,
                        System.currentTimeMillis()
                                + TERMINAL_RETENTION.toMillis()));
    }

    /**
     * 终止尚未成功启动的临时上传并立即清理。
     *
     * @param uploadId 内部临时上传 ID
     */
    public void abort(String uploadId) {
        cleanupRequest(uploadId, true);
    }

    /**
     * 清理一批到期上传记录。
     *
     * @param limit 单次最大处理数量
     * @return 已领取并完成一次处理的上传记录数量
     */
    public int cleanupExpired(int limit) {
        int processed = 0;
        int safeLimit = Math.max(1, limit);
        for (int index = 0; index < safeLimit; index++) {
            long now = System.currentTimeMillis();
            Optional<String> claimed = uploadStore.claimExpired(
                    now,
                    now + CLEANUP_RETRY_DELAY.toMillis());
            if (claimed.isEmpty()) {
                break;
            }
            String uploadId = claimed.get();
            processed++;
            try {
                cleanupRequest(uploadId, false);
            } catch (RuntimeException error) {
                logCleanupFailure(uploadId, error);
            }
        }
        return processed;
    }

    /**
     * 校验 multipart 文件字段和既有变量冲突，并复用运行参数校验限制。
     *
     * @param workflowContent 工作流内容
     * @param variables 普通变量
     * @param fileParts 文件 Part
     * @param fileParameterNames 文件参数名
     * @param requiredFileParameterNames 必填文件参数名
     */
    private void validateFileParts(
            String workflowContent,
            Map<String, Object> variables,
            Map<String, List<MultipartFile>> fileParts,
            Set<String> fileParameterNames,
            Set<String> requiredFileParameterNames) {
        for (String requiredParameterName
                : requiredFileParameterNames) {
            List<MultipartFile> uploaded =
                    fileParts.get(requiredParameterName);
            if (!hasValue(variables.get(requiredParameterName))
                    && (uploaded == null || uploaded.isEmpty())) {
                throw new BusinessException(
                        400,
                        40016,
                        "缺少必填文件参数 " + requiredParameterName
                                + "，请使用 files."
                                + requiredParameterName);
            }
        }
        Map<String, Object> candidates = new LinkedHashMap<>(variables);
        for (Map.Entry<String, List<MultipartFile>> entry :
                fileParts.entrySet()) {
            String parameterName = entry.getKey();
            if (!StringUtils.hasText(parameterName)
                    || !fileParameterNames.contains(parameterName)) {
                throw new BusinessException(
                        400,
                        40016,
                        "文件 Part " + parameterName
                                + " 不是开始节点的文件参数");
            }
            if (hasValue(variables.get(parameterName))) {
                throw new BusinessException(
                        400,
                        40016,
                        "文件参数 " + parameterName
                                + " 不能同时通过 metadata 和文件 Part 传值");
            }
            List<MultipartFile> files = entry.getValue();
            if (files == null || files.isEmpty()) {
                throw new BusinessException(
                        400,
                        40016,
                        "文件参数 " + parameterName + " 不能为空");
            }
            List<Map<String, Object>> descriptors =
                    new ArrayList<>(files.size());
            for (int index = 0; index < files.size(); index++) {
                MultipartFile file = files.get(index);
                validateMultipartFile(file, parameterName);
                descriptors.add(fileDescriptor(
                        file,
                        "multipart://" + parameterName + "/" + index));
            }
            candidates.put(parameterName, descriptors);
        }
        try {
            parameterResolver.normalizeRuntimeVariables(
                    workflowContent,
                    candidates);
        } catch (BusinessException error) {
            throw translateFileValidationFailure(error);
        }
    }

    /**
     * 将运行参数解析器中的文件校验错误转换为稳定公共错误码。
     *
     * @param error 原文件参数校验异常
     * @return 原异常或带稳定错误码的异常
     */
    private BusinessException translateFileValidationFailure(
            BusinessException error) {
        String message = error.getMessage();
        if (message == null || !message.startsWith("文件参数 ")) {
            return error;
        }
        boolean limitExceeded = message.contains("超过")
                || message.contains("最多上传");
        return new BusinessException(
                limitExceeded ? 413 : 400,
                limitExceeded ? 41301 : 40016,
                message,
                error);
    }

    /**
     * 将通过校验的文件写入统一存储。
     *
     * @param variables 普通变量
     * @param fileParts 文件 Part
     * @param record 上传记录
     * @return 已注入真实存储路径的变量
     */
    private Map<String, Object> storeFiles(
            Map<String, Object> variables,
            Map<String, List<MultipartFile>> fileParts,
            WorkflowApiUploadRecord record) {
        Map<String, Object> resolved = new LinkedHashMap<>(variables);
        for (Map.Entry<String, List<MultipartFile>> entry :
                fileParts.entrySet()) {
            List<Map<String, Object>> descriptors =
                    new ArrayList<>(entry.getValue().size());
            for (MultipartFile file : entry.getValue()) {
                FileStorageWriteHandle writeHandle;
                try {
                    writeHandle = fileStorageService.prepareRecoverableWrite(
                                STORAGE_PATH_PREFIX
                                        + record.getUploadId(),
                                buildStorageFilename(
                                        file,
                                        record.getStoredFiles().size()));
                } catch (RuntimeException error) {
                    throw translateStorageFailure(error);
                }
                String locator = writeHandle.encodeLocator();
                int storedFileIndex = record.getStoredFiles().size();
                record.getStoredFiles().add(
                        new WorkflowApiStoredFile(null, locator));
                // 先持久化精确 locator，物理写入中途退出后仍可由清理任务定位。
                uploadStore.save(record);

                FileStorageWriteResult writeResult;
                try {
                    writeResult = fileStorageService.saveRecoverable(
                            file,
                            writeHandle);
                } catch (RuntimeException error) {
                    throw translateStorageFailure(error);
                }
                if (!locator.equals(writeResult.getLocator())) {
                    throw new IllegalStateException(
                            "文件存储返回了不一致的恢复定位符");
                }
                if (!StringUtils.hasText(writeResult.getUrl())) {
                    throw new IllegalStateException(
                            "文件存储未返回有效路径: "
                                    + file.getOriginalFilename());
                }
                record.getStoredFiles().set(
                        storedFileIndex,
                        record.getStoredFiles()
                                .get(storedFileIndex)
                                .withFilePath(writeResult.getUrl()));
                uploadStore.save(record);
                descriptors.add(fileDescriptor(
                        file,
                        writeResult.getUrl()));
            }
            resolved.put(entry.getKey(), descriptors);
        }
        return resolved;
    }

    /**
     * 构建不包含用户目录片段的稳定存储文件名。
     *
     * @param file 上传文件
     * @param index 当前请求内文件序号
     * @return 安全存储文件名
     */
    private String buildStorageFilename(
            MultipartFile file,
            int index) {
        String original = file.getOriginalFilename();
        String extension = "";
        int separator = original == null
                ? -1
                : original.lastIndexOf('.');
        if (separator >= 0 && separator < original.length() - 1) {
            String candidate = original.substring(separator + 1);
            if (candidate.length() <= 16
                    && candidate.matches("[A-Za-z0-9]+")) {
                extension = "." + candidate.toLowerCase(Locale.ROOT);
            }
        }
        return String.format(
                Locale.ROOT,
                "%03d-%s%s",
                index,
                UUID.randomUUID().toString().replace("-", ""),
                extension);
    }

    /**
     * 校验单个 multipart 文件的基础元数据。
     *
     * @param file 文件
     * @param parameterName 工作流文件参数名
     */
    private void validateMultipartFile(
            MultipartFile file,
            String parameterName) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(
                    400,
                    40016,
                    "文件参数 " + parameterName + " 包含空文件");
        }
        if (!StringUtils.hasText(file.getOriginalFilename())) {
            throw new BusinessException(
                    400,
                    40016,
                    "文件参数 " + parameterName + " 缺少文件名");
        }
    }

    /**
     * 归一化全部文件 Part 的文件名和内容类型。
     *
     * @param fileParts 原始文件 Part
     * @return 保持参数和文件顺序的归一化视图
     */
    private Map<String, List<MultipartFile>> normalizeFileParts(
            Map<String, List<MultipartFile>> fileParts) {
        Map<String, List<MultipartFile>> normalized =
                new LinkedHashMap<>();
        for (Map.Entry<String, List<MultipartFile>> entry
                : fileParts.entrySet()) {
            List<MultipartFile> source = entry.getValue();
            if (source == null) {
                normalized.put(entry.getKey(), List.of());
                continue;
            }
            normalized.put(
                    entry.getKey(),
                    source.stream()
                            .map(fileNormalizer::normalize)
                            .toList());
        }
        return normalized;
    }

    /**
     * 将对象存储异常转换为不泄露底层配置的公共错误。
     *
     * @param error 原始存储异常
     * @return 安全业务异常
     */
    private BusinessException translateStorageFailure(
            RuntimeException error) {
        if (isTransientStorageFailure(error)) {
            return new BusinessException(
                    503,
                    50301,
                    "文件存储暂时不可用，请稍后重试",
                    error);
        }
        return new BusinessException(
                500,
                50001,
                "文件存储处理失败，请联系管理员并提供 requestId",
                error);
    }

    /**
     * 保守识别可直接重试的网络和超时故障。
     *
     * @param error 原始异常
     * @return 是否为暂时性依赖故障
     */
    private boolean isTransientStorageFailure(Throwable error) {
        Throwable current = error;
        while (current != null) {
            if (current instanceof ConnectException
                    || current instanceof SocketTimeoutException
                    || current instanceof HttpTimeoutException
                    || current instanceof TimeoutException) {
                return true;
            }
            String className = current.getClass().getSimpleName();
            if ("InsufficientDataException".equals(className)
                    || "ServerException".equals(className)) {
                return true;
            }
            String message = current.getMessage();
            if (message != null) {
                String normalized = message.toLowerCase(Locale.ROOT);
                if (normalized.contains("timeout")
                        || normalized.contains("timed out")
                        || normalized.contains("connection refused")
                        || normalized.contains("temporarily unavailable")
                        || normalized.contains("service unavailable")) {
                    return true;
                }
            }
            current = current.getCause();
        }
        return false;
    }

    /**
     * 构建工作流运行态文件描述。
     *
     * @param file multipart 文件
     * @param filePath 存储路径或校验占位路径
     * @return 文件描述
     */
    private Map<String, Object> fileDescriptor(
            MultipartFile file,
            String filePath) {
        Map<String, Object> descriptor = new LinkedHashMap<>();
        descriptor.put("fileName", file.getOriginalFilename());
        descriptor.put("filePath", filePath);
        if (StringUtils.hasText(file.getContentType())) {
            descriptor.put("contentType", file.getContentType());
        }
        descriptor.put("size", file.getSize());
        return descriptor;
    }

    /**
     * 判断 metadata 中是否已经提供有效值。
     *
     * @param value metadata 变量值
     * @return 是否存在有效值
     */
    private boolean hasValue(Object value) {
        if (value == null) {
            return false;
        }
        if (value instanceof String text) {
            return StringUtils.hasText(text);
        }
        if (value instanceof Collection<?> collection) {
            return !collection.isEmpty();
        }
        return true;
    }

    /**
     * 使用请求线程持有的最新恢复定位符清理准备阶段失败的文件。
     *
     * <p>当 Redis 更新恰好失败时，重新读取的记录可能缺少最后一次状态，
     * 因此必须优先使用内存中的记录执行补偿。</p>
     *
     * @param record 请求线程持有的最新上传记录
     */
    private void cleanupPreparationFailure(
            WorkflowApiUploadRecord record) {
        try {
            deleteFiles(record);
            uploadStore.remove(record);
        } catch (RuntimeException cleanupError) {
            try {
                uploadStore.save(record);
            } catch (RuntimeException persistenceError) {
                cleanupError.addSuppressed(persistenceError);
            }
            throw cleanupError;
        }
    }

    /**
     * 在分布式锁下清理一条上传记录。
     *
     * @param uploadId 内部上传 ID
     * @param force 是否忽略工作流运行状态立即清理
     * @return 是否成功删除记录
     */
    private boolean cleanupRequest(String uploadId, boolean force) {
        RedisLockExecutor.LockHandle handle =
                redisLockExecutor.tryAcquire(
                        CLEANUP_LOCK_PREFIX + uploadId,
                        CLEANUP_LOCK_WAIT,
                        CLEANUP_LOCK_LEASE);
        if (handle == null) {
            return false;
        }
        try (handle) {
            WorkflowApiUploadRecord record =
                    uploadStore.find(uploadId).orElse(null);
            if (record == null) {
                uploadStore.removeMissingIndex(uploadId);
                return false;
            }
            if (!force && shouldRetain(record)) {
                uploadStore.schedule(
                        record,
                        System.currentTimeMillis()
                                + ACTIVE_RECHECK.toMillis());
                return false;
            }
            deleteFiles(record);
            uploadStore.remove(record);
            return true;
        }
    }

    /**
     * 记录带 HTTP 请求关联标识的异步清理异常。
     *
     * @param uploadId 内部上传 ID
     * @param error 清理异常
     */
    private void logCleanupFailure(
            String uploadId,
            RuntimeException error) {
        WorkflowApiUploadRecord record = null;
        try {
            record = uploadStore.find(uploadId).orElse(null);
        } catch (RuntimeException lookupError) {
            if (lookupError != error) {
                error.addSuppressed(lookupError);
            }
        }
        LOG.error(
                "清理工作流 API 临时上传失败, uploadId={}, requestId={}, executeId={}",
                uploadId,
                record == null ? null : record.getRequestId(),
                record == null ? null : record.getExecuteId(),
                error);
    }

    /**
     * 判断上传记录是否仍被运行中或挂起的工作流使用。
     *
     * @param record 上传记录
     * @return 是否需要继续保留
     */
    private boolean shouldRetain(WorkflowApiUploadRecord record) {
        if (!StringUtils.hasText(record.getExecuteId())) {
            return false;
        }
        ChainState state = chainStateRepository.load(record.getExecuteId());
        return state != null
                && state.getStatus() != null
                && !state.getStatus().isTerminal();
    }

    /**
     * 幂等删除记录中的全部临时文件。
     *
     * @param record 上传记录
     */
    private void deleteFiles(WorkflowApiUploadRecord record) {
        RuntimeException firstFailure = null;
        for (WorkflowApiStoredFile storedFile :
                record.getStoredFiles()) {
            try {
                fileStorageService.deleteRecoverable(
                        FileStorageWriteHandle.decodeLocator(
                                storedFile.storageLocator()));
            } catch (RuntimeException error) {
                firstFailure = appendFailure(firstFailure, error);
            }
        }
        // 兼容开发阶段已经写入 Redis 的旧版 URL 记录。
        for (String filePath : record.getFilePaths()) {
            try {
                fileStorageService.delete(filePath);
            } catch (RuntimeException error) {
                firstFailure = appendFailure(firstFailure, error);
            }
        }
        if (firstFailure != null) {
            throw firstFailure;
        }
    }

    /**
     * 聚合文件清理异常并保留全部失败原因。
     *
     * @param firstFailure 首个异常
     * @param currentFailure 当前异常
     * @return 聚合后的首个异常
     */
    private RuntimeException appendFailure(
            RuntimeException firstFailure,
            RuntimeException currentFailure) {
        if (firstFailure == null) {
            return currentFailure;
        }
        firstFailure.addSuppressed(currentFailure);
        return firstFailure;
    }
}
