package com.easyagents.skill.codec;

import com.easyagents.skill.exception.SkillPackageException;
import com.easyagents.skill.exception.SkillValidationException;
import com.easyagents.skill.model.Skill;
import com.easyagents.skill.model.SkillDocument;
import com.easyagents.skill.model.SkillPackage;
import com.easyagents.skill.model.SkillPackageLayout;
import com.easyagents.skill.model.SkillPackageLimits;
import com.easyagents.skill.model.SkillResource;
import com.easyagents.skill.model.SkillResourceKind;
import com.easyagents.skill.store.SkillContentStage;
import com.easyagents.skill.store.SkillContentStore;
import com.easyagents.skill.store.file.TemporaryFileSkillContentStore;
import com.easyagents.skill.util.SkillFrontmatter;
import com.easyagents.skill.util.SkillHashes;
import com.easyagents.skill.util.SkillPaths;
import com.easyagents.skill.util.SkillResources;
import com.easyagents.skill.util.SkillUtf8;
import com.easyagents.skill.validation.SkillValidationIssue;
import com.easyagents.skill.validation.SkillValidationMode;
import com.easyagents.skill.validation.SkillValidationReport;
import com.easyagents.skill.validation.SkillValidationSeverity;
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.SeekableByteChannel;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
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.zip.ZipEntry;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 标准 Skill ZIP 的安全双向流式 Codec。
 */
public class ZipSkillPackageCodec implements AutoCloseable {

    private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
    private static final int CENTRAL_DIRECTORY_HEADER_SIZE = 46;
    private static final int CENTRAL_DIRECTORY_SIGNATURE = 0x02014B50;
    private static final int END_OF_CENTRAL_DIRECTORY_MAX_SIZE = 65_557;
    private static final int END_OF_CENTRAL_DIRECTORY_MIN_SIZE = 22;
    private static final int END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054B50;
    private static final long STABLE_ZIP_TIMESTAMP = 0L;
    private static final int ZIP64_END_OF_CENTRAL_DIRECTORY_MIN_SIZE = 56;
    private static final int ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06064B50;
    private static final int ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE = 20;
    private static final int ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE = 0x07064B50;
    private static final long UINT32_MAX = 0xFFFF_FFFFL;
    private static final int UINT16_MAX = 0xFFFF;

    private final SkillContentStore contentStore;
    private final boolean ownsContentStore;

    /**
     * 创建使用临时文件内容存储的 ZIP Codec。
     *
     * <p>该实例拥有默认内容存储，使用完毕后应调用 {@link #close()} 清理临时内容。</p>
     */
    public ZipSkillPackageCodec() {
        this(new TemporaryFileSkillContentStore(), true);
    }

    /**
     * 创建 ZIP Codec。
     *
     * @param contentStore 二进制内容存储
     */
    public ZipSkillPackageCodec(SkillContentStore contentStore) {
        this(contentStore, false);
    }

    private ZipSkillPackageCodec(SkillContentStore contentStore, boolean ownsContentStore) {
        if (contentStore == null) {
            throw new SkillPackageException("Skill content store is required.");
        }
        this.contentStore = contentStore;
        this.ownsContentStore = ownsContentStore;
    }

    /**
     * 关闭 Codec 自有的默认临时内容存储。
     *
     * <p>通过构造函数注入的内容存储由调用方管理，本方法不会关闭它。</p>
     */
    @Override
    public void close() {
        if (ownsContentStore) {
            ((TemporaryFileSkillContentStore) contentStore).close();
        }
    }

    /**
     * 安全解码 Skill ZIP。
     *
     * @param inputStream ZIP 输入流
     * @param options 读取选项
     * @return 解码结果
     */
    public SkillPackageReadResult decode(InputStream inputStream, SkillPackageReadOptions options) {
        if (inputStream == null) {
            throw packageError("ZIP_INPUT_REQUIRED", null, "ZIP input stream is required.");
        }
        SkillPackageReadOptions effectiveOptions = options == null
                ? SkillPackageReadOptions.defaults() : options;
        SkillPackageLimits limits = effectiveOptions.getLimits();
        Path temporaryZip = null;
        StageTracker stages = new StageTracker(contentStore);
        try {
            temporaryZip = Files.createTempFile("easy-agents-skill-", ".zip");
            InputCopyResult copyResult = copyInput(inputStream, temporaryZip,
                    limits.getMaxCompressedPackageBytes());
            validateZipSignature(temporaryZip);
            DecodeState state = readArchive(temporaryZip, limits, stages);
            SkillValidationReport report = new SkillValidationReport();
            for (Skill skill : state.skills) {
                String packageRoot = state.layout == SkillPackageLayout.ROOT_SKILL
                        ? null : skill.getPackageRoot();
                report.merge(prefixValidationPaths(
                        validateForCodec(skill, limits, SkillValidationMode.DRAFT_IMPORT),
                        packageRoot));
            }
            if (effectiveOptions.getMode() == SkillPackageReadMode.REPORT_ONLY) {
                stages.rollbackAll();
                return new SkillPackageReadResult(
                        new SkillPackage(state.layout, state.skills), report, copyResult.hash);
            }
            if (report.hasErrors()) {
                throw new SkillPackageException("Skill package validation failed.", report);
            }
            stages.commitAll();
            return new SkillPackageReadResult(
                    new SkillPackage(state.layout, state.skills), report, copyResult.hash);
        } catch (SkillPackageException e) {
            stages.cleanupAfterFailure(e);
            throw e;
        } catch (SkillValidationException e) {
            stages.cleanupAfterFailure(e);
            throw validationPackageError(e);
        } catch (IOException e) {
            stages.cleanupAfterFailure(e);
            throw new SkillPackageException("ZIP_IO_ERROR", null,
                    "Failed to read Skill ZIP package.", e);
        } catch (RuntimeException e) {
            stages.cleanupAfterFailure(e);
            throw new SkillPackageException("SKILL_CONTENT_STORE_ERROR", null,
                    "Failed to persist Skill package content.", e);
        } finally {
            deleteQuietly(temporaryZip);
        }
    }

    /**
     * 将 Skill 包稳定编码为标准 ZIP。
     *
     * @param skillPackage Skill 包
     * @param outputStream 输出流，不由本方法关闭
     * @param options 写出选项
     * @return 编码结果
     */
    public SkillPackageWriteResult encode(SkillPackage skillPackage, OutputStream outputStream,
                                          SkillPackageWriteOptions options) {
        if (skillPackage == null || skillPackage.getSkills() == null || skillPackage.getSkills().isEmpty()) {
            throw packageError("EMPTY_SKILL_PACKAGE", null, "Skill package must contain at least one Skill.");
        }
        if (outputStream == null) {
            throw packageError("ZIP_OUTPUT_REQUIRED", null, "ZIP output stream is required.");
        }
        SkillPackageWriteOptions effectiveOptions = options == null
                ? SkillPackageWriteOptions.defaults() : options;
        SkillPackageLimits limits = effectiveOptions.getLimits();
        List<OutputFile> files = prepareOutput(skillPackage.getSkills(), limits);
        Path temporaryZip = null;
        try {
            temporaryZip = Files.createTempFile("easy-agents-skill-export-", ".zip");
            OutputWriteResult writeResult = writeArchive(temporaryZip, files, limits);
            try (InputStream input = Files.newInputStream(temporaryZip)) {
                transfer(input, outputStream, limits.getMaxCompressedPackageBytes(), null);
            }
            outputStream.flush();
            SkillPackageLayout outputLayout = skillPackage.getSkills().size() > 1
                    ? SkillPackageLayout.MULTI_DIRECTORY : SkillPackageLayout.SINGLE_DIRECTORY;
            return new SkillPackageWriteResult(
                    writeResult.hash, writeResult.size, files.size(), outputLayout);
        } catch (SkillPackageException e) {
            throw e;
        } catch (IOException e) {
            if (hasCause(e, CompressedSizeLimitException.class)) {
                throw packageError("COMPRESSED_SIZE_LIMIT", null,
                        "Compressed Skill ZIP exceeds " + limits.getMaxCompressedPackageBytes() + " bytes.");
            }
            throw new SkillPackageException("ZIP_IO_ERROR", null,
                    "Failed to write Skill ZIP package.", e);
        } finally {
            deleteQuietly(temporaryZip);
        }
    }

