package tech.easyflow.skill.store;

import com.easyagents.skill.store.SkillContentStore;
import com.easyagents.skill.store.SkillContentStage;
import com.easyagents.skill.util.SkillHashes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.cache.DistributedScheduledLock;
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 tech.easyflow.skill.entity.SkillContent;
import tech.easyflow.skill.entity.SkillContentWriteIntent;
import tech.easyflow.skill.mapper.SkillContentMapper;
import tech.easyflow.skill.mapper.SkillContentWriteIntentMapper;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;

/**
 * 基于数据库引用索引与平台文件存储的 Skill 二进制内容仓库。
 *
 * <p>put、retain 与 release 的引用计数变更始终加入调用方事务。新内容在物理写入前通过独立事务
 * 保存可恢复写入意图，正式索引与意图删除在调用方事务中原子提交；进程在任意写入阶段退出时，
 * 数据库回滚都会恢复可供定时任务精确清理的意图。最后引用对应的物理文件仅在事务提交后删除。</p>
 */
@Component
public class DBSkillContentStore implements SkillContentStore {

    private static final Logger LOG = LoggerFactory.getLogger(DBSkillContentStore.class);
    private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
    private static final String PENDING_PREFIX = "__PENDING__:";
    private static final String INTENT_PENDING = "PENDING";
    private static final String INTENT_WRITING = "WRITING";
    private static final String INTENT_CLEANING = "CLEANING";
    private static final String CONTENT_PATH_PREFIX = "skill-content/";
    private static final Pattern CONTENT_REF_PATTERN = Pattern.compile("sha256:[0-9a-f]{64}");
    private static final int RELEASE_RETRY_LIMIT = 8;

    private final SkillContentMapper skillContentMapper;
    private final SkillContentWriteIntentMapper writeIntentMapper;
    private final FileStorageService fileStorageService;
    private final TransactionTemplate requiredTransactionTemplate;
    private final TransactionTemplate cleanupTransactionTemplate;

    /** 未完成内容占位的最长保留时间。 */
    @Value("${easyflow.skill.content-pending-ttl-ms:1800000}")
    private long pendingTtlMs = 1_800_000L;

    /** 零引用内容进入定时重试前的保护时间，避免与提交后清理并发。 */
    @Value("${easyflow.skill.content-release-retry-delay-ms:60000}")
    private long releaseRetryDelayMs = 60_000L;

    /** 单次清理任务处理的最大记录数。 */
    @Value("${easyflow.skill.content-cleanup-batch-size:100}")
    private int cleanupBatchSize = 100;

    /**
     * 创建 Skill 内容仓库。
     *
     * @param skillContentMapper 内容索引 Mapper
     * @param writeIntentMapper 内容写入意图 Mapper
     * @param fileStorageService 平台文件存储
     * @param transactionManager 平台事务管理器
     */
    public DBSkillContentStore(SkillContentMapper skillContentMapper,
                               SkillContentWriteIntentMapper writeIntentMapper,
                               @Qualifier("default") FileStorageService fileStorageService,
                               PlatformTransactionManager transactionManager) {
        this.skillContentMapper = skillContentMapper;
        this.writeIntentMapper = writeIntentMapper;
        this.fileStorageService = fileStorageService;
        this.requiredTransactionTemplate = new TransactionTemplate(transactionManager);
        this.cleanupTransactionTemplate = new TransactionTemplate(transactionManager);
        this.cleanupTransactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    }

    /**
     * 保存字节内容并取得一份引用。
     *
     * @param bytes 内容字节
     * @return sha256 内容引用
     */
    @Override
    public String put(byte[] bytes) {
        byte[] safeBytes = bytes == null ? new byte[0] : bytes.clone();
        return executeRequired(() -> putKnownHash(
                new ByteArrayMultipartFile(safeBytes, "content.bin", DEFAULT_MEDIA_TYPE),
                SkillHashes.sha256Ref(safeBytes), safeBytes.length, DEFAULT_MEDIA_TYPE));
    }

