package tech.easyflow.skill.service.impl;

import com.easyagents.skill.exception.SkillException;
import com.easyagents.skill.factory.SkillFactory;
import com.easyagents.skill.model.SkillDocument;
import com.easyagents.skill.util.SkillFrontmatter;
import com.easyagents.skill.util.SkillHashes;
import com.easyagents.skill.util.SkillPaths;
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
import com.easyagents.skill.validation.SkillValidationMode;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillResource;
import tech.easyflow.skill.mapper.SkillMapper;
import tech.easyflow.skill.service.SkillCategoryService;
import tech.easyflow.skill.service.SkillResourceService;
import tech.easyflow.skill.service.SkillService;
import tech.easyflow.skill.store.DBSkillContentStore;
import tech.easyflow.skill.support.SkillModelConverter;
import tech.easyflow.skill.validation.SkillValidationIssue;
import tech.easyflow.skill.validation.SkillValidationResult;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Skill 业务服务实现。
 */
@Service
public class SkillServiceImpl extends ServiceImpl<SkillMapper, Skill> implements SkillService {

    private final DefaultSkillValidator skillValidator = new DefaultSkillValidator();
    private final SkillCategoryService skillCategoryService;
    private final SkillResourceService skillResourceService;
    private final DBSkillContentStore contentStore;
    private final ResourceAccessService resourceAccessService;
    private final CategoryPermissionService categoryPermissionService;
    private final ObjectMapper objectMapper;