    private DecodeState readArchive(Path archivePath, SkillPackageLimits limits, StageTracker stages)
            throws IOException {
        enforceCentralDirectoryEntryLimit(archivePath, limits.getMaxEntryCount());
        List<ArchiveFile> files = new ArrayList<>();
        Set<String> exactPaths = new HashSet<>();
        Map<String, String> collisionPaths = new HashMap<>();
        Map<String, String> descendantPaths = new HashMap<>();
        long declaredTotal = 0;
        int entryCount = 0;

        try (ZipFile zipFile = new ZipFile(archivePath)) {
            Enumeration<ZipArchiveEntry> entries = zipFile.getEntriesInPhysicalOrder();
            while (entries.hasMoreElements()) {
                ZipArchiveEntry entry = entries.nextElement();
                String rawPath = strictUtf8EntryName(entry);
                entryCount++;
                if (entryCount > limits.getMaxEntryCount()) {
                    throw packageError("ENTRY_COUNT_LIMIT", rawPath,
                            "Skill ZIP exceeds " + limits.getMaxEntryCount() + " entries.");
                }
                if (entry.isUnixSymlink()) {
                    throw packageError("SYMLINK_ENTRY", rawPath,
                            "Symbolic link entries are not allowed in Skill packages.");
                }
                if (!zipFile.canReadEntryData(entry)) {
                    throw packageError("UNSUPPORTED_ZIP_ENTRY", rawPath,
                            "Encrypted or unsupported ZIP entries are not allowed.");
                }
                boolean ignoredSystemPath = SkillPaths.isIgnoredSystemPath(rawPath);
                String pathForValidation = entry.isDirectory() ? stripDirectorySuffix(rawPath) : rawPath;
                String path = normalizeArchivePath(pathForValidation, limits, ignoredSystemPath);
                if (entry.isDirectory() || ignoredSystemPath) {
                    continue;
                }
                if (!exactPaths.add(path)) {
                    throw packageError("DUPLICATE_ENTRY", path, "Duplicate ZIP entry: " + path);
                }
                String collisionKey = SkillPaths.collisionKey(path);
                String conflictingPath = collisionPaths.get(collisionKey);
                if (conflictingPath != null) {
                    throw packageError("PATH_COLLISION", path,
                            "ZIP entry conflicts with " + conflictingPath + " after Unicode/case normalization.");
                }
                String hierarchyConflict = findHierarchyConflict(
                        collisionKey, collisionPaths, descendantPaths);
                if (hierarchyConflict != null) {
                    throw packageError("PATH_HIERARCHY_CONFLICT", path,
                            "ZIP entry conflicts with file " + hierarchyConflict
                                    + " in the same path hierarchy.");
                }
                collisionPaths.put(collisionKey, path);
                indexDescendantPath(collisionKey, path, descendantPaths);

                long size = entry.getSize();
                long compressedSize = entry.getCompressedSize();
                if (size < 0 || compressedSize < 0) {
                    throw packageError("UNKNOWN_ENTRY_SIZE", path,
                            "ZIP entry sizes must be present in the central directory.");
                }
                if (entry.getCrc() < 0) {
                    throw packageError("UNKNOWN_ENTRY_CRC", path,
                            "ZIP entry CRC must be present in the central directory.");
                }
                checkCompressionRatio(path, size, compressedSize, limits);
                try {
                    declaredTotal = Math.addExact(declaredTotal, size);
                } catch (ArithmeticException e) {
                    throw packageError("TOTAL_SIZE_LIMIT", path, "Skill ZIP uncompressed size overflow.");
                }
                if (declaredTotal > limits.getMaxTotalUncompressedBytes()) {
                    throw packageError("TOTAL_SIZE_LIMIT", path,
                            "Skill ZIP exceeds " + limits.getMaxTotalUncompressedBytes()
                                    + " uncompressed bytes.");
                }
                files.add(new ArchiveFile(entry, path));
            }
            if (files.isEmpty()) {
                throw packageError("EMPTY_SKILL_PACKAGE", null,
                        "ZIP package must contain at least one Skill.");
            }

            ArchiveLayout archiveLayout = determineLayout(files);
            List<Skill> skills = new ArrayList<>();
            for (ArchiveGroup group : archiveLayout.groups) {
                skills.add(readSkill(zipFile, group, archiveLayout.layout, limits, stages));
            }
            return new DecodeState(archiveLayout.layout, skills);
        } catch (SkillPackageException e) {
            throw e;
        } catch (IOException e) {
            throw new SkillPackageException("INVALID_ZIP", null,
                    "Skill package ZIP structure or entry data is invalid.", e);
        }
    }

    private Skill readSkill(ZipFile zipFile, ArchiveGroup group, SkillPackageLayout layout,
                            SkillPackageLimits limits, StageTracker stages) throws IOException {
        ArchiveFile skillFile = group.files.stream()
                .filter(file -> SkillPaths.SKILL_FILE.equals(file.relativePath))
                .findFirst()
                .orElseThrow(() -> packageError("SKILL_FILE_REQUIRED", group.archiveRoot,
                        "Skill directory must contain exactly one SKILL.md."));

        String skillContent = readStrictText(zipFile, skillFile, limits.getMaxTextFileBytes());
        SkillDocument document;
        try {
            document = SkillFrontmatter.parseDocument(skillContent, limits);
        } catch (SkillValidationException e) {
            throw validationPackageError(e,
                    layout == SkillPackageLayout.ROOT_SKILL ? null : group.packageRoot);
        }
        String name = scalar(document.getFrontmatter().get("name"));

        List<SkillResource> resources = new ArrayList<>();
        for (ArchiveFile file : group.files) {
            if (SkillPaths.SKILL_FILE.equals(file.relativePath)) {
                continue;
            }
            SkillResource resource = readResource(zipFile, file, limits, stages);
            resources.add(resource);
        }
        resources.sort(Comparator.comparing(SkillResource::getPath));

        Skill skill = new Skill();
        skill.setPackageRoot(layout == SkillPackageLayout.ROOT_SKILL ? name : group.packageRoot);
        skill.setDocument(document);
        skill.setResources(resources);
        return skill;
    }

    private SkillResource readResource(ZipFile zipFile, ArchiveFile file, SkillPackageLimits limits,
                                       StageTracker stages) throws IOException {
        SkillResourceKind kind = SkillResources.classify(file.relativePath);
        String mediaType = detectMediaType(file.relativePath);
        boolean text = SkillResources.isText(file.relativePath, mediaType);
        long singleLimit = text ? limits.getMaxTextFileBytes() : limits.getMaxBinaryFileBytes();
        if (file.entry.getSize() > singleLimit) {
            throw packageError(text ? "TEXT_FILE_SIZE_LIMIT" : "BINARY_FILE_SIZE_LIMIT", file.fullPath,
                    "Skill resource exceeds " + singleLimit + " bytes.");
        }

        SkillResource resource = new SkillResource();
        resource.setPath(file.relativePath);
        resource.setKind(kind);
        resource.setMediaType(mediaType);
        if (text) {
            String content = readStrictText(zipFile, file, singleLimit);
            byte[] bytes = strictUtf8Bytes(content, file.fullPath);
            resource.setTextContent(content);
            resource.setContentHash(SkillHashes.sha256Hex(bytes));
            resource.setSize(bytes.length);
        } else {
            CRC32 crc = new CRC32();
            try (InputStream input = zipFile.getInputStream(file.entry);
                 CheckedInputStream checkedInput = new CheckedInputStream(input, crc)) {
                SkillContentStage stage = contentStore.stage(checkedInput, singleLimit);
                if (stage.getSize() != file.entry.getSize()) {
                    contentStore.rollback(stage);
                    throw packageError("ENTRY_SIZE_MISMATCH", file.fullPath,
                            "ZIP entry size changed while reading.");
                }
                try {
                    verifyCrc(file.entry, crc.getValue(), file.fullPath);
                } catch (RuntimeException e) {
                    contentStore.rollback(stage);
                    throw e;
                }
                resource.setContentRef(stage.getContentRef());
                resource.setContentHash(stage.getContentHash());
                resource.setSize(stage.getSize());
                stages.add(stage, resource, file.fullPath);
            }
        }
        return resource;
    }