    /**
     * 将二进制内容流式写入本机受控临时文件，完成全包校验前不增加正式引用。
     *
     * @param inputStream 内容流
     * @param maxBytes 最大字节数
     * @return 暂存描述
     */
    @Override
    public SkillContentStage stage(InputStream inputStream, long maxBytes) {
        if (inputStream == null || maxBytes < 0) {
            throw new BusinessException("Skill 内容暂存参数不正确");
        }
        Path path = null;
        try {
            path = Files.createTempFile("easyflow-skill-stage-", ".bin");
            java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
            long size = 0;
            try (OutputStream output = Files.newOutputStream(path, StandardOpenOption.TRUNCATE_EXISTING)) {
                byte[] buffer = new byte[8192];
                int length;
                while ((length = inputStream.read(buffer)) >= 0) {
                    if (length == 0) {
                        continue;
                    }
                    if (size > maxBytes - length) {
                        throw new BusinessException("Skill 二进制内容超过 " + maxBytes + " 字节限制");
                    }
                    size += length;
                    digest.update(buffer, 0, length);
                    output.write(buffer, 0, length);
                }
            }
            String hash = java.util.HexFormat.of().formatHex(digest.digest());
            return new SkillContentStage(path.toAbsolutePath().toString(), "sha256:" + hash, hash, size);
        } catch (BusinessException exception) {
            deleteStageQuietly(path);
            throw exception;
        } catch (Exception exception) {
            deleteStageQuietly(path);
            throw new BusinessException(500, 500, "暂存 Skill 二进制内容失败", exception);
        }
    }

    /**
     * 提交一个已校验的临时内容并取得正式引用。
     *
     * @param stage 暂存描述
     * @return 正式内容引用
     */
    @Override
    public String commit(SkillContentStage stage) {
        return commitStage(stage, DEFAULT_MEDIA_TYPE);
    }

    /**
     * 以指定媒体类型提交一个已校验的临时内容。
     *
     * @param stage 暂存描述
     * @param mediaType 内容媒体类型
     * @return 正式内容引用
     */
    private String commitStage(SkillContentStage stage, String mediaType) {
        Path path = requireStagePath(stage);
        try {
            validateStage(stage, path);
            String normalizedMediaType = normalizeMediaType(mediaType);
            PathMultipartFile file = new PathMultipartFile(
                    path, stage.getContentRef() + ".bin", normalizedMediaType);
            String contentRef = executeRequired(() ->
                    putKnownHash(file, stage.getContentRef(), stage.getSize(), normalizedMediaType));
            deleteStageQuietly(path);
            return contentRef;
        } catch (RuntimeException exception) {
            deleteStageQuietly(path);
            throw exception;
        }
    }

    /**
     * 回滚尚未提交的临时内容。
     *
     * @param stage 暂存描述
     */
    @Override
    public void rollback(SkillContentStage stage) {
        if (stage != null) {
            deleteStageQuietly(requireStagePath(stage));
        }
    }