    /**
     * 创建 Skill 业务服务。
     *
     * @param skillCategoryService Skill 分类服务
     * @param skillResourceService 通用资源服务
     * @param contentStore 二进制内容仓库
     * @param resourceAccessService 资源访问服务
     * @param categoryPermissionService 分类权限服务
     * @param objectMapper JSON 映射器
     */
    public SkillServiceImpl(SkillCategoryService skillCategoryService,
                            SkillResourceService skillResourceService,
                            DBSkillContentStore contentStore,
                            ResourceAccessService resourceAccessService,
                            CategoryPermissionService categoryPermissionService,
                            ObjectMapper objectMapper) {
        this.skillCategoryService = skillCategoryService;
        this.skillResourceService = skillResourceService;
        this.contentStore = contentStore;
        this.resourceAccessService = resourceAccessService;
        this.categoryPermissionService = categoryPermissionService;
        this.objectMapper = objectMapper;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Skill getDetail(BigInteger id) {
        Skill skill = requireSkill(id);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
        fillResources(skill);
        return skill;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Skill getManagementDetail(BigInteger id) {
        Skill skill = requireSkill(id);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
        fillResourceDescriptors(skill);
        return skill;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Skill getPackageDetail(BigInteger id) {
        Skill skill = requireSkill(id);
        resourceAccessService.assertAccess(
                CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
        fillResources(skill);
        return skill;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Skill saveDraft(Skill skill) {
        if (skill == null) {
            throw new BusinessException("Skill 不能为空");
        }
        skillCategoryService.lockAndValidateUsableCategory(skill.getCategoryId());
        validateDraft(skill);
        assertUniqueName(skill.getName(), null);
        List<SkillResource> resources = skill.getResources() == null
                ? new ArrayList<>() : new ArrayList<>(skill.getResources());
        applyDraftDefaults(skill);
        normalizeResources(skill, resources);
        skill.setPackageHash(calculatePackageHash(skill.getSkillContent(), resources));
        skill.setResources(resources);
        try {
            if (!save(skill)) {
                throw new BusinessException(500, 500, "保存 Skill 失败，请稍后重试");
            }
        } catch (DuplicateKeyException exception) {
            throw new BusinessException(409, 4092, "当前租户已存在同名 Skill");
        }
        resources.forEach(resource -> resource.setSkillId(skill.getId()));
        replaceResources(skill, resources);
        return getDetail(skill.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Skill updateDraft(Skill skill) {
        return updateDraftInternal(skill, null, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Skill overwriteImportedDraft(Skill skill) {
        return updateDraftInternal(skill, null, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Skill updateDraftIfContentMatches(Skill skill, String expectedSkillContentHash) {
        if (expectedSkillContentHash == null || !expectedSkillContentHash.matches("^[a-f0-9]{64}$")) {
            throw new BusinessException(409, 4091, "缺少或无效的文件版本，请重新加载后再保存");
        }
        return updateDraftInternal(skill, expectedSkillContentHash, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Skill copyDraft(BigInteger sourceId, String name, String displayName, BigInteger categoryId) {
        if (sourceId == null) {
            throw new BusinessException("源 Skill ID 不能为空");
        }
        String normalizedName = name == null ? "" : name.trim();
        if (!isCanonicalName(normalizedName)) {
            throw new BusinessException("新 Skill 名称仅支持小写字母、数字和连字符");
        }

        Skill source = getDetail(sourceId);
        SkillDocument document;
        try {
            document = SkillFrontmatter.parseDocument(source.getSkillContent());
            document.putFrontmatter("name", normalizedName);
        } catch (SkillException exception) {
            throw new BusinessException("复制 Skill 时解析 SKILL.md 失败：" + exception.getMessage());
        }

        List<SkillResource> resources = source.getResources() == null ? List.of()
                : source.getResources().stream().map(this::copyResource).toList();
        // 新草稿对每个二进制资源持有独立引用；外层事务失败时引用计数会随数据库事务回滚。
        resources.stream().map(SkillResource::getContentRef)
                .filter(contentRef -> contentRef != null && !contentRef.isBlank())
                .forEach(contentStore::retain);

        Skill draft = new Skill();
        draft.setCategoryId(categoryId);
        draft.setDisplayName(displayName == null || displayName.isBlank()
                ? normalizedName : displayName.trim());
        draft.setSkillContent(document.render());
        draft.setVisibilityScope(VisibilityScope.PRIVATE.name());
        draft.setResources(resources);
        return saveDraft(draft);
    }

    private Skill updateDraftInternal(Skill skill,
                                      String expectedSkillContentHash,
                                      boolean requireDraftStatus) {
        if (skill == null || skill.getId() == null) {
            throw new BusinessException("Skill ID 不能为空");
        }
        // 与分类删除保持“分类树 -> Skill 行”的统一锁顺序，避免相反顺序形成死锁。
        skillCategoryService.lockAndValidateUsableCategory(skill.getCategoryId());
        Skill existing = requireSkill(skill.getId(), true);
        String originalSkillContent = existing.getSkillContent();
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, existing, ResourceAction.MANAGE, "无权限管理该 Skill");
        if (requireDraftStatus && PublishStatus.from(existing.getPublishStatus()) != PublishStatus.DRAFT) {
            throw new BusinessException(409, 4092, "仅允许覆盖草稿状态的 Skill：" + existing.getName());
        }
        if (skill.getSkillContent() == null) {
            skill.setSkillContent(existing.getSkillContent());
        }
        if (expectedSkillContentHash != null) {
            String actualHash = SkillHashes.sha256Hex((existing.getSkillContent() == null ? "" : existing.getSkillContent())
                    .getBytes(StandardCharsets.UTF_8));
            if (!expectedSkillContentHash.equals(actualHash)) {
                throw new BusinessException(409, 4091, "文件已被其他操作更新，请重新加载后合并内容");
            }
        }
        validateDraft(skill);
        assertUniqueName(skill.getName(), skill.getId());
        List<SkillResource> resources = skill.getResources() != null
                ? new ArrayList<>(skill.getResources())
                : listResources(skill.getId());
        normalizeResources(existing, resources);
        applyDraftUpdate(existing, skill);
        existing.setPackageHash(calculatePackageHash(existing.getSkillContent(), resources));
        existing.setResources(resources);
        try {
            QueryWrapper updateQuery = tenantSkillQuery(existing.getId());
            if (expectedSkillContentHash != null) {
                // BINARY 比较保证文本大小写变化也会使旧版本条件失效。
                updateQuery.and("BINARY skill_content = ?", originalSkillContent);
            }
            if (getMapper().updateByQuery(existing, updateQuery) != 1) {
                if (expectedSkillContentHash != null) {
                    throw new BusinessException(409, 4091, "文件已被其他操作更新，请重新加载后合并内容");
                }
                throw new BusinessException(500, 500, "更新 Skill 失败，请稍后重试");
            }
        } catch (DuplicateKeyException exception) {
            throw new BusinessException(409, 4092, "当前租户已存在同名 Skill");
        }
        if (skill.getResources() != null) {
            replaceResources(existing, resources);
        }
        return getDetail(existing.getId());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SkillValidationResult validateSkill(BigInteger id, boolean publishValidation) {
        Skill detail = getDetail(id);
        if (publishValidation) {
            resourceAccessService.assertAccess(CategoryResourceType.SKILL, detail, ResourceAction.MANAGE,
                    "无权限管理该 Skill");
        }
        List<SkillValidationIssue> issues = new ArrayList<>();
        com.easyagents.skill.validation.SkillValidationReport packageReport =
                skillValidator.validateReport(
                        SkillModelConverter.toAgentSkill(detail), null,
                        publishValidation ? SkillValidationMode.STANDARD
                                : SkillValidationMode.DRAFT_IMPORT);
        for (com.easyagents.skill.validation.SkillValidationIssue source : packageReport.getIssues()) {
            SkillValidationIssue issue = SkillValidationIssue.of(source.getSeverity().name(), source.getCode(),
                    source.getMessage(), source.getPath());
            issue.setLine(source.getLine());
            issue.setColumn(source.getColumn());
            issue.setSuggestion(source.getSuggestion());
            issues.add(issue);
        }
        SkillValidationResult result = new SkillValidationResult();
        result.setIssues(issues);
        result.setValid(issues.stream().noneMatch(issue -> "ERROR".equals(issue.getSeverity())));
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void refreshPackageState(BigInteger id) {
        Skill skill = requireSkill(id);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE,
                "无权限管理该 Skill");
        List<SkillResource> resources = listResources(id);
        skill.setPackageHash(calculatePackageHash(skill.getSkillContent(), resources));
        skill.setModified(new Date());
        skill.setModifiedBy(requireCurrentLoginAccount().getId());
        if (getMapper().updateByQuery(skill, tenantSkillQuery(skill.getId())) != 1) {
            throw new BusinessException(500, 500, "刷新 Skill 包状态失败，请稍后重试");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, Object> buildPublishSnapshot(Skill skill) {
        Skill detail = getDetail(skill.getId());
        SkillValidationResult validation = validateSkill(detail.getId(), true);
        validation.getIssues().stream().filter(issue -> "ERROR".equals(issue.getSeverity())).findFirst()
                .ifPresent(issue -> {
                    throw new BusinessException("Skill 发布校验失败：" + issue.getMessage());
                });
        Map<String, Object> snapshot = new LinkedHashMap<>();
        snapshot.put("schemaVersion", 2);
        snapshot.put("name", detail.getName());
        snapshot.put("description", detail.getDescription());
        snapshot.put("skillContent", detail.getSkillContent());
        snapshot.put("packageHash", detail.getPackageHash());
        snapshot.put("resources", buildResourceSnapshot(detail.getResources()));
        String snapshotHash = hashJson(snapshot);
        snapshot.put("snapshotHash", snapshotHash);
        return snapshot;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, Object> buildGovernanceSnapshot(Skill skill) {
        if (skill == null || skill.getId() == null) {
            throw new BusinessException("Skill 治理快照缺少资源标识");
        }
        Map<String, Object> snapshot = new LinkedHashMap<>();
        snapshot.put("schemaVersion", 2);
        snapshot.put("id", skill.getId());
        snapshot.put("tenantId", skill.getTenantId());
        snapshot.put("deptId", skill.getDeptId());
        snapshot.put("categoryId", skill.getCategoryId());
        snapshot.put("name", skill.getName());
        snapshot.put("displayName", skill.getDisplayName());
        snapshot.put("publishStatus", skill.getPublishStatus());
        snapshot.put("visibilityScope", skill.getVisibilityScope());
        snapshot.put("packageHash", skill.getPackageHash());
        snapshot.put("createdBy", skill.getCreatedBy());
        return snapshot;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void assertSnapshotHash(Map<String, Object> snapshot) {
        if (snapshot == null || snapshot.isEmpty()) {
            throw new BusinessException("Skill 发布快照为空");
        }
        Object declared = snapshot.get("snapshotHash");
        Map<String, Object> content = new LinkedHashMap<>(snapshot);
        content.remove("snapshotHash");
        if (!(declared instanceof String value) || !value.equals(hashJson(content))) {
            throw new BusinessException(409, 4092, "Skill 发布快照校验失败");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void retainSnapshotContents(Map<String, Object> snapshot) {
        for (String contentRef : snapshotContentRefs(snapshot)) {
            contentStore.retain(contentRef);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void releaseSnapshotContents(Map<String, Object> snapshot) {
        for (String contentRef : snapshotContentRefs(snapshot)) {
            contentStore.release(contentRef);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Skill fromSnapshot(Map<String, Object> snapshot) {
        if (snapshot == null || snapshot.isEmpty()) {
            throw new BusinessException("Skill 发布快照为空");
        }
        Skill skill = objectMapper.convertValue(snapshot, Skill.class);
        skill.setPublishStatus(PublishStatus.PUBLISHED.getCode());
        skill.setPublishedSnapshotJson(snapshot);
        return skill;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeAggregate(BigInteger id) {
        removeAggregate(id, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeLifecycleAggregate(BigInteger id) {
        removeAggregate(id, true);
    }

    /**
     * 在锁定 Skill 主行后删除完整聚合并释放内容引用。
     *
     * @param id Skill ID
     * @param lifecycleDelete 是否来自统一发布生命周期
     * @throws BusinessException Skill 不存在、无权限、状态不可删除或聚合删除失败
     */
    private void removeAggregate(BigInteger id, boolean lifecycleDelete) {
        if (id == null) {
            return;
        }
        // 文件和资源更新同样先锁 Skill 行；删除必须持有该锁直到引用计数变更完成。
        Skill skill = requireSkill(id, true);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE, "无权限删除该 Skill");
        assertRemovableStatus(skill, lifecycleDelete);
        List<SkillResource> resources = listResources(id);
        if (!skillResourceService.remove(QueryWrapper.create()
                .eq(SkillResource::getTenantId, skill.getTenantId())
                .eq(SkillResource::getSkillId, id))) {
            if (!resources.isEmpty()) {
                throw new BusinessException(500, 500, "删除 Skill 资源失败，请稍后重试");
            }
        }
        if (getMapper().deleteByQuery(tenantSkillQuery(id)) != 1) {
            throw new BusinessException(500, 500, "删除 Skill 失败，请稍后重试");
        }
        releaseContents(resources);
        releaseSnapshotContents(skill.getPublishedSnapshotJson());
    }

    /**
     * 校验普通仓储删除与生命周期删除各自允许的发布状态。
     *
     * @param skill 已锁定的 Skill
     * @param lifecycleDelete 是否来自统一发布生命周期
     * @throws BusinessException Skill 已发布或处于当前删除入口不允许的审批状态
     */
    private void assertRemovableStatus(Skill skill, boolean lifecycleDelete) {
        PublishStatus status = PublishStatus.from(skill.getPublishStatus());
        if (status == PublishStatus.PUBLISHED) {
            throw new BusinessException(409, 4092, "当前 Skill 已发布，请先下线后再删除");
        }
        if (status == PublishStatus.PUBLISH_PENDING || status == PublishStatus.OFFLINE_PENDING
                || (!lifecycleDelete && status == PublishStatus.DELETE_PENDING)) {
            throw new BusinessException(409, 4092, "当前 Skill 存在进行中的审批，请先处理完成");
        }
    }

    private Skill requireSkill(BigInteger id) {
        return requireSkill(id, false);
    }

    private Skill requireSkill(BigInteger id, boolean forUpdate) {
        if (id == null) {
            throw new BusinessException("Skill ID 不能为空");
        }
        QueryWrapper query = tenantSkillQuery(id);
        if (forUpdate) {
            query.forUpdate();
        }
        Skill skill = getOne(query);
        if (skill == null) {
            throw new BusinessException(404, 404, "Skill 不存在");
        }
        return skill;
    }

    private void validateDraft(Skill skill) {
        if (skill == null) {
            throw new BusinessException("Skill 不能为空");
        }
        normalizeFromSkillContent(skill);
        if (skill.getName() == null || skill.getName().isBlank()) {
            throw new BusinessException("Skill 名称不能为空");
        }
        if (skill.getDescription() == null || skill.getDescription().isBlank()) {
            throw new BusinessException("Skill 描述不能为空");
        }
        if (skill.getDisplayName() != null && skill.getDisplayName().length() > 128) {
            throw new BusinessException("Skill 展示名称不能超过 128 个字符");
        }
        if (skill.getSkillContent() == null || skill.getSkillContent().isBlank()) {
            throw new BusinessException("SKILL.md 内容不能为空");
        }
        if (!isCanonicalName(skill.getName())) {
            throw new BusinessException("Skill 名称仅支持小写字母、数字和连字符");
        }
        validateTargetCategoryVisible(skill.getCategoryId());
        skill.setVisibilityScope(VisibilityScope.fromOrDefault(skill.getVisibilityScope(), VisibilityScope.PRIVATE).name());
        validateSkillPackage(skill);
    }

    private void assertUniqueName(String name, BigInteger excludeId) {
        QueryWrapper query = QueryWrapper.create()
                .eq(Skill::getTenantId, requireCurrentLoginAccount().getTenantId())
                .eq(Skill::getName, name);
        if (excludeId != null) {
            query.ne(Skill::getId, excludeId);
        }
        if (count(query) > 0) {
            throw new BusinessException(409, 4092, "当前租户已存在同名 Skill：" + name);
        }
    }

    private void validateTargetCategoryVisible(BigInteger categoryId) {
        if (categoryId == null) {
            return;
        }
        RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
        if (access.isRestricted() && !access.getCategoryIds().contains(categoryId)) {
            throw new BusinessException("无权限使用该 Skill 分类");
        }
    }

    private void applyDraftDefaults(Skill skill) {
        LoginAccount account = requireCurrentLoginAccount();
        Date now = new Date();
        skill.setTenantId(account.getTenantId());
        skill.setDeptId(account.getDeptId());
        skill.setCreated(now);
        skill.setCreatedBy(account.getId());
        skill.setModified(now);
        skill.setModifiedBy(account.getId());
        skill.setPublishStatus(PublishStatus.DRAFT.getCode());
    }

    private void applyDraftUpdate(Skill existing, Skill incoming) {
        LoginAccount account = requireCurrentLoginAccount();
        existing.setCategoryId(incoming.getCategoryId());
        existing.setName(incoming.getName());
        existing.setDisplayName(incoming.getDisplayName());
        existing.setDescription(incoming.getDescription());
        existing.setSkillContent(incoming.getSkillContent());
        existing.setVisibilityScope(incoming.getVisibilityScope());
        existing.setModified(new Date());
        existing.setModifiedBy(account.getId());
    }

    private void fillResources(Skill skill) {
        List<SkillResource> resources = listResources(skill.getId());
        skill.setResources(resources);
        refreshPackageSummary(skill, resources);
    }

    private void fillResourceDescriptors(Skill skill) {
        List<SkillResource> resources = skillResourceService.listDescriptors(
                skill.getId(), requireCurrentLoginAccount().getTenantId());
        skill.setResources(resources);
        refreshPackageSummary(skill, resources);
    }

    private void refreshPackageSummary(Skill skill, List<SkillResource> resources) {
        String previousHash = skill.getPackageHash();
        skill.setPackageHash(calculatePackageHash(skill.getSkillContent(), resources));
        if (previousHash == null || previousHash.isBlank()) {
            getMapper().backfillPackageHash(skill.getId(), skill.getTenantId(), skill.getPackageHash());
        }
    }

    private List<SkillResource> listResources(BigInteger skillId) {
        return skillResourceService.list(QueryWrapper.create()
                .eq(SkillResource::getTenantId, requireCurrentLoginAccount().getTenantId())
                .eq(SkillResource::getSkillId, skillId)
                .orderBy("normalized_path asc"));
    }

    private void replaceResources(Skill skill, List<SkillResource> resources) {
        List<SkillResource> oldResources = listResources(skill.getId());
        if (!oldResources.isEmpty()) {
            if (!skillResourceService.remove(QueryWrapper.create()
                    .eq(SkillResource::getTenantId, skill.getTenantId())
                    .eq(SkillResource::getSkillId, skill.getId()))) {
                throw new BusinessException(500, 500, "替换 Skill 资源失败，请稍后重试");
            }
        }
        if (!resources.isEmpty()) {
            if (!skillResourceService.saveBatch(resources)) {
                throw new BusinessException(500, 500, "保存 Skill 资源失败，请稍后重试");
            }
        }
        for (SkillResource oldResource : oldResources) {
            if (oldResource.getContentRef() != null) {
                contentStore.release(oldResource.getContentRef());
            }
        }
    }

    /**
     * 复制通用资源配置，数据库归属和审计字段由新草稿保存流程重建。
     *
     * @param source 源资源
     * @return 无持久化标识的资源副本
     */
    private SkillResource copyResource(SkillResource source) {
        SkillResource target = new SkillResource();
        target.setPath(source.getPath());
        target.setNormalizedPath(source.getNormalizedPath());
        target.setMediaType(source.getMediaType());
        target.setIsText(source.getIsText());
        target.setTextContent(source.getTextContent());
        target.setContentRef(source.getContentRef());
        target.setContentHash(source.getContentHash());
        target.setSize(source.getSize());
        return target;
    }

    private void normalizeResources(Skill skill, List<SkillResource> resources) {
        Set<String> paths = new HashSet<>();
        LoginAccount account = requireCurrentLoginAccount();
        Date now = new Date();
        for (int index = 0; index < resources.size(); index++) {
            SkillResource resource = resources.get(index);
            if (resource == null) {
                throw new BusinessException("Skill 资源不能为空");
            }
            String normalizedPath;
            try {
                normalizedPath = SkillPaths.normalize(resource.getPath() == null
                        ? resource.getNormalizedPath() : resource.getPath());
            } catch (SkillException exception) {
                throw new BusinessException("Skill 资源路径不合法：" + exception.getMessage());
            }
            if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
                throw new BusinessException("SKILL.md 必须保存在 Skill 主表中");
            }
            if (normalizedPath.split("/").length > 16) {
                throw new BusinessException("Skill 资源路径层级不能超过 16 层：" + normalizedPath);
            }
            if (!paths.add(collisionKey(normalizedPath))) {
                throw new BusinessException("Skill 资源路径重复：" + normalizedPath);
            }
            resource.setId(null);
            resource.setTenantId(skill.getTenantId());
            resource.setSkillId(skill.getId());
            resource.setPath(normalizedPath);
            resource.setNormalizedPath(normalizedPath);
            resource.setCreated(now);
            resource.setCreatedBy(account.getId());
            resource.setModified(now);
            resource.setModifiedBy(account.getId());
            if (Boolean.TRUE.equals(resource.getIsText())) {
                byte[] bytes = (resource.getTextContent() == null ? "" : resource.getTextContent())
                        .getBytes(StandardCharsets.UTF_8);
                resource.setContentRef(null);
                resource.setContentHash(SkillHashes.sha256Hex(bytes));
                resource.setSize((long) bytes.length);
            } else {
                if (resource.getContentRef() == null || !contentStore.exists(resource.getContentRef())) {
                    throw new BusinessException("Skill 二进制资源内容不存在：" + normalizedPath);
                }
                String expectedHash = resource.getContentRef().startsWith("sha256:")
                        ? resource.getContentRef().substring("sha256:".length()) : null;
                if (expectedHash == null || !expectedHash.equals(resource.getContentHash())) {
                    throw new BusinessException("Skill 二进制资源 hash 不一致：" + normalizedPath);
                }
                resource.setTextContent(null);
            }
        }
    }

    private void releaseContents(List<SkillResource> resources) {
        for (SkillResource resource : resources) {
            if (resource.getContentRef() != null) {
                contentStore.release(resource.getContentRef());
            }
        }
    }

    private void normalizeFromSkillContent(Skill skill) {
        try {
            com.easyagents.skill.model.Skill parsed = SkillFactory.createWithResources(
                    skill.getSkillContent(),
                    SkillModelConverter.toAgentResources(skill.getResources())
            );
            skill.setName(parsed.getName());
            if (skill.getDisplayName() == null || skill.getDisplayName().isBlank()) {
                skill.setDisplayName(parsed.getName());
            }
            skill.setDescription(parsed.getDescription());
        } catch (SkillException exception) {
            throw new BusinessException("SKILL.md frontmatter 不合法：" + exception.getMessage());
        }
    }

    private void validateSkillPackage(Skill skill) {
        try {
            skillValidator.validate(SkillModelConverter.toAgentSkill(skill));
        } catch (SkillException exception) {
            throw new BusinessException("Skill 包校验失败：" + exception.getMessage());
        }
    }

    private String calculatePackageHash(String skillContent, List<SkillResource> resources) {
        StringBuilder canonical = new StringBuilder();
        canonical.append(SkillPaths.SKILL_FILE).append('\n')
                .append(SkillHashes.sha256Hex((skillContent == null ? "" : skillContent)
                        .getBytes(StandardCharsets.UTF_8))).append('\n');
        resources.stream().sorted(Comparator.comparing(SkillResource::getNormalizedPath))
                .forEach(resource -> canonical.append(resource.getNormalizedPath()).append('\n')
                        .append(resource.getContentHash()).append('\n'));
        return SkillHashes.sha256Hex(canonical.toString().getBytes(StandardCharsets.UTF_8));
    }

    private List<Map<String, Object>> buildResourceSnapshot(List<SkillResource> resources) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (SkillResource resource : resources) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("path", resource.getNormalizedPath());
            item.put("mediaType", resource.getMediaType());
            item.put("text", resource.getIsText());
            item.put("textContent", resource.getTextContent());
            item.put("contentRef", resource.getContentRef());
            item.put("contentHash", resource.getContentHash());
            item.put("size", resource.getSize());
            result.add(item);
        }
        return result;
    }

    private String hashJson(Object value) {
        try {
            return SkillHashes.sha256Hex(objectMapper.writeValueAsBytes(canonicalizeJson(value)));
        } catch (JsonProcessingException exception) {
            throw new BusinessException(500, 500, "计算 Skill 发布快照 hash 失败", exception);
        }
    }

    private Object canonicalizeJson(Object value) {
        if (value instanceof Map<?, ?> map) {
            Map<String, Object> sorted = new TreeMap<>();
            map.forEach((key, item) -> sorted.put(String.valueOf(key), canonicalizeJson(item)));
            return sorted;
        }
        if (value instanceof List<?> list) {
            return list.stream().map(this::canonicalizeJson).toList();
        }
        return value;
    }

    /**
     * 读取新旧发布快照中的二进制内容引用。
     *
     * @param snapshot 发布或审批快照
     * @return 按资源出现次数保留的内容引用
     */
    private List<String> snapshotContentRefs(Map<String, Object> snapshot) {
        if (snapshot == null) {
            return List.of();
        }
        Object resources = snapshot.get("resources");
        if (resources instanceof List<?> resourceList) {
            return collectSnapshotContentRefs(resourceList);
        }
        // V24 published snapshots stored binary resources in assets[]. Keep this fallback until
        // every legacy snapshot has naturally been replaced or removed through the lifecycle.
        Object assets = snapshot.get("assets");
        return assets instanceof List<?> assetList ? collectSnapshotContentRefs(assetList) : List.of();
    }

    /**
     * 从资源数组中收集非空内容引用。
     *
     * @param resources 快照资源数组
     * @return 内容引用列表
     */
    private List<String> collectSnapshotContentRefs(List<?> resources) {
        List<String> refs = new ArrayList<>();
        for (Object item : resources) {
            if (item instanceof Map<?, ?> resource) {
                Object contentRef = resource.get("contentRef");
                if (contentRef instanceof String value && !value.isBlank()) {
                    refs.add(value);
                }
            }
        }
        return refs;
    }

    private boolean isCanonicalName(String name) {
        return name != null && name.matches("[a-z0-9]+(?:-[a-z0-9]+)*");
    }

    private String collisionKey(String path) {
        return Normalizer.normalize(path, Normalizer.Form.NFKC).toLowerCase(Locale.ROOT);
    }

    private LoginAccount requireCurrentLoginAccount() {
        LoginAccount account = SaTokenUtil.getLoginAccount();
        if (account == null || account.getId() == null || account.getTenantId() == null) {
            throw new BusinessException(401, 401, "未登录或登录态无效");
        }
        return account;
    }

    private QueryWrapper tenantSkillQuery(BigInteger skillId) {
        return QueryWrapper.create()
                .eq(Skill::getId, skillId)
                .eq(Skill::getTenantId, requireCurrentLoginAccount().getTenantId());
    }

}