    private static ArchiveLayout determineLayout(List<ArchiveFile> files) {
        boolean hasRootSkill = files.stream().anyMatch(file -> SkillPaths.SKILL_FILE.equals(file.fullPath));
        if (hasRootSkill) {
            boolean containsNestedSkill = files.stream().anyMatch(file ->
                    !SkillPaths.SKILL_FILE.equals(file.fullPath)
                            && file.fullPath.endsWith("/" + SkillPaths.SKILL_FILE));
            if (containsNestedSkill) {
                throw packageError("MIXED_PACKAGE_LAYOUT", null,
                        "Root SKILL.md cannot be mixed with wrapped Skill directories.");
            }
            List<ArchiveFile> relativeFiles = files.stream()
                    .map(file -> file.withRelativePath(file.fullPath))
                    .toList();
            return new ArchiveLayout(SkillPackageLayout.ROOT_SKILL,
                    List.of(new ArchiveGroup(null, null, relativeFiles)));
        }

        List<String> skillRoots = files.stream()
                .map(file -> file.fullPath)
                .filter(path -> path.endsWith("/" + SkillPaths.SKILL_FILE))
                .map(path -> path.substring(0, path.length() - SkillPaths.SKILL_FILE.length() - 1))
                .distinct()
                .sorted()
                .toList();
        if (skillRoots.isEmpty()) {
            throw packageError("SKILL_FILE_REQUIRED", null,
                    "Wrapped Skill ZIP must contain at least one Skill directory.");
        }

        String commonParent = parentPath(skillRoots.get(0));
        boolean supportedParent = commonParent.isEmpty() || commonParent.indexOf('/') < 0;
        boolean sameParent = skillRoots.stream().allMatch(root -> parentPath(root).equals(commonParent));
        if (!supportedParent || !sameParent) {
            throw packageError("MIXED_PACKAGE_LAYOUT", null,
                    "Skill directories must be direct ZIP roots or share one optional parent directory.");
        }

        Map<String, List<ArchiveFile>> grouped = new LinkedHashMap<>();
        for (String root : skillRoots) {
            grouped.put(root, new ArrayList<>());
        }
        for (ArchiveFile file : files) {
            String owner = skillRoots.stream()
                    .filter(root -> file.fullPath.startsWith(root + "/"))
                    .findFirst()
                    .orElse(null);
            if (owner == null) {
                throw packageError("UNOWNED_ROOT_FILE", file.fullPath,
                        "Wrapped Skill ZIP can contain files only inside Skill directories.");
            }
            String relativePath = file.fullPath.substring(owner.length() + 1);
            grouped.get(owner)
                    .add(file.withRelativePath(relativePath));
        }
        List<ArchiveGroup> groups = new ArrayList<>();
        for (Map.Entry<String, List<ArchiveFile>> entry : grouped.entrySet()) {
            long skillFileCount = entry.getValue().stream()
                    .filter(file -> SkillPaths.SKILL_FILE.equals(file.relativePath))
                    .count();
            if (skillFileCount != 1) {
                throw packageError("SKILL_FILE_REQUIRED", entry.getKey(),
                        "Each Skill directory must contain exactly one SKILL.md.");
            }
            groups.add(new ArchiveGroup(entry.getKey(), fileName(entry.getKey()), entry.getValue()));
        }
        groups.sort(Comparator.comparing(group -> group.archiveRoot));
        SkillPackageLayout layout = groups.size() == 1
                ? SkillPackageLayout.SINGLE_DIRECTORY : SkillPackageLayout.MULTI_DIRECTORY;
        return new ArchiveLayout(layout, groups);
    }

    /**
     * 获取归一化归档路径的父目录。
     *
     * @param path 归一化归档路径
     * @return 父目录，顶层路径返回空字符串
     */
    private static String parentPath(String path) {
        int separator = path.lastIndexOf('/');
        return separator < 0 ? "" : path.substring(0, separator);
    }

    /**
     * 获取归一化归档路径的末级目录名。
     *
     * @param path 归一化归档路径
     * @return 末级目录名
     */
    private static String fileName(String path) {
        int separator = path.lastIndexOf('/');
        return separator < 0 ? path : path.substring(separator + 1);
    }

    /**
     * 执行 Codec 不可绕过的标准安全校验。
     *
     * @param skill Skill 聚合
     * @param limits 当前读写操作限额
     * @param mode 标准校验模式
     * @return 结构化报告
     */
    private SkillValidationReport validateForCodec(Skill skill, SkillPackageLimits limits,
                                                   SkillValidationMode mode) {
        return new DefaultSkillValidator(limits).validateReport(skill, limits, mode);
    }

    private List<OutputFile> prepareOutput(List<Skill> skills, SkillPackageLimits limits) {
        SkillValidationReport report = new SkillValidationReport();
        Set<String> roots = new HashSet<>();
        List<OutputFile> files = new ArrayList<>();
        long totalSize = 0;

        for (Skill skill : skills) {
            SkillValidationReport skillReport = validateForCodec(
                    skill, limits, SkillValidationMode.STANDARD);
            String diagnosticRoot = null;
            if (skills.size() > 1 && skill != null) {
                diagnosticRoot = skill.getPackageRoot();
                if (diagnosticRoot == null || diagnosticRoot.isBlank()) {
                    diagnosticRoot = skill.getName();
                }
            }
            report.merge(prefixValidationPaths(skillReport, diagnosticRoot));
            if (skill == null || skillReport.hasErrors()) {
                continue;
            }
            if (skill.getName() == null
                    || !skill.getName().matches("[a-z0-9]+(?:-[a-z0-9]+)*")) {
                report.add(new SkillValidationIssue("NON_CANONICAL_EXPORT_NAME",
                        SkillValidationSeverity.ERROR, SkillPaths.SKILL_FILE, null, null,
                        "Standard export requires a canonical hyphenated Skill name.",
                        "Rename the Skill before export."));
                continue;
            }
            String collisionRoot = skill.getName().toLowerCase(Locale.ROOT);
            if (!roots.add(collisionRoot)) {
                report.add(new SkillValidationIssue("DUPLICATE_SKILL_NAME", SkillValidationSeverity.ERROR,
                        SkillPaths.SKILL_FILE, null, null,
                        "Skill package contains duplicate names: " + skill.getName(), null));
                continue;
            }

            String skillContent = skill.getSkillContent();
            String skillPath = skill.getName() + "/" + SkillPaths.SKILL_FILE;
            validateOutputPath(skillPath, limits);
            long skillSize = utf8Size(skillContent, skillPath);
            if (skillSize > limits.getMaxTextFileBytes()) {
                report.add(new SkillValidationIssue("TEXT_FILE_SIZE_LIMIT", SkillValidationSeverity.ERROR,
                        skillPath, null, null,
                        "SKILL.md exceeds the text file size limit.", null));
            }
            files.add(OutputFile.text(skillPath, skillContent));
            totalSize = checkedOutputTotal(totalSize, skillSize, limits, skillPath);

            List<SkillResource> resources = new ArrayList<>(skill.getResources());
            resources.sort(Comparator.comparing(SkillResource::getPath));
            for (SkillResource resource : resources) {
                String path = skill.getName() + "/" + SkillPaths.normalize(resource.getPath());
                validateOutputPath(path, limits);
                if (resource.isText()) {
                    String textContent = resource.getTextContent();
                    long textSize = utf8Size(textContent, path);
                    if (textSize > limits.getMaxTextFileBytes()) {
                        report.add(new SkillValidationIssue("TEXT_FILE_SIZE_LIMIT",
                                SkillValidationSeverity.ERROR, path, null, null,
                                "Text Skill resource exceeds the text file size limit.", null));
                        continue;
                    }
                    files.add(OutputFile.text(path, textContent));
                    totalSize = checkedOutputTotal(totalSize, textSize, limits, path);
                } else {
                    if (resource.getSize() > limits.getMaxBinaryFileBytes()) {
                        report.add(new SkillValidationIssue("BINARY_FILE_SIZE_LIMIT",
                                SkillValidationSeverity.ERROR, path, null, null,
                                "Binary Skill resource exceeds the binary file size limit.", null));
                        continue;
                    }
                    if (!contentExists(resource.getContentRef(), path)) {
                        report.add(new SkillValidationIssue("CONTENT_NOT_FOUND", SkillValidationSeverity.ERROR,
                                path, null, null, "Skill resource content does not exist.", null));
                        continue;
                    }
                    files.add(OutputFile.binary(path, resource));
                    totalSize = checkedOutputTotal(totalSize, resource.getSize(), limits, path);
                }
            }
        }
        if (files.size() > limits.getMaxEntryCount()) {
            report.add(new SkillValidationIssue("ENTRY_COUNT_LIMIT", SkillValidationSeverity.ERROR,
                    null, null, null, "Skill export exceeds the entry count limit.", null));
        }
        if (report.hasErrors()) {
            throw new SkillPackageException("Skill package cannot be exported.", report);
        }

        files.sort(Comparator.comparing(file -> file.path));
        Set<String> paths = new HashSet<>();
        Map<String, String> collisionPaths = new HashMap<>();
        Map<String, String> descendantPaths = new HashMap<>();
        for (OutputFile file : files) {
            String collisionKey = SkillPaths.collisionKey(file.path);
            String collision = collisionPaths.get(collisionKey);
            if (!paths.add(file.path) || collision != null) {
                throw packageError("PATH_COLLISION", file.path,
                        "Skill export contains duplicate or case-conflicting paths.");
            }
            String hierarchyConflict = findHierarchyConflict(
                    collisionKey, collisionPaths, descendantPaths);
            if (hierarchyConflict != null) {
                throw packageError("PATH_HIERARCHY_CONFLICT", file.path,
                        "Skill export path conflicts with file " + hierarchyConflict
                                + " in the same path hierarchy.");
            }
            collisionPaths.put(collisionKey, file.path);
            indexDescendantPath(collisionKey, file.path, descendantPaths);
        }
        return files;
    }