    /**
     * 保存上传文件并取得一份引用，计算 hash 时使用输入流，不创建额外全量字节副本。
     *
     * @param file 上传文件
     * @param mediaType 媒体类型
     * @return sha256 内容引用
     */
    public String put(MultipartFile file, String mediaType) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException("Skill 资源文件不能为空");
        }
        try (InputStream inputStream = file.getInputStream()) {
            SkillContentStage stage = stage(inputStream, file.getSize());
            return commitStage(stage, mediaType);
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "读取 Skill 资源文件失败", exception);
        }
    }

    /**
     * 为已有内容增加一份持有引用。
     *
     * @param contentRef 内容引用
     */
    @Override
    public void retain(String contentRef) {
        executeRequired(() -> {
            if (contentRef == null || contentRef.isBlank() || skillContentMapper.retain(contentRef) != 1) {
                throw new BusinessException(404, 404, "Skill 二进制内容不存在");
            }
            return null;
        });
    }

    /**
     * 释放一份内容引用，最后一个持有者释放后删除物理文件。
     *
     * @param contentRef 内容引用
     */
    @Override
    public void release(String contentRef) {
        if (contentRef == null || contentRef.isBlank()) {
            return;
        }
        executeRequired(() -> {
            releaseInTransaction(contentRef);
            return null;
        });
    }

    /**
     * 打开内容读取流。
     *
     * @param contentRef 内容引用
     * @return 内容流，调用方负责关闭
     */
    @Override
    public InputStream open(String contentRef) {
        SkillContent content = requireContent(contentRef);
        try {
            return fileStorageService.readStream(content.getFilePath());
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "读取 Skill 二进制内容失败", exception);
        }
    }

    /**
     * 将内容流式复制到目标输出流。
     *
     * @param contentRef 内容引用
     * @param outputStream 目标输出流
     */
    public void transferTo(String contentRef, OutputStream outputStream) {
        try (InputStream inputStream = open(contentRef)) {
            inputStream.transferTo(outputStream);
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "输出 Skill 二进制内容失败", exception);
        }
    }

    /**
     * 判断内容是否存在。
     *
     * @param contentRef 内容引用
     * @return 存在时为 true
     */
    @Override
    public boolean exists(String contentRef) {
        return contentRef != null && !contentRef.isBlank() && skillContentMapper.countVisible(contentRef) > 0;
    }

    /**
     * 在当前事务中抢占或复用指定 hash 的正式内容引用。
     *
     * @param file 待保存文件
     * @param contentRef 内容引用
     * @param size 内容大小
     * @param mediaType 媒体类型
     * @return 正式内容引用
     */
    private String putKnownHash(MultipartFile file, String contentRef, long size, String mediaType) {
        validateContentRef(contentRef);
        if (file == null || size < 0) {
            throw new BusinessException("Skill 内容写入参数不正确");
        }
        String hex = contentRef.substring("sha256:".length());
        String storagePath = expectedStoragePath(hex);
        String filename = expectedStorageFilename(hex);
        FileStorageWriteHandle handle = fileStorageService.prepareRecoverableWrite(storagePath, filename);
        validateDeterministicHandle(handle, contentRef);
        String locator = handle.encodeLocator();
        String reservationToken = java.util.UUID.randomUUID().toString();
        int reserved;
        boolean duplicateIntent = false;
        try {
            reserved = executeCleanupTransaction(() -> writeIntentMapper.reserve(
                    contentRef, reservationToken, hex, locator, mediaType, size));
        } catch (DuplicateKeyException duplicateException) {
            reserved = 0;
            duplicateIntent = true;
        }
        if (reserved != 1) {
            if (!duplicateIntent) {
                throw new BusinessException(500, 500, "创建 Skill 内容写入意图失败");
            }
            if (skillContentMapper.retainMatching(contentRef, size) == 1) {
                return contentRef;
            }
            throw conflictingContentException(skillContentMapper.selectForUpdate(contentRef), size);
        }

        // 独立意图提交后才触碰内容索引，避免外层缺失主键 gap lock 与内层预留等待形成等待环。
        if (skillContentMapper.retainMatching(contentRef, size) == 1) {
            if (writeIntentMapper.deleteIfActiveExists(contentRef, reservationToken) != 1) {
                throw new BusinessException(500, 500, "复用 Skill 内容后删除写入意图失败");
            }
            return contentRef;
        }

        SkillContent existing = skillContentMapper.selectForUpdate(contentRef);
        if (existing != null) {
            if (isReleasedLegacyContent(existing, contentRef, size)) {
                try {
                    verifyLegacyPhysicalContent(existing, contentRef, size);
                } catch (RuntimeException exception) {
                    discardPendingIntent(contentRef, reservationToken, exception);
                    throw exception;
                }
                if (skillContentMapper.resurrectVerifiedLegacy(
                        contentRef, hex, existing.getFilePath(), size) != 1) {
                    BusinessException exception = new BusinessException(
                            503, 5031, "Skill 旧版内容状态已变化，请重试");
                    discardPendingIntent(contentRef, reservationToken, exception);
                    throw exception;
                }
                if (writeIntentMapper.deleteIfActiveExists(contentRef, reservationToken) != 1) {
                    throw new BusinessException(500, 500, "恢复 Skill 旧版内容后删除写入意图失败");
                }
                return contentRef;
            }
            BusinessException exception = conflictingContentException(existing, size);
            discardPendingIntent(contentRef, reservationToken, exception);
            throw exception;
        }

        if (writeIntentMapper.claimForWrite(contentRef, reservationToken) != 1) {
            throw new BusinessException(503, 5031, "Skill 内容写入意图状态已变化，请重试");
        }
        try {
            FileStorageWriteResult result = fileStorageService.saveRecoverable(file, handle);
            if (!locator.equals(result.getLocator())) {
                throw new BusinessException(500, 500, "Skill 文件存储返回了不一致的恢复定位符");
            }
            if (skillContentMapper.insertActive(
                    contentRef, hex, result.getUrl(), locator, mediaType, size) != 1) {
                throw new BusinessException(500, 500, "完成 Skill 内容索引写入失败");
            }
            if (writeIntentMapper.deleteIfActiveExists(contentRef, reservationToken) != 1) {
                throw new BusinessException(500, 500, "完成 Skill 内容写入意图提交失败");
            }
            return contentRef;
        } catch (DuplicateKeyException exception) {
            throw new BusinessException(
                    503, 5031, "Skill 内容索引发生并发冲突，请重试", exception);
        }
    }

    /**
     * 为 hash 冲突、释放中或状态不完整的内容索引构造明确异常。
     *
     * @param existing 当前读取得的内容索引，可为 null
     * @param expectedSize 本次内容大小
     * @return 对应索引状态的业务异常
     */
    private BusinessException conflictingContentException(SkillContent existing, long expectedSize) {
        if (existing == null) {
            return new BusinessException(503, 5031, "Skill 内容正在写入或清理，请稍后重试");
        }
        if (existing.getSize() == null || existing.getSize() != expectedSize) {
            return new BusinessException(500, 500, "Skill 内容 hash 冲突或索引大小不一致");
        }
        if (existing.getStorageLocator() == null && existing.getRefCount() != null
                && existing.getRefCount() == 0) {
            return new BusinessException(500, 500,
                    "Skill 旧版内容无法完成物理校验，请核对原存储并执行 storage_locator 迁移");
        }
        return new BusinessException(503, 5031, "Skill 内容正在写入或释放，请稍后重试");
    }

    /**
     * 判断内容是否为允许经过物理校验后恢复的迁移前零引用索引。
     *
     * @param content 当前读锁定的内容索引
     * @param contentRef 预期内容引用
     * @param expectedSize 预期内容大小
     * @return 仅缺少 locator 的完整旧版零引用内容返回 true
     */
    private boolean isReleasedLegacyContent(SkillContent content, String contentRef, long expectedSize) {
        return content != null && content.getRefCount() != null && content.getRefCount() == 0
                && content.getStorageLocator() == null
                && content.getFilePath() != null && !content.getFilePath().isBlank()
                && !content.getFilePath().startsWith(PENDING_PREFIX)
                && content.getSize() != null && content.getSize() == expectedSize
                && contentRef.equals("sha256:" + content.getContentHash());
    }

    /**
     * 从旧版读取路径重新校验对象大小与完整 SHA-256，确认同 hash 内容仍可安全复用。
     *
     * @param content 旧版内容索引
     * @param contentRef 预期内容引用
     * @param expectedSize 预期内容大小
     * @throws BusinessException 对象不可读、大小变化或哈希不一致时抛出
     */
    private void verifyLegacyPhysicalContent(SkillContent content, String contentRef, long expectedSize) {
        try {
            long actualSize = fileStorageService.getFileSize(content.getFilePath());
            if (actualSize != expectedSize) {
                throw new BusinessException(500, 500, "Skill 旧版内容物理大小与索引不一致");
            }
            try (InputStream inputStream = fileStorageService.readStream(content.getFilePath())) {
                if (!contentRef.substring("sha256:".length()).equals(sha256Hex(inputStream))) {
                    throw new BusinessException(500, 500, "Skill 旧版内容物理哈希与索引不一致");
                }
            }
        } catch (BusinessException exception) {
            throw exception;
        } catch (IOException | RuntimeException exception) {
            throw new BusinessException(500, 500,
                    "读取 Skill 旧版内容失败，请核对原存储并执行 storage_locator 迁移", exception);
        }
    }

    /**
     * 在独立事务中丢弃确认不会对应任何新物理对象的 PENDING 意图。
     *
     * @param contentRef 内容引用
     * @param reservationToken 当前预留令牌
     * @param primaryException 即将返回给调用方的主异常
     */
    private void discardPendingIntent(
            String contentRef, String reservationToken, RuntimeException primaryException) {
        try {
            int deleted = executeCleanupTransaction(() ->
                    writeIntentMapper.deletePending(contentRef, reservationToken));
            if (deleted != 1) {
                LOG.warn("未能立即删除无物理写入的 Skill PENDING 意图，等待定时清理，contentRef={}",
                        contentRef);
            }
        } catch (RuntimeException cleanupException) {
            primaryException.addSuppressed(cleanupException);
        }
    }

    /**
     * 在当前事务中原子释放一份引用，并处理 retain/release 并发更新。
     *
     * @param contentRef 内容引用
     */
    private void releaseInTransaction(String contentRef) {
        for (int attempt = 0; attempt < RELEASE_RETRY_LIMIT; attempt++) {
            if (skillContentMapper.releaseShared(contentRef) == 1) {
                return;
            }
            SkillContent content = skillContentMapper.selectForUpdate(contentRef);
            if (!isVisible(content)) {
                return;
            }
            if (skillContentMapper.markReleased(
                    contentRef, content.getFilePath(), content.getStorageLocator()) == 1) {
                content.setRefCount(0);
                if (content.getStorageLocator() == null || content.getStorageLocator().isBlank()) {
                    LOG.warn("Skill 旧版内容缺少稳定定位符，保留零引用索引等待人工迁移，contentRef={}",
                            contentRef);
                } else {
                    scheduleReleasedContentPurge(content);
                }
                return;
            }
        }
        throw new BusinessException(503, 5031, "Skill 内容引用计数并发更新失败，请重试");
    }

    /**
     * 获取当前可读的正式内容索引。
     *
     * @param contentRef 内容引用
     * @return 可读内容索引
     */
    private SkillContent requireContent(String contentRef) {
        if (contentRef == null || contentRef.isBlank()) {
            throw new BusinessException("Skill 内容引用不能为空");
        }
        SkillContent content = skillContentMapper.selectOneById(contentRef);
        if (!isVisible(content)) {
            throw new BusinessException(404, 404, "Skill 二进制内容不存在");
        }
        return content;
    }

    /**
     * 判断索引是否已完成写入且仍有引用。
     *
     * @param content 内容索引
     * @return 可见时为 true
     */
    private boolean isVisible(SkillContent content) {
        return content != null && content.getRefCount() != null && content.getRefCount() > 0
                && content.getFilePath() != null && !content.getFilePath().isBlank()
                && !content.getFilePath().startsWith(PENDING_PREFIX)
                && (content.getStorageLocator() == null || !content.getStorageLocator().isBlank());
    }

    /**
     * 校验 sha256 内容引用格式。
     *
     * @param contentRef 内容引用
     */
    private void validateContentRef(String contentRef) {
        if (contentRef == null || !CONTENT_REF_PATTERN.matcher(contentRef).matches()) {
            throw new BusinessException("Skill 内容引用格式不正确");
        }
    }

    /**
     * 返回内容哈希对应的稳定相对目录。
     *
     * @param contentHash 小写 SHA-256 十六进制值
     * @return 不带结尾斜杠的相对目录
     */
    private String expectedStoragePath(String contentHash) {
        return CONTENT_PATH_PREFIX + contentHash.substring(0, 2);
    }

    /**
     * 返回内容哈希对应的稳定文件名。
     *
     * @param contentHash 小写 SHA-256 十六进制值
     * @return 固定二进制文件名
     */
    private String expectedStorageFilename(String contentHash) {
        return contentHash + ".bin";
    }

    /**
     * 校验恢复句柄只指向当前内容哈希的确定性对象位置。
     *
     * @param handle 待校验句柄
     * @param contentRef 内容引用
     */
    private void validateDeterministicHandle(FileStorageWriteHandle handle, String contentRef) {
        validateContentRef(contentRef);
        if (handle == null) {
            throw new BusinessException(500, 500, "文件存储未返回可恢复写入句柄");
        }
        String contentHash = contentRef.substring("sha256:".length());
        String expectedPath = expectedStoragePath(contentHash) + "/";
        if (!expectedPath.equals(handle.getPath())
                || !expectedStorageFilename(contentHash).equals(handle.getFilename())) {
            throw new BusinessException(500, 500, "文件存储恢复句柄与内容哈希不一致");
        }
    }

    /**
     * 通过 REQUIRED 传播显式执行事务，覆盖类内方法调用场景。
     *
     * @param action 事务动作
     * @param <T> 返回类型
     * @return 动作结果
     */
    private <T> T executeRequired(Supplier<T> action) {
        return requiredTransactionTemplate.execute(status -> action.get());
    }

    /**
     * 在独立事务中执行写入意图预留或清理动作。
     *
     * @param action 独立事务动作
     * @param <T> 返回类型
     * @return 动作结果
     */
    private <T> T executeCleanupTransaction(Supplier<T> action) {
        return cleanupTransactionTemplate.execute(status -> action.get());
    }

    /**
     * 规范化内容媒体类型。
     *
     * @param mediaType 原媒体类型
     * @return 非空媒体类型
     */
    private String normalizeMediaType(String mediaType) {
        if (mediaType == null || mediaType.isBlank()) {
            return DEFAULT_MEDIA_TYPE;
        }
        String normalized = mediaType.trim();
        if (normalized.length() > 128 || normalized.codePoints().anyMatch(Character::isISOControl)) {
            throw new BusinessException("Skill 内容媒体类型不合法或超过长度限制");
        }
        return normalized;
    }

    /**
     * 流式计算 SHA-256 十六进制摘要。
     *
     * @param inputStream 内容流
     * @return 十六进制摘要
     * @throws IOException 读取内容失败
     */
    private String sha256Hex(InputStream inputStream) throws IOException {
        try {
            java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[8192];
            int length;
            while ((length = inputStream.read(buffer)) >= 0) {
                digest.update(buffer, 0, length);
            }
            return java.util.HexFormat.of().formatHex(digest.digest());
        } catch (java.security.NoSuchAlgorithmException exception) {
            throw new IllegalStateException("SHA-256 算法不可用", exception);
        }
    }

    /**
     * 重新校验暂存描述、文件大小与内容摘要。
     *
     * @param stage 暂存描述
     * @param path 受控暂存路径
     */
    private void validateStage(SkillContentStage stage, Path path) {
        validateContentRef(stage.getContentRef());
        if (stage.getSize() < 0
                || !stage.getContentRef().substring("sha256:".length()).equals(stage.getContentHash())) {
            throw new BusinessException(500, 500, "Skill 内容暂存描述不一致");
        }
        try {
            if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.size(path) != stage.getSize()) {
                throw new BusinessException(500, 500, "Skill 暂存内容不存在或大小已变化");
            }
            try (InputStream inputStream = Files.newInputStream(path)) {
                String actualHash = sha256Hex(inputStream);
                if (!stage.getContentHash().equals(actualHash)) {
                    throw new BusinessException(500, 500, "Skill 暂存内容校验失败");
                }
            }
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "读取 Skill 暂存内容失败", exception);
        }
    }

    /**
     * 校验并返回位于系统临时目录下的受控暂存路径。
     *
     * @param stage 暂存描述
     * @return 规范化暂存路径
     */
    private Path requireStagePath(SkillContentStage stage) {
        if (stage == null || stage.getStageId() == null) {
            throw new BusinessException("Skill 内容暂存描述不能为空");
        }
        Path tempRoot = Path.of(System.getProperty("java.io.tmpdir")).toAbsolutePath().normalize();
        Path path = Path.of(stage.getStageId()).toAbsolutePath().normalize();
        if (!path.startsWith(tempRoot) || !path.getFileName().toString().startsWith("easyflow-skill-stage-")) {
            throw new BusinessException(500, 500, "Skill 内容暂存路径不合法");
        }
        return path;
    }

    /**
     * 尽力删除已使用或回滚的暂存文件。
     *
     * @param path 暂存路径
     */
    private void deleteStageQuietly(Path path) {
        if (path == null) {
            return;
        }
        try {
            Files.deleteIfExists(path);
        } catch (IOException ignored) {
            // 临时文件清理由系统临时目录兜底；主异常优先返回。
        }
    }

    /**
     * 定时清理过期占位与待重试删除的零引用内容。
     */
    @Scheduled(
            fixedDelayString = "${easyflow.skill.content-cleanup-delay-ms:300000}",
            initialDelayString = "${easyflow.skill.content-cleanup-delay-ms:300000}"
    )
    @DistributedScheduledLock(key = "easyflow:schedule:skill-content:cleanup", leaseSeconds = 600L)
    public void cleanupStaleContent() {
        cleanupStaleContent(new Date(), cleanupBatchSize);
    }

    /**
     * 按给定基准时间执行一轮内容清理。
     *
     * @param now 清理基准时间
     * @param batchSize 单类记录最大处理数量
     */
    void cleanupStaleContent(Date now, int batchSize) {
        Date safeNow = now == null ? new Date() : now;
        int safeBatchSize = Math.max(1, Math.min(batchSize, 1_000));
        Date pendingCutoff = subtractSafely(safeNow, pendingTtlMs);
        Date releasedCutoff = subtractSafely(safeNow, releaseRetryDelayMs);
        try {
            List<SkillContentWriteIntent> intents = writeIntentMapper.findStale(pendingCutoff, safeBatchSize);
            if (intents != null) {
                for (SkillContentWriteIntent intent : intents) {
                    cleanupWriteIntent(intent, pendingCutoff);
                }
            }
        } catch (RuntimeException exception) {
            LOG.error("扫描过期 Skill 内容写入意图失败", exception);
        }
        try {
            List<SkillContent> pendingContents = skillContentMapper.findStalePending(pendingCutoff, safeBatchSize);
            if (pendingContents != null) {
                for (SkillContent content : pendingContents) {
                    if (content != null && content.getContentRef() != null && content.getFilePath() != null) {
                        skillContentMapper.deleteStalePending(
                                content.getContentRef(), content.getFilePath(), pendingCutoff);
                    }
                }
            }
        } catch (RuntimeException exception) {
            LOG.error("清理过期 Skill 内容占位失败", exception);
        }
        try {
            List<SkillContent> releasedContents =
                    skillContentMapper.findReleasedBefore(releasedCutoff, safeBatchSize);
            if (releasedContents != null) {
                for (SkillContent content : releasedContents) {
                    purgeReleasedContent(content);
                }
            }
        } catch (RuntimeException exception) {
            LOG.error("扫描零引用 Skill 内容失败", exception);
        }
    }

    /**
     * 清理一个已经超过保留期限的写入意图。
     *
     * <p>先在独立事务中确认是否已有正式活动内容；没有活动内容时通过令牌与状态 CAS 取得清理权，
     * 再执行事务外物理 I/O。删除失败时保留 CLEANING 意图，供下一轮幂等重试。</p>
     *
     * @param intent 查询到的写入意图
     * @param cutoff 本轮过期截止时间
     */
    private void cleanupWriteIntent(SkillContentWriteIntent intent, Date cutoff) {
        if (!isValidIntentIdentity(intent) || cutoff == null) {
            return;
        }
        String contentRef = intent.getContentRef();
        String reservationToken = intent.getReservationToken();
        try {
            int activeCleanup = executeCleanupTransaction(() ->
                    writeIntentMapper.deleteIfActiveExists(contentRef, reservationToken));
            if (activeCleanup == 1) {
                return;
            }
            int claimed = executeCleanupTransaction(() -> writeIntentMapper.claimForCleanup(
                    contentRef, reservationToken, intent.getState(), cutoff));
            if (claimed != 1) {
                return;
            }
            FileStorageWriteHandle handle = decodeAndValidateHandle(
                    intent.getStorageLocator(), contentRef, intent.getContentHash());
            fileStorageService.deleteRecoverable(handle);
            if (fileStorageService.existsRecoverable(handle)) {
                throw new IllegalStateException("清理后 Skill 写入意图物理对象仍存在");
            }
            int deleted = executeCleanupTransaction(() ->
                    writeIntentMapper.deleteClaimed(contentRef, reservationToken));
            if (deleted != 1) {
                LOG.warn("Skill 内容写入意图物理对象已清理，但意图状态已变化，contentRef={}", contentRef);
            }
        } catch (RuntimeException exception) {
            LOG.error("清理 Skill 内容写入意图失败，contentRef={}", contentRef, exception);
        }
    }

    /**
     * 校验写入意图包含安全、彼此一致的主键、令牌、哈希与状态。
     *
     * @param intent 写入意图
     * @return 可进入清理状态机时返回 true
     */
    private boolean isValidIntentIdentity(SkillContentWriteIntent intent) {
        if (intent == null || intent.getContentRef() == null || intent.getReservationToken() == null
                || intent.getReservationToken().isBlank() || intent.getContentHash() == null
                || intent.getStorageLocator() == null || intent.getStorageLocator().isBlank()
                || intent.getSize() == null || intent.getSize() < 0
                || !(INTENT_PENDING.equals(intent.getState())
                || INTENT_WRITING.equals(intent.getState())
                || INTENT_CLEANING.equals(intent.getState()))) {
            LOG.error("发现结构不完整的 Skill 内容写入意图，保留记录等待人工核查");
            return false;
        }
        if (!CONTENT_REF_PATTERN.matcher(intent.getContentRef()).matches()
                || !intent.getContentRef().equals("sha256:" + intent.getContentHash())) {
            LOG.error("发现哈希不一致的 Skill 内容写入意图，保留记录等待人工核查");
            return false;
        }
        return true;
    }

    /**
     * 解码恢复定位符，并验证其确定性对象路径与内容哈希一致。
     *
     * @param locator 稳定恢复定位符
     * @param contentRef 内容引用
     * @param contentHash 内容哈希
     * @return 通过校验的恢复句柄
     */
    private FileStorageWriteHandle decodeAndValidateHandle(
            String locator, String contentRef, String contentHash) {
        validateContentRef(contentRef);
        String expectedHash = contentRef.substring("sha256:".length());
        if (contentHash == null || !expectedHash.equals(contentHash)) {
            throw new IllegalStateException("Skill 内容索引哈希与内容引用不一致");
        }
        FileStorageWriteHandle handle = FileStorageWriteHandle.decodeLocator(locator);
        validateDeterministicHandle(handle, contentRef);
        return handle;
    }

    /**
     * 将最后引用的物理清理延迟到当前事务提交之后。
     *
     * @param content 已标记为零引用的内容
     */
    private void scheduleReleasedContentPurge(SkillContent content) {
        Runnable purge = () -> purgeReleasedContent(content);
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    purge.run();
                }
            });
        } else {
            LOG.warn("当前事务未启用同步回调，Skill 零引用内容将由定时任务延迟清理，contentRef={}",
                    content.getContentRef());
        }
    }

    /**
     * 删除零引用物理内容，并在独立事务中条件删除索引。
     *
     * @param content 零引用内容
     */
    private void purgeReleasedContent(SkillContent content) {
        if (content == null || content.getContentRef() == null || content.getFilePath() == null
                || content.getFilePath().startsWith(PENDING_PREFIX)
                || content.getStorageLocator() == null || content.getStorageLocator().isBlank()
                || content.getRefCount() == null || content.getRefCount() != 0) {
            return;
        }
        try {
            FileStorageWriteHandle handle = decodeAndValidateHandle(
                    content.getStorageLocator(), content.getContentRef(), content.getContentHash());
            fileStorageService.deleteRecoverable(handle);
            if (fileStorageService.existsRecoverable(handle)) {
                throw new IllegalStateException("删除后 Skill 内容物理对象仍存在");
            }
        } catch (RuntimeException exception) {
            LOG.error("清理已释放 Skill 内容失败，contentRef={}", content.getContentRef(), exception);
            return;
        }
        try {
            cleanupTransactionTemplate.executeWithoutResult(status ->
                    skillContentMapper.deleteReleased(
                            content.getContentRef(), content.getFilePath(), content.getStorageLocator()));
        } catch (RuntimeException exception) {
            LOG.error("删除已释放 Skill 内容索引失败，contentRef={}", content.getContentRef(), exception);
        }
    }

    /**
     * 避免异常配置导致时间减法溢出。
     *
     * @param base 基准时间
     * @param durationMillis 回溯毫秒数
     * @return 截止时间
     */
    private Date subtractSafely(Date base, long durationMillis) {
        try {
            return new Date(Math.subtractExact(base.getTime(), Math.max(0L, durationMillis)));
        } catch (ArithmeticException exception) {
            return new Date(Long.MIN_VALUE);
        }
    }

    /**
     * 基于字节数组的 MultipartFile 适配器。
     */
    private static final class ByteArrayMultipartFile implements MultipartFile {

        private final byte[] bytes;
        private final String filename;
        private final String contentType;

        private ByteArrayMultipartFile(byte[] bytes, String filename, String contentType) {
            this.bytes = bytes;
            this.filename = filename;
            this.contentType = contentType;
        }

        @Override public String getName() { return "file"; }
        @Override public String getOriginalFilename() { return filename; }
        @Override public String getContentType() { return contentType; }
        @Override public boolean isEmpty() { return bytes.length == 0; }
        @Override public long getSize() { return bytes.length; }
        @Override public byte[] getBytes() { return bytes; }
        @Override public InputStream getInputStream() { return new ByteArrayInputStream(bytes); }
        @Override public void transferTo(File destination) throws IOException { org.springframework.util.FileCopyUtils.copy(bytes, destination); }
    }

    /**
     * 基于临时路径的流式 MultipartFile 适配器。
     */
    private static final class PathMultipartFile implements MultipartFile {

        private final Path path;
        private final String filename;
        private final String contentType;

        private PathMultipartFile(Path path, String filename, String contentType) {
            this.path = path;
            this.filename = filename;
            this.contentType = contentType;
        }

        @Override public String getName() { return "file"; }
        @Override public String getOriginalFilename() { return filename; }
        @Override public String getContentType() { return contentType; }
        @Override public boolean isEmpty() { return getSize() == 0; }
        @Override public long getSize() { try { return Files.size(path); } catch (IOException exception) { throw new BusinessException(500, 500, "读取 Skill 临时内容大小失败", exception); } }
        @Override public byte[] getBytes() throws IOException { return Files.readAllBytes(path); }
        @Override public InputStream getInputStream() throws IOException { return Files.newInputStream(path); }
        @Override public void transferTo(File destination) throws IOException { Files.copy(path, destination.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); }
    }
}