    private OutputWriteResult writeArchive(Path archivePath, List<OutputFile> files,
                                           SkillPackageLimits limits) throws IOException {
        OutputWriteResult initialResult = writeArchiveAttempt(archivePath, files, limits, Map.of());
        Map<String, StoredEntryMetadata> storedEntries = findEntriesExceedingCompressionRatio(
                archivePath, limits);
        if (storedEntries.isEmpty()) {
            return initialResult;
        }

        OutputWriteResult rewrittenResult = writeArchiveAttempt(
                archivePath, files, limits, storedEntries);
        Map<String, StoredEntryMetadata> remainingViolations = findEntriesExceedingCompressionRatio(
                archivePath, limits);
        if (!remainingViolations.isEmpty()) {
            String path = remainingViolations.keySet().iterator().next();
            throw packageError("COMPRESSION_RATIO_LIMIT", path,
                    "Encoded Skill ZIP cannot satisfy the configured compression ratio limit.");
        }
        return rewrittenResult;
    }

    /**
     * 按指定 entry 存储策略写出一次稳定 ZIP。
     *
     * @param archivePath 临时归档路径
     * @param files 输出文件计划
     * @param limits 当前写出限额
     * @param storedEntries 需要使用 STORED 的 entry 元数据
     * @return 本次写出结果
     * @throws IOException 写出失败
     */
    private OutputWriteResult writeArchiveAttempt(Path archivePath, List<OutputFile> files,
                                                  SkillPackageLimits limits,
                                                  Map<String, StoredEntryMetadata> storedEntries)
            throws IOException {
        MessageDigest digest = SkillHashes.newSha256Digest();
        LimitedOutputStream limitedOutput = new LimitedOutputStream(
                Files.newOutputStream(archivePath, StandardOpenOption.TRUNCATE_EXISTING),
                limits.getMaxCompressedPackageBytes());
        try (DigestOutputStream digestOutput = new DigestOutputStream(limitedOutput, digest);
             ZipOutputStream zipOutput = new ZipOutputStream(digestOutput, StandardCharsets.UTF_8)) {
            for (OutputFile file : files) {
                ZipEntry entry = new ZipEntry(file.path);
                entry.setTime(STABLE_ZIP_TIMESTAMP);
                StoredEntryMetadata storedMetadata = storedEntries.get(file.path);
                if (storedMetadata != null) {
                    entry.setMethod(ZipEntry.STORED);
                    entry.setSize(storedMetadata.size);
                    entry.setCompressedSize(storedMetadata.size);
                    entry.setCrc(storedMetadata.crc);
                }
                zipOutput.putNextEntry(entry);
                if (file.text != null) {
                    writeTextResource(zipOutput, file.text, file.path);
                } else {
                    writeBinaryResource(zipOutput, file.resource, limits, file.path);
                }
                zipOutput.closeEntry();
            }
            zipOutput.finish();
        }
        return new OutputWriteResult(SkillHashes.toHex(digest.digest()), limitedOutput.getCount());
    }

    /**
     * 找出会在同一限额下被解码器判定为高压缩比的 entry。
     *
     * @param archivePath 已完成的临时归档
     * @param limits 当前写出限额
     * @return 需要改用 STORED 的 entry 元数据
     * @throws IOException 归档检查失败
     */
    private static Map<String, StoredEntryMetadata> findEntriesExceedingCompressionRatio(
            Path archivePath, SkillPackageLimits limits) throws IOException {
        Map<String, StoredEntryMetadata> entries = new LinkedHashMap<>();
        try (ZipFile zipFile = new ZipFile(archivePath)) {
            Enumeration<ZipArchiveEntry> archiveEntries = zipFile.getEntriesInPhysicalOrder();
            while (archiveEntries.hasMoreElements()) {
                ZipArchiveEntry entry = archiveEntries.nextElement();
                long size = entry.getSize();
                long compressedSize = entry.getCompressedSize();
                if (size > 0 && (compressedSize == 0
                        || ((double) size / compressedSize) > limits.getMaxCompressionRatio())) {
                    entries.put(entry.getName(), new StoredEntryMetadata(size, entry.getCrc()));
                }
            }
        }
        return entries;
    }

    /**
     * 将单个文本严格编码到当前 ZIP entry，不在输出计划中保留 UTF-8 byte 数组。
     *
     * @param output 当前 ZIP 输出流
     * @param text 文本内容
     * @param path 包内文件路径
     * @throws IOException 写出失败
     */
    private void writeTextResource(OutputStream output, String text, String path) throws IOException {
        output.write(strictUtf8Bytes(text, path));
    }

    /**
     * 计算单个文本文件的 UTF-8 字节数；结果只用于边界校验，不在输出计划中保留 byte 数组。
     *
     * @param text 文本内容
     * @param path 包内文件路径
     * @return UTF-8 字节数
     */
    private long utf8Size(String text, String path) {
        try {
            return SkillUtf8.byteLength(text);
        } catch (CharacterCodingException e) {
            throw new SkillPackageException("INVALID_UTF8", path,
                    "Text Skill resource must be losslessly encodable as UTF-8.", e);
        }
    }

    private void writeBinaryResource(OutputStream output, SkillResource resource,
                                     SkillPackageLimits limits, String path) throws IOException {
        long maxBytes = Math.min(limits.getMaxBinaryFileBytes(), resource.getSize());
        MessageDigest digest = SkillHashes.newSha256Digest();
        long count;
        try (InputStream input = contentStore.open(resource.getContentRef())) {
            count = transfer(input, output, maxBytes, digest);
        } catch (SkillPackageException | IOException e) {
            throw e;
        } catch (RuntimeException e) {
            throw new SkillPackageException("CONTENT_STORE_ERROR", path,
                    "Failed to open binary Skill resource content.", e);
        }
        if (count != resource.getSize()) {
            throw packageError("RESOURCE_SIZE_MISMATCH", path,
                    "Binary resource size does not match stored content.");
        }
        String actualHash = SkillHashes.toHex(digest.digest());
        if (!actualHash.equals(resource.getContentHash())) {
            throw packageError("RESOURCE_HASH_MISMATCH", path,
                    "Binary resource hash does not match stored content.");
        }
    }

    private boolean contentExists(String contentRef, String path) {
        try {
            return contentStore.exists(contentRef);
        } catch (RuntimeException e) {
            throw new SkillPackageException("CONTENT_STORE_ERROR", path,
                    "Failed to inspect binary Skill resource content.", e);
        }
    }

    private static String readStrictText(ZipFile zipFile, ArchiveFile file, long limit) throws IOException {
        if (file.entry.getSize() > limit) {
            throw packageError("TEXT_FILE_SIZE_LIMIT", file.fullPath,
                    "Text resource exceeds " + limit + " bytes.");
        }
        byte[] bytes;
        try (InputStream input = zipFile.getInputStream(file.entry)) {
            bytes = readBytes(input, limit, file.fullPath);
        }
        if (bytes.length != file.entry.getSize()) {
            throw packageError("ENTRY_SIZE_MISMATCH", file.fullPath,
                    "ZIP entry size changed while reading.");
        }
        CRC32 crc = new CRC32();
        crc.update(bytes);
        verifyCrc(file.entry, crc.getValue(), file.fullPath);
        try {
            return StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(bytes))
                    .toString();
        } catch (CharacterCodingException e) {
            throw new SkillPackageException("INVALID_UTF8", file.fullPath,
                    "Text Skill resource must be valid UTF-8.", e);
        }
    }

    private static byte[] readBytes(InputStream input, long limit, String path) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream((int) Math.min(limit, 8_192));
        transfer(input, output, limit, null, path);
        return output.toByteArray();
    }

    /**
     * 将已识别为文本的内容严格编码为 UTF-8 字节。
     *
     * @param text 文本内容
     * @param path 包内文件路径
     * @return UTF-8 字节
     * @throws SkillPackageException 文本包含非法 UTF-16 code unit
     */
    private static byte[] strictUtf8Bytes(String text, String path) {
        try {
            return SkillUtf8.encode(text);
        } catch (CharacterCodingException e) {
            throw new SkillPackageException("INVALID_UTF8", path,
                    "Text Skill resource must be losslessly encodable as UTF-8.", e);
        }
    }

    private static InputCopyResult copyInput(InputStream input, Path target, long limit) throws IOException {
        MessageDigest digest = SkillHashes.newSha256Digest();
        long count = 0;
        try (OutputStream fileOutput = Files.newOutputStream(target, StandardOpenOption.TRUNCATE_EXISTING);
             DigestOutputStream digestOutput = new DigestOutputStream(fileOutput, digest)) {
            byte[] buffer = new byte[16 * 1024];
            int read;
            while ((read = input.read(buffer)) >= 0) {
                if (read == 0) {
                    continue;
                }
                if (count > limit - read) {
                    throw packageError("COMPRESSED_SIZE_LIMIT", null,
                            "Compressed Skill ZIP exceeds " + limit + " bytes.");
                }
                digestOutput.write(buffer, 0, read);
                count += read;
            }
        }
        if (count == 0) {
            throw packageError("EMPTY_ZIP", null, "Skill ZIP input cannot be empty.");
        }
        return new InputCopyResult(SkillHashes.toHex(digest.digest()), count);
    }

    private static void validateZipSignature(Path archivePath) throws IOException {
        byte[] signature = new byte[4];
        try (InputStream input = Files.newInputStream(archivePath)) {
            if (input.read(signature) != signature.length) {
                throw packageError("INVALID_ZIP", null, "Skill package is not a valid ZIP archive.");
            }
        }
        boolean localFile = signature[0] == 0x50 && signature[1] == 0x4B
                && signature[2] == 0x03 && signature[3] == 0x04;
        boolean emptyArchive = signature[0] == 0x50 && signature[1] == 0x4B
                && signature[2] == 0x05 && signature[3] == 0x06;
        if (!localFile && !emptyArchive) {
            throw packageError("INVALID_ZIP", null,
                    "Skill package must start with a standard ZIP signature.");
        }
    }

    /**
     * 在 Commons Compress 构造所有 entry 对象前，以常量内存扫描中央目录并执行条目数限制。
     *
     * @param archivePath ZIP 临时文件
     * @param maxEntryCount 最大条目数
     * @throws SkillPackageException 中央目录非法或条目数超限
     */
    private static void enforceCentralDirectoryEntryLimit(Path archivePath, int maxEntryCount) {
        try (SeekableByteChannel channel = Files.newByteChannel(archivePath, StandardOpenOption.READ)) {
            CentralDirectoryDescriptor descriptor = readCentralDirectoryDescriptor(channel);
            if (descriptor.entryCount > maxEntryCount) {
                throw packageError("ENTRY_COUNT_LIMIT", null,
                        "Skill ZIP exceeds " + maxEntryCount + " entries.");
            }

            long centralDirectoryEnd = Math.addExact(
                    descriptor.offset, descriptor.size);
            if (descriptor.offset < 0 || descriptor.size < 0
                    || centralDirectoryEnd != descriptor.endRecordOffset
                    || centralDirectoryEnd > channel.size()) {
                throw invalidZip("ZIP central directory bounds are invalid.");
            }

            long position = descriptor.offset;
            long actualEntryCount = 0;
            while (position < centralDirectoryEnd) {
                if (centralDirectoryEnd - position < CENTRAL_DIRECTORY_HEADER_SIZE) {
                    throw invalidZip("ZIP central directory is truncated.");
                }
                ByteBuffer header = readAt(channel, position, CENTRAL_DIRECTORY_HEADER_SIZE);
                if (header.getInt(0) != CENTRAL_DIRECTORY_SIGNATURE) {
                    throw invalidZip("ZIP central directory contains an invalid entry header.");
                }
                long variableSize = (long) unsignedShort(header, 28)
                        + unsignedShort(header, 30)
                        + unsignedShort(header, 32);
                long nextPosition = Math.addExact(position,
                        Math.addExact((long) CENTRAL_DIRECTORY_HEADER_SIZE, variableSize));
                if (nextPosition > centralDirectoryEnd) {
                    throw invalidZip("ZIP central directory entry exceeds its declared bounds.");
                }
                actualEntryCount++;
                if (actualEntryCount > maxEntryCount) {
                    throw packageError("ENTRY_COUNT_LIMIT", null,
                            "Skill ZIP exceeds " + maxEntryCount + " entries.");
                }
                position = nextPosition;
            }
            if (actualEntryCount != descriptor.entryCount) {
                throw invalidZip("ZIP central directory entry count is inconsistent.");
            }
        } catch (SkillPackageException exception) {
            throw exception;
        } catch (IOException | ArithmeticException exception) {
            throw new SkillPackageException("INVALID_ZIP", null,
                    "Skill package ZIP central directory is invalid.", exception);
        }
    }

    /**
     * 从 EOCD 或 ZIP64 EOCD 读取受信前所需的中央目录边界信息。
     *
     * @param channel ZIP 文件通道
     * @return 中央目录描述
     * @throws IOException 文件读取失败
     */
    private static CentralDirectoryDescriptor readCentralDirectoryDescriptor(
            SeekableByteChannel channel) throws IOException {
        long archiveSize = channel.size();
        if (archiveSize < END_OF_CENTRAL_DIRECTORY_MIN_SIZE) {
            throw invalidZip("ZIP end-of-central-directory record is missing.");
        }
        int tailSize = (int) Math.min(archiveSize, END_OF_CENTRAL_DIRECTORY_MAX_SIZE);
        long tailOffset = archiveSize - tailSize;
        ByteBuffer tail = readAt(channel, tailOffset, tailSize);
        int eocdIndex = findEndOfCentralDirectory(tail);
        if (eocdIndex < 0) {
            throw invalidZip("ZIP end-of-central-directory record is missing or malformed.");
        }

        long eocdOffset = tailOffset + eocdIndex;
        int diskNumber = unsignedShort(tail, eocdIndex + 4);
        int centralDirectoryDisk = unsignedShort(tail, eocdIndex + 6);
        int entriesOnDisk = unsignedShort(tail, eocdIndex + 8);
        int totalEntries = unsignedShort(tail, eocdIndex + 10);
        long centralDirectorySize = unsignedInt(tail, eocdIndex + 12);
        long centralDirectoryOffset = unsignedInt(tail, eocdIndex + 16);
        boolean zip64 = diskNumber == UINT16_MAX
                || centralDirectoryDisk == UINT16_MAX
                || entriesOnDisk == UINT16_MAX
                || totalEntries == UINT16_MAX
                || centralDirectorySize == UINT32_MAX
                || centralDirectoryOffset == UINT32_MAX;
        if (!zip64) {
            if (diskNumber != 0 || centralDirectoryDisk != 0 || entriesOnDisk != totalEntries) {
                throw packageError("MULTI_DISK_ZIP", null,
                        "Multi-disk ZIP packages are not supported.");
            }
            return new CentralDirectoryDescriptor(
                    centralDirectoryOffset, centralDirectorySize, totalEntries, eocdOffset);
        }
        return readZip64CentralDirectoryDescriptor(channel, eocdOffset,
                diskNumber, centralDirectoryDisk, entriesOnDisk, totalEntries,
                centralDirectorySize, centralDirectoryOffset);
    }

    /**
     * 读取并交叉校验 ZIP64 EOCD 与定位器。
     *
     * @param channel ZIP 文件通道
     * @param eocdOffset 传统 EOCD 偏移
     * @param diskNumber 传统 EOCD 磁盘号
     * @param centralDirectoryDisk 传统 EOCD 中央目录磁盘号
     * @param entriesOnDisk 传统 EOCD 当前磁盘条目数
     * @param totalEntries 传统 EOCD 总条目数
     * @param centralDirectorySize 传统 EOCD 中央目录大小
     * @param centralDirectoryOffset 传统 EOCD 中央目录偏移
     * @return ZIP64 中央目录描述
     * @throws IOException 文件读取失败
     */
    private static CentralDirectoryDescriptor readZip64CentralDirectoryDescriptor(
            SeekableByteChannel channel,
            long eocdOffset,
            int diskNumber,
            int centralDirectoryDisk,
            int entriesOnDisk,
            int totalEntries,
            long centralDirectorySize,
            long centralDirectoryOffset) throws IOException {
        long locatorOffset = eocdOffset - ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE;
        if (locatorOffset < 0) {
            throw invalidZip("ZIP64 end-of-central-directory locator is missing.");
        }
        ByteBuffer locator = readAt(
                channel, locatorOffset, ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE);
        if (locator.getInt(0) != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE) {
            throw invalidZip("ZIP64 end-of-central-directory locator is missing.");
        }
        long locatorDisk = unsignedInt(locator, 4);
        long zip64Offset = unsignedLong(locator, 8);
        long totalDisks = unsignedInt(locator, 16);
        if (locatorDisk != 0 || totalDisks != 1) {
            throw packageError("MULTI_DISK_ZIP", null,
                    "Multi-disk ZIP packages are not supported.");
        }

        ByteBuffer zip64 = readAt(
                channel, zip64Offset, ZIP64_END_OF_CENTRAL_DIRECTORY_MIN_SIZE);
        if (zip64.getInt(0) != ZIP64_END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
            throw invalidZip("ZIP64 end-of-central-directory record is missing.");
        }
        long zip64RecordSize = unsignedLong(zip64, 4);
        if (zip64RecordSize < 44) {
            throw invalidZip("ZIP64 end-of-central-directory record is too short.");
        }
        long zip64RecordEnd = Math.addExact(zip64Offset, Math.addExact(12L, zip64RecordSize));
        if (zip64RecordEnd != locatorOffset) {
            throw invalidZip("ZIP64 end-of-central-directory bounds are invalid.");
        }

        long zip64DiskNumber = unsignedInt(zip64, 16);
        long zip64CentralDirectoryDisk = unsignedInt(zip64, 20);
        long zip64EntriesOnDisk = unsignedLong(zip64, 24);
        long zip64TotalEntries = unsignedLong(zip64, 32);
        long zip64CentralDirectorySize = unsignedLong(zip64, 40);
        long zip64CentralDirectoryOffset = unsignedLong(zip64, 48);
        if (zip64DiskNumber != 0 || zip64CentralDirectoryDisk != 0
                || zip64EntriesOnDisk != zip64TotalEntries) {
            throw packageError("MULTI_DISK_ZIP", null,
                    "Multi-disk ZIP packages are not supported.");
        }
        validateLegacyZip64Value(diskNumber, UINT16_MAX, zip64DiskNumber,
                "ZIP disk number is inconsistent with ZIP64 metadata.");
        validateLegacyZip64Value(centralDirectoryDisk, UINT16_MAX,
                zip64CentralDirectoryDisk,
                "ZIP central-directory disk is inconsistent with ZIP64 metadata.");
        validateLegacyZip64Value(entriesOnDisk, UINT16_MAX, zip64EntriesOnDisk,
                "ZIP entry count is inconsistent with ZIP64 metadata.");
        validateLegacyZip64Value(totalEntries, UINT16_MAX, zip64TotalEntries,
                "ZIP entry count is inconsistent with ZIP64 metadata.");
        validateLegacyZip64Value(centralDirectorySize, UINT32_MAX,
                zip64CentralDirectorySize,
                "ZIP central-directory size is inconsistent with ZIP64 metadata.");
        validateLegacyZip64Value(centralDirectoryOffset, UINT32_MAX,
                zip64CentralDirectoryOffset,
                "ZIP central-directory offset is inconsistent with ZIP64 metadata.");
        return new CentralDirectoryDescriptor(zip64CentralDirectoryOffset,
                zip64CentralDirectorySize, zip64TotalEntries, zip64Offset);
    }

    /**
     * 校验传统 EOCD 中未使用哨兵值的字段与 ZIP64 元数据一致。
     *
     * @param legacyValue 传统字段值
     * @param sentinel ZIP64 哨兵值
     * @param zip64Value ZIP64 字段值
     * @param message 不一致时的错误消息
     */
    private static void validateLegacyZip64Value(long legacyValue, long sentinel,
                                                 long zip64Value, String message) {
        if (legacyValue != sentinel && legacyValue != zip64Value) {
            throw invalidZip(message);
        }
    }

    /**
     * 在文件尾缓冲区中定位与注释长度一致的 EOCD。
     *
     * @param tail ZIP 文件尾缓冲区
     * @return EOCD 相对偏移，未找到时返回 -1
     */
    private static int findEndOfCentralDirectory(ByteBuffer tail) {
        for (int index = tail.limit() - END_OF_CENTRAL_DIRECTORY_MIN_SIZE; index >= 0; index--) {
            if (tail.getInt(index) == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
                int commentLength = unsignedShort(tail, index + 20);
                if (index + END_OF_CENTRAL_DIRECTORY_MIN_SIZE + commentLength == tail.limit()) {
                    return index;
                }
            }
        }
        return -1;
    }

    /**
     * 从通道指定位置完整读取固定长度数据，并按小端序返回。
     *
     * @param channel 文件通道
     * @param position 起始偏移
     * @param length 读取长度
     * @return 已翻转的小端缓冲区
     * @throws IOException 读取失败或文件截断
     */
    private static ByteBuffer readAt(SeekableByteChannel channel, long position, int length)
            throws IOException {
        if (position < 0 || length < 0 || position > channel.size() - length) {
            throw new EOFException("ZIP record exceeds archive bounds.");
        }
        ByteBuffer buffer = ByteBuffer.allocate(length).order(ByteOrder.LITTLE_ENDIAN);
        channel.position(position);
        while (buffer.hasRemaining()) {
            int read = channel.read(buffer);
            if (read < 0) {
                throw new EOFException("ZIP record is truncated.");
            }
        }
        buffer.flip();
        return buffer;
    }

    /**
     * 读取无符号 16 位小端整数。
     *
     * @param buffer 小端缓冲区
     * @param offset 字段偏移
     * @return 无符号值
     */
    private static int unsignedShort(ByteBuffer buffer, int offset) {
        return Short.toUnsignedInt(buffer.getShort(offset));
    }

    /**
     * 读取无符号 32 位小端整数。
     *
     * @param buffer 小端缓冲区
     * @param offset 字段偏移
     * @return 无符号值
     */
    private static long unsignedInt(ByteBuffer buffer, int offset) {
        return Integer.toUnsignedLong(buffer.getInt(offset));
    }

    /**
     * 读取当前实现可安全表示的无符号 64 位小端整数。
     *
     * @param buffer 小端缓冲区
     * @param offset 字段偏移
     * @return 非负 long 值
     */
    private static long unsignedLong(ByteBuffer buffer, int offset) {
        long value = buffer.getLong(offset);
        if (value < 0) {
            throw invalidZip("ZIP64 value exceeds the supported range.");
        }
        return value;
    }

    /**
     * 创建稳定的非法 ZIP 异常。
     *
     * @param message 错误消息
     * @return 非法 ZIP 异常
     */
    private static SkillPackageException invalidZip(String message) {
        return packageError("INVALID_ZIP", null, message);
    }

    private static long transfer(InputStream input, OutputStream output, long limit, MessageDigest digest)
            throws IOException {
        return transfer(input, output, limit, digest, null);
    }

    private static long transfer(InputStream input, OutputStream output, long limit,
                                 MessageDigest digest, String path) throws IOException {
        byte[] buffer = new byte[16 * 1024];
        long total = 0;
        int read;
        while ((read = input.read(buffer)) >= 0) {
            if (read == 0) {
                continue;
            }
            if (total > limit - read) {
                throw packageError("SIZE_LIMIT", path, "Skill content exceeds " + limit + " bytes.");
            }
            output.write(buffer, 0, read);
            if (digest != null) {
                digest.update(buffer, 0, read);
            }
            total += read;
        }
        return total;
    }

    private static String normalizeArchivePath(String rawPath, SkillPackageLimits limits,
                                               boolean ignoredSystemPath) {
        String normalized;
        try {
            normalized = ignoredSystemPath
                    ? SkillPaths.normalizeIgnoredSystemPath(rawPath)
                    : SkillPaths.normalize(rawPath);
        } catch (SkillValidationException e) {
            throw packageError("UNSAFE_ENTRY_PATH", rawPath, e.getMessage());
        }
        validateOutputPath(normalized, limits);
        return normalized;
    }

    /**
     * 严格校验并返回 ZIP entry 的 UTF-8 文件名。
     *
     * @param entry ZIP entry
     * @return 唯一的 UTF-8 文件名
     * @throws SkillPackageException 原始文件名字节非法或存在歧义
     */
    private static String strictUtf8EntryName(ZipArchiveEntry entry) {
        byte[] rawName = entry.getRawName();
        if (rawName == null) {
            throw packageError("INVALID_UTF8_ENTRY_NAME", entry.getName(),
                    "ZIP entry name bytes are required.");
        }
        try {
            String decodedName = StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(rawName))
                    .toString();
            if (!decodedName.equals(entry.getName())) {
                throw packageError("INVALID_UTF8_ENTRY_NAME", entry.getName(),
                        "ZIP entry name must have one unambiguous UTF-8 representation.");
            }
            return decodedName;
        } catch (CharacterCodingException e) {
            throw new SkillPackageException("INVALID_UTF8_ENTRY_NAME", entry.getName(),
                    "ZIP entry name must be valid UTF-8.", e);
        }
    }

    private static String stripDirectorySuffix(String path) {
        int end = path == null ? 0 : path.length();
        while (end > 0 && (path.charAt(end - 1) == '/' || path.charAt(end - 1) == '\\')) {
            end--;
        }
        return path == null ? null : path.substring(0, end);
    }

    private static void validateOutputPath(String path, SkillPackageLimits limits) {
        try {
            SkillUtf8.encode(path);
        } catch (CharacterCodingException e) {
            throw new SkillPackageException("INVALID_UTF8_PATH", path,
                    "Skill output path must be losslessly encodable as UTF-8.", e);
        }
        if (path.length() > limits.getMaxPathLength()) {
            throw packageError("PATH_LENGTH_LIMIT", path,
                    "Skill path exceeds " + limits.getMaxPathLength() + " characters.");
        }
        if (path.split("/", -1).length > limits.getMaxPathDepth()) {
            throw packageError("PATH_DEPTH_LIMIT", path,
                    "Skill path exceeds depth " + limits.getMaxPathDepth() + ".");
        }
    }

    /**
     * 查找当前文件路径与已索引文件之间的祖先或后代冲突。
     *
     * @param pathKey 当前路径冲突键
     * @param filePaths 已索引的文件路径
     * @param descendantPaths 已索引路径对应的首个后代文件
     * @return 冲突文件路径，不存在时返回 null
     */
    private static String findHierarchyConflict(String pathKey, Map<String, String> filePaths,
                                                Map<String, String> descendantPaths) {
        String descendant = descendantPaths.get(pathKey);
        if (descendant != null) {
            return descendant;
        }
        int separator = pathKey.indexOf('/');
        while (separator >= 0) {
            String ancestor = filePaths.get(pathKey.substring(0, separator));
            if (ancestor != null) {
                return ancestor;
            }
            separator = pathKey.indexOf('/', separator + 1);
        }
        return null;
    }

    /**
     * 为文件路径的每个祖先建立后代索引。
     *
     * @param pathKey 文件路径冲突键
     * @param path 原始文件路径
     * @param descendantPaths 后代索引
     */
    private static void indexDescendantPath(String pathKey, String path,
                                            Map<String, String> descendantPaths) {
        int separator = pathKey.indexOf('/');
        while (separator >= 0) {
            descendantPaths.putIfAbsent(pathKey.substring(0, separator), path);
            separator = pathKey.indexOf('/', separator + 1);
        }
    }

    private static void checkCompressionRatio(String path, long size, long compressedSize,
                                              SkillPackageLimits limits) {
        if (size == 0) {
            return;
        }
        if (compressedSize == 0 || ((double) size / compressedSize) > limits.getMaxCompressionRatio()) {
            throw packageError("COMPRESSION_RATIO_LIMIT", path,
                    "ZIP entry compression ratio exceeds " + limits.getMaxCompressionRatio() + ":1.");
        }
    }

    private static void verifyCrc(ZipArchiveEntry entry, long actualCrc, String path) {
        if (entry.getCrc() != actualCrc) {
            throw packageError("CRC_MISMATCH", path,
                    "ZIP entry CRC does not match decompressed content.");
        }
    }

    private static long checkedOutputTotal(long current, long size, SkillPackageLimits limits, String path) {
        if (size < 0) {
            throw packageError("NEGATIVE_RESOURCE_SIZE", path, "Skill resource size cannot be negative.");
        }
        long total;
        try {
            total = Math.addExact(current, size);
        } catch (ArithmeticException e) {
            throw packageError("TOTAL_SIZE_LIMIT", path, "Skill package size overflow.");
        }
        if (total > limits.getMaxTotalUncompressedBytes()) {
            throw packageError("TOTAL_SIZE_LIMIT", path,
                    "Skill package exceeds the total uncompressed size limit.");
        }
        return total;
    }

    private static String detectMediaType(String path) {
        String lowerPath = path.toLowerCase(Locale.ROOT);
        if (lowerPath.endsWith(".md") || lowerPath.endsWith(".markdown")) {
            return "text/markdown";
        }
        if (lowerPath.endsWith(".yaml") || lowerPath.endsWith(".yml")) {
            return "application/yaml";
        }
        if (lowerPath.endsWith(".json")) {
            return "application/json";
        }
        if (lowerPath.endsWith(".py")) {
            return "text/x-python";
        }
        if (lowerPath.endsWith(".js") || lowerPath.endsWith(".mjs") || lowerPath.endsWith(".cjs")) {
            return "text/javascript";
        }
        if (lowerPath.endsWith(".sh") || lowerPath.endsWith(".bash") || lowerPath.endsWith(".zsh")) {
            return "text/x-shellscript";
        }
        String detected = URLConnection.guessContentTypeFromName(path);
        return detected == null ? DEFAULT_MEDIA_TYPE : detected;
    }

    private static String scalar(Object value) {
        return value instanceof String text ? text : null;
    }

    private static SkillPackageException validationPackageError(SkillValidationException exception) {
        return validationPackageError(exception, null);
    }

    /**
     * 将文档校验异常转换为包异常，并按需补齐包装 Skill 根目录。
     *
     * @param exception 文档校验异常
     * @param packageRoot 包装 Skill 根目录，根直压时为空
     * @return 带完整结构化报告的包异常
     */
    private static SkillPackageException validationPackageError(
            SkillValidationException exception, String packageRoot) {
        SkillValidationReport report = exception.getReport();
        if (report == null) {
            report = new SkillValidationReport().add(new SkillValidationIssue(
                    exception.getCode(), SkillValidationSeverity.ERROR, exception.getPath(),
                    exception.getLine(), exception.getColumn(), exception.getMessage(), null));
        }
        return new SkillPackageException("Invalid Skill document.",
                prefixValidationPaths(report, packageRoot));
    }

    /**
     * 为包装布局中的校验问题补齐 Skill 根目录，使批量预览可唯一定位文件。
     *
     * @param report 原始 Skill 相对路径报告
     * @param packageRoot 包装 Skill 根目录，根直压时为空
     * @return 路径补齐后的报告
     */
    private static SkillValidationReport prefixValidationPaths(
            SkillValidationReport report, String packageRoot) {
        if (report == null || packageRoot == null || packageRoot.isBlank()) {
            return report;
        }
        SkillValidationReport prefixed = new SkillValidationReport();
        for (SkillValidationIssue issue : report.getIssues()) {
            String path = issue.getPath();
            if (path != null && !path.isBlank()
                    && !path.equals(packageRoot) && !path.startsWith(packageRoot + "/")) {
                path = packageRoot + "/" + path;
            }
            prefixed.add(new SkillValidationIssue(issue.getCode(), issue.getSeverity(), path,
                    issue.getLine(), issue.getColumn(), issue.getMessage(), issue.getSuggestion()));
        }
        return prefixed;
    }

    private static SkillPackageException packageError(String code, String path, String message) {
        return new SkillPackageException(code, path, message);
    }

    private static boolean hasCause(Throwable throwable, Class<? extends Throwable> type) {
        Throwable current = throwable;
        while (current != null) {
            if (type.isInstance(current)) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }

    private static void deleteQuietly(Path path) {
        if (path == null) {
            return;
        }
        try {
            Files.deleteIfExists(path);
        } catch (IOException ignored) {
            // 临时文件由操作系统兜底清理；主异常优先返回给调用方。
        }
    }

    private record ArchiveFile(ZipArchiveEntry entry, String fullPath, String relativePath) {

        private ArchiveFile(ZipArchiveEntry entry, String fullPath) {
            this(entry, fullPath, fullPath);
        }

        private ArchiveFile withRelativePath(String value) {
            return new ArchiveFile(entry, fullPath, value);
        }
    }

    private record ArchiveGroup(String archiveRoot, String packageRoot, List<ArchiveFile> files) {
    }

    private record ArchiveLayout(SkillPackageLayout layout, List<ArchiveGroup> groups) {
    }

    private record DecodeState(SkillPackageLayout layout, List<Skill> skills) {
    }

    /**
     * 预扫描得到的中央目录边界与条目数。
     *
     * @param offset 中央目录偏移
     * @param size 中央目录大小
     * @param entryCount 声明条目数
     * @param endRecordOffset 紧随中央目录的 EOCD 或 ZIP64 EOCD 偏移
     */
    private record CentralDirectoryDescriptor(long offset, long size, long entryCount,
                                              long endRecordOffset) {
    }

    private record InputCopyResult(String hash, long size) {
    }

    private record OutputWriteResult(String hash, long size) {
    }

    /**
     * STORED entry 写出所需的中央目录元数据。
     *
     * @param size 未压缩字节数
     * @param crc CRC-32
     */
    private record StoredEntryMetadata(long size, long crc) {
    }

    private static final class OutputFile {

        private final String path;
        private final String text;
        private final SkillResource resource;

        private OutputFile(String path, String text, SkillResource resource) {
            this.path = path;
            this.text = text;
            this.resource = resource;
        }

        private static OutputFile text(String path, String text) {
            return new OutputFile(path, text, null);
        }

        private static OutputFile binary(String path, SkillResource resource) {
            return new OutputFile(path, null, resource);
        }
    }

    private static final class StageTracker {

        private final SkillContentStore contentStore;
        private final List<StageRecord> records = new ArrayList<>();
        private boolean finalized;

        private StageTracker(SkillContentStore contentStore) {
            this.contentStore = contentStore;
        }

        private void add(SkillContentStage stage, SkillResource resource, String path) {
            records.add(new StageRecord(stage, resource, path));
        }

        private void commitAll() {
            for (StageRecord record : records) {
                String contentRef = contentStore.commit(record.stage);
                record.committedRef = contentRef;
                if (!record.stage.getContentRef().equals(contentRef)) {
                    throw packageError("CONTENT_REF_MISMATCH", record.path,
                            "Content store returned a different reference during commit.");
                }
                record.resource.setContentRef(contentRef);
            }
            finalized = true;
        }

        private void rollbackAll() {
            if (finalized) {
                return;
            }
            SkillPackageException rollbackFailure = null;
            for (int index = records.size() - 1; index >= 0; index--) {
                StageRecord record = records.get(index);
                if (record.cleaned) {
                    continue;
                }
                try {
                    contentStore.rollback(record.stage);
                    record.cleaned = true;
                } catch (RuntimeException cleanupError) {
                    if (rollbackFailure == null) {
                        rollbackFailure = new SkillPackageException(
                                "SKILL_CONTENT_ROLLBACK_ERROR", record.path,
                                "Failed to rollback staged Skill package content.", cleanupError);
                    } else {
                        rollbackFailure.addSuppressed(cleanupError);
                    }
                }
            }
            if (rollbackFailure != null) {
                throw rollbackFailure;
            }
            finalized = true;
        }

        private void cleanupAfterFailure(Throwable primary) {
            if (finalized) {
                return;
            }
            for (int index = records.size() - 1; index >= 0; index--) {
                StageRecord record = records.get(index);
                if (record.cleaned) {
                    continue;
                }
                try {
                    if (record.committedRef == null) {
                        contentStore.rollback(record.stage);
                    } else {
                        contentStore.release(record.committedRef);
                    }
                    record.cleaned = true;
                } catch (RuntimeException cleanupError) {
                    primary.addSuppressed(cleanupError);
                }
            }
            finalized = records.stream().allMatch(record -> record.cleaned);
        }
    }

    private static final class StageRecord {

        private final SkillContentStage stage;
        private final SkillResource resource;
        private final String path;
        private String committedRef;
        private boolean cleaned;

        private StageRecord(SkillContentStage stage, SkillResource resource, String path) {
            this.stage = stage;
            this.resource = resource;
            this.path = path;
        }
    }

    private static final class LimitedOutputStream extends OutputStream {

        private final OutputStream delegate;
        private final long limit;
        private long count;

        private LimitedOutputStream(OutputStream delegate, long limit) {
            this.delegate = delegate;
            this.limit = limit;
        }

        @Override
        public void write(int value) throws IOException {
            ensureCapacity(1);
            delegate.write(value);
            count++;
        }

        @Override
        public void write(byte[] bytes, int offset, int length) throws IOException {
            ensureCapacity(length);
            delegate.write(bytes, offset, length);
            count += length;
        }

        @Override
        public void flush() throws IOException {
            delegate.flush();
        }

        @Override
        public void close() throws IOException {
            delegate.close();
        }

        private long getCount() {
            return count;
        }

        private void ensureCapacity(int length) throws IOException {
            if (count > limit - length) {
                throw new CompressedSizeLimitException(
                        "Compressed Skill ZIP exceeds " + limit + " bytes.");
            }
        }
    }

    private static final class CompressedSizeLimitException extends IOException {

        private CompressedSizeLimitException(String message) {
            super(message);
        }
    }
}
