package com.easyagents.skill.validation.defaults;

import com.easyagents.skill.exception.SkillValidationException;
import com.easyagents.skill.model.Skill;
import com.easyagents.skill.model.SkillDocument;
import com.easyagents.skill.model.SkillPackageLimits;
import com.easyagents.skill.model.SkillResource;
import com.easyagents.skill.model.SkillResourceKind;
import com.easyagents.skill.model.SkillSourceLocation;
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 java.nio.charset.CharacterCodingException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * 默认 Skill 聚合结构化校验器。
 */
public class DefaultSkillValidator {

    private static final Pattern CANONICAL_NAME = Pattern.compile("[a-z0-9]+(?:-[a-z0-9]+)*");
    private static final Pattern LEGACY_UNDERSCORE_NAME = Pattern.compile("[a-z0-9]+(?:_[a-z0-9]+)+");
    private static final Pattern SHA_256 = Pattern.compile("[a-f0-9]{64}");

    private final SkillPackageLimits limits;

    /**
     * 使用默认安全限额创建校验器。
     */
    public DefaultSkillValidator() {
        this(SkillPackageLimits.defaults());
    }

    /**
     * 使用指定安全限额创建校验器。
     *
     * @param limits 安全限额
     */
    public DefaultSkillValidator(SkillPackageLimits limits) {
        this.limits = limits == null ? SkillPackageLimits.defaults() : limits;
    }

    /**
     * 校验 Skill 聚合，存在错误时抛出包含完整报告的异常。
     *
     * @param skill Skill 聚合
     * @throws SkillValidationException 校验失败
     */
    public void validate(Skill skill) {
        validateReport(skill, limits, SkillValidationMode.STANDARD).throwIfInvalid();
    }

    /**
     * 聚合校验 Skill。
     *
     * @param skill Skill 聚合
     * @return 结构化校验报告
     */
    public SkillValidationReport validateReport(Skill skill) {
        return validateReport(skill, limits);
    }

    /**
     * 使用当前 Codec 操作的安全限额聚合校验 Skill。
     *
     * @param skill Skill 聚合
     * @param operationLimits 当前读写操作的安全限额
     * @return 结构化校验报告
     */
    public SkillValidationReport validateReport(Skill skill, SkillPackageLimits operationLimits) {
        return validateReport(skill, operationLimits, SkillValidationMode.DRAFT_IMPORT);
    }

    /**
     * 使用指定标准模式和当前操作限额聚合校验 Skill。
     *
     * @param skill Skill 聚合
     * @param operationLimits 当前读写操作的安全限额
     * @param mode 标准校验模式
     * @return 结构化校验报告
     */
    public SkillValidationReport validateReport(Skill skill, SkillPackageLimits operationLimits,
                                                SkillValidationMode mode) {
        SkillPackageLimits effectiveLimits = operationLimits == null ? limits : operationLimits;
        SkillValidationMode effectiveMode = mode == null
                ? SkillValidationMode.DRAFT_IMPORT : mode;
        SkillValidationReport report = new SkillValidationReport();
        if (skill == null) {
            return report.add(error("SKILL_REQUIRED", null, "Skill is required.", null));
        }

        SkillDocument document = parseDocument(skill, report, effectiveLimits);
        validateName(skill, document, report, effectiveMode);
        validateDescription(document, report);
        if (document != null) {
            validateDocument(document, report);
        }
        List<SkillResource> resources = skill.getResources();
        validateAggregateLimits(skill, resources, report, effectiveLimits);
        validateResources(skill.getName(), resources, report, effectiveLimits);
        return report;
    }

    private static void validateName(Skill skill, SkillDocument document,
                                     SkillValidationReport report, SkillValidationMode mode) {
        SkillSourceLocation location = sourceLocation(document, "name");
        String name = frontmatterString(document, "name");
        if (isBlank(name)) {
            report.add(errorAt("NAME_REQUIRED", SkillPaths.SKILL_FILE, location, "Skill name is required.",
                    "Set frontmatter name to a portable Skill name."));
            return;
        }
        if (name.length() > 64) {
            report.add(errorAt("NAME_TOO_LONG", SkillPaths.SKILL_FILE, location,
                    "Skill name cannot exceed 64 characters.", "Shorten the frontmatter name."));
        } else if (LEGACY_UNDERSCORE_NAME.matcher(name).matches()) {
            SkillValidationSeverity severity = mode == SkillValidationMode.STANDARD
                    ? SkillValidationSeverity.ERROR : SkillValidationSeverity.WARNING;
            report.add(new SkillValidationIssue("NON_CANONICAL_NAME", severity,
                    SkillPaths.SKILL_FILE, line(location), column(location),
                    mode == SkillValidationMode.STANDARD
                            ? "Standard Skill name must use hyphens instead of underscores: " + name
                            : "Legacy underscore Skill name is accepted only for draft import: " + name,
                    "Replace underscores with single hyphens before publishing or standard export."));
        } else if (!CANONICAL_NAME.matcher(name).matches()) {
            report.add(errorAt("INVALID_NAME", SkillPaths.SKILL_FILE, location,
                    "Skill name must use lowercase letters, numbers, and single hyphens.",
                    "Use a name such as data-analysis."));
        }
        if (!isBlank(skill.getPackageRoot()) && !name.equals(skill.getPackageRoot())) {
            report.add(errorAt("ROOT_NAME_MISMATCH", SkillPaths.SKILL_FILE, location,
                    "Skill package directory must match frontmatter name.",
                    "Rename the package directory or update frontmatter name."));
        }
    }

    private static void validateDescription(SkillDocument document, SkillValidationReport report) {
        SkillSourceLocation location = sourceLocation(document, "description");
        String description = frontmatterString(document, "description");
        if (isBlank(description)) {
            report.add(errorAt("DESCRIPTION_REQUIRED", SkillPaths.SKILL_FILE, location,
                    "Skill description is required.", "Describe both the capability and when to use it."));
        } else if (description.length() > 1_024) {
            report.add(errorAt("DESCRIPTION_TOO_LONG", SkillPaths.SKILL_FILE, location,
                    "Skill description cannot exceed 1024 characters.", "Shorten the description."));
        }
    }

    private static SkillDocument parseDocument(Skill skill, SkillValidationReport report,
                                               SkillPackageLimits limits) {
        if (isBlank(skill.getSkillContent())) {
            report.add(error("SKILL_CONTENT_REQUIRED", SkillPaths.SKILL_FILE,
                    "SKILL.md content is required.", null));
            return null;
        }
        try {
            return SkillFrontmatter.parseDocument(skill.getSkillContent(), limits);
        } catch (SkillValidationException e) {
            report.add(new SkillValidationIssue(e.getCode(), SkillValidationSeverity.ERROR, e.getPath(),
                    e.getLine(), e.getColumn(), e.getMessage(), "Fix the YAML frontmatter and retry."));
            return null;
        }
    }

    private static void validateDocument(SkillDocument document, SkillValidationReport report) {
        Map<String, Object> values = document.getFrontmatter().getValues();
        validateOptionalString(document, values, "license", null, report);
        Object compatibility = values.get("compatibility");
        if (compatibility != null && (!(compatibility instanceof String text)
                || text.isBlank() || text.length() > 500)) {
            report.add(errorAt("INVALID_COMPATIBILITY", SkillPaths.SKILL_FILE,
                    sourceLocation(document, "compatibility"),
                    "Frontmatter compatibility must be a non-blank string of at most 500 characters.", null));
        }
        validateOptionalString(document, values, "allowed-tools", "INVALID_ALLOWED_TOOLS", report);
        validateMetadataField(document, values.get("metadata"), report);
        if (document.getMarkdownBody().length() > 30_000) {
            report.add(new SkillValidationIssue("LONG_SKILL_BODY", SkillValidationSeverity.WARNING,
                    SkillPaths.SKILL_FILE, null, null,
                    "SKILL.md body is large and may reduce progressive-disclosure efficiency.",
                    "Move detailed material into references/."));
        }
    }

    private static void validateOptionalString(SkillDocument document, Map<String, Object> values,
                                               String key, String code,
                                               SkillValidationReport report) {
        Object value = values.get(key);
        if (value != null && (!(value instanceof String text) || text.isBlank())) {
            report.add(errorAt(code == null ? "INVALID_" + key.toUpperCase(Locale.ROOT) : code,
                    SkillPaths.SKILL_FILE, sourceLocation(document, key),
                    "Frontmatter " + key + " must be a non-blank string when provided.", null));
        }
    }

    private static void validateMetadataField(SkillDocument document, Object metadata,
                                              SkillValidationReport report) {
        if (metadata == null) {
            return;
        }
        if (!(metadata instanceof Map<?, ?> map)) {
            report.add(errorAt("INVALID_METADATA", SkillPaths.SKILL_FILE,
                    sourceLocation(document, "metadata"),
                    "Frontmatter metadata must be a mapping.", null));
            return;
        }
        if (map.values().stream().anyMatch(value -> !(value instanceof String))) {
            SkillSourceLocation location = sourceLocation(document, "metadata");
            report.add(new SkillValidationIssue("NON_STANDARD_METADATA",
                    SkillValidationSeverity.WARNING, SkillPaths.SKILL_FILE,
                    line(location), column(location),
                    "Nested or non-string metadata is preserved for AgentScope compatibility.",
                    "Use string values only for strict Agent Skills interoperability."));
        }
    }

    private static void validateResources(String skillName, List<SkillResource> resources,
                                          SkillValidationReport report,
                                          SkillPackageLimits limits) {
        Set<String> exactPaths = new HashSet<>();
        Map<String, String> collisionPaths = new HashMap<>();
        Map<String, String> descendantPaths = new HashMap<>();
        exactPaths.add(SkillPaths.SKILL_FILE);
        collisionPaths.put(SkillPaths.collisionKey(SkillPaths.SKILL_FILE), SkillPaths.SKILL_FILE);
        for (SkillResource resource : resources) {
            if (resource == null) {
                report.add(error("NULL_RESOURCE", null, "Skill resource cannot be null.", null));
                continue;
            }
            if (isBlank(resource.getPath())) {
                report.add(error("RESOURCE_PATH_REQUIRED", null,
                        "Skill resource path is required.",
                        "Set a safe relative path inside the Skill package."));
                continue;
            }
            try {
                SkillUtf8.encode(resource.getPath());
            } catch (CharacterCodingException e) {
                report.add(error("INVALID_UTF8_PATH", resource.getPath(),
                        "Skill resource path must be losslessly encodable as UTF-8.",
                        "Remove malformed UTF-16 surrogate code units from the path."));
                continue;
            }
            String path;
            try {
                path = SkillPaths.normalize(resource.getPath());
            } catch (SkillValidationException e) {
                report.add(error("UNSAFE_RESOURCE_PATH", resource.getPath(), e.getMessage(), null));
                continue;
            }
            String portablePath = isBlank(skillName) ? path : skillName + "/" + path;
            if (portablePath.length() > limits.getMaxPathLength()) {
                report.add(error("PATH_LENGTH_LIMIT", path,
                        "Skill resource path exceeds the configured length limit.", null));
            }
            if (SkillPaths.depth(portablePath) > limits.getMaxPathDepth()) {
                report.add(error("PATH_DEPTH_LIMIT", path,
                        "Skill resource path exceeds the configured depth limit.", null));
            }
            if (!path.equals(resource.getPath())) {
                report.add(error("NON_NORMALIZED_PATH", path,
                        "Skill resource path must already be normalized.", null));
            }
            if (!exactPaths.add(path)) {
                report.add(error("DUPLICATE_RESOURCE_PATH", path,
                        "Duplicate Skill resource path: " + path, null));
                continue;
            }
            String collisionKey = SkillPaths.collisionKey(path);
            String conflicting = collisionPaths.get(collisionKey);
            if (conflicting != null) {
                report.add(error("RESOURCE_PATH_COLLISION", path,
                        "Skill resource path conflicts with " + conflicting + ".", null));
                continue;
            }
            String hierarchyConflict = findHierarchyConflict(collisionKey, collisionPaths, descendantPaths);
            if (hierarchyConflict != null) {
                report.add(error("RESOURCE_PATH_HIERARCHY_CONFLICT", path,
                        "Skill resource path conflicts with file " + hierarchyConflict
                                + " in the same path hierarchy.", null));
                continue;
            }
            collisionPaths.put(collisionKey, path);
            indexDescendantPath(collisionKey, path, descendantPaths);
            SkillResourceKind expectedKind = SkillResources.classify(path);
            if (resource.getKind() != expectedKind) {
                report.add(error("RESOURCE_KIND_MISMATCH", path,
                        "Skill resource kind does not match its path.", null));
            }
            validateResourceContent(resource, path, report, limits);
        }
    }

    private static void validateResourceContent(SkillResource resource, String path,
                                                SkillValidationReport report, SkillPackageLimits limits) {
        boolean expectedText = SkillResources.isText(path, resource.getMediaType());
        if (expectedText != resource.isText()) {
            report.add(error("RESOURCE_CONTENT_MODE_MISMATCH", path,
                    expectedText
                            ? "Resource path and media type require strict UTF-8 text content."
                            : "Resource path and media type require a binary content reference.",
                    "Store the resource using the canonical text or binary representation."));
        }
        if (resource.getKind() == SkillResourceKind.SCRIPT && resource.isText() && resource.getSize() == 0) {
            report.add(new SkillValidationIssue("EMPTY_SCRIPT", SkillValidationSeverity.WARNING,
                    path, null, null, "Script resource is empty.",
                    "Add script source or remove the unused file."));
        }
        if (isBlank(resource.getMediaType())) {
            report.add(error("MEDIA_TYPE_REQUIRED", path, "Skill resource media type is required.", null));
        }
        if (resource.getSize() < 0) {
            report.add(error("NEGATIVE_RESOURCE_SIZE", path,
                    "Skill resource size cannot be negative.", null));
        }
        byte[] textBytes = null;
        if (resource.isText()) {
            try {
                textBytes = SkillUtf8.encode(resource.getTextContent());
            } catch (CharacterCodingException e) {
                report.add(error("INVALID_UTF8", path,
                        "Text Skill resource must be losslessly encodable as UTF-8.",
                        "Remove malformed UTF-16 surrogate code units."));
                return;
            }
        }
        if (isBlank(resource.getContentHash()) || !SHA_256.matcher(resource.getContentHash()).matches()) {
            report.add(error("INVALID_RESOURCE_HASH", path,
                    "Skill resource SHA-256 is required and must be lowercase hexadecimal.", null));
            return;
        }
        if (resource.isText()) {
            if (resource.getSize() > limits.getMaxTextFileBytes()) {
                report.add(error("TEXT_FILE_SIZE_LIMIT", path,
                        "Text Skill resource exceeds the configured size limit.", null));
            }
            if (resource.getContentRef() != null) {
                report.add(error("AMBIGUOUS_RESOURCE_CONTENT", path,
                        "Text resources cannot also contain a binary content reference.", null));
            }
            if (resource.getSize() != textBytes.length) {
                report.add(error("RESOURCE_SIZE_MISMATCH", path,
                        "Skill resource size does not match UTF-8 content.", null));
            }
            if (!resource.getContentHash().equals(SkillHashes.sha256Hex(textBytes))) {
                report.add(error("RESOURCE_HASH_MISMATCH", path,
                        "Skill resource hash does not match text content.", null));
            }
        } else {
            if (resource.getSize() > limits.getMaxBinaryFileBytes()) {
                report.add(error("BINARY_FILE_SIZE_LIMIT", path,
                        "Binary Skill resource exceeds the configured size limit.", null));
            }
            if (isBlank(resource.getContentRef())) {
                report.add(error("CONTENT_REF_REQUIRED", path,
                        "Binary Skill resource content reference is required.", null));
            } else if (!resource.getContentRef().equals("sha256:" + resource.getContentHash())) {
                report.add(error("CONTENT_REF_MISMATCH", path,
                        "Binary Skill resource content reference must match its SHA-256.", null));
            }
        }
    }

    private static void validateAggregateLimits(Skill skill, List<SkillResource> resources,
                                                SkillValidationReport report, SkillPackageLimits limits) {
        if (!isBlank(skill.getName())) {
            String skillPath = skill.getName() + "/" + SkillPaths.SKILL_FILE;
            if (skillPath.length() > limits.getMaxPathLength()) {
                report.add(error("PATH_LENGTH_LIMIT", skillPath,
                        "Skill entry path exceeds the configured length limit.", null));
            }
            if (SkillPaths.depth(skillPath) > limits.getMaxPathDepth()) {
                report.add(error("PATH_DEPTH_LIMIT", skillPath,
                        "Skill entry path exceeds the configured depth limit.", null));
            }
        }
        if (resources.size() + 1 > limits.getMaxEntryCount()) {
            report.add(error("ENTRY_COUNT_LIMIT", null,
                    "Skill contains too many files.", null));
        }
        long total = 0;
        if (skill.getSkillContent() != null) {
            try {
                total = SkillUtf8.byteLength(skill.getSkillContent());
            } catch (CharacterCodingException ignored) {
                // parseDocument 已返回可定位的 INVALID_UTF8；非法文本不参与后续大小计算。
            }
        }
        if (total > limits.getMaxTextFileBytes()) {
            report.add(error("TEXT_FILE_SIZE_LIMIT", SkillPaths.SKILL_FILE,
                    "SKILL.md exceeds the configured size limit.", null));
        }
        for (SkillResource resource : resources) {
            if (resource == null || resource.getSize() < 0) {
                continue;
            }
            try {
                total = Math.addExact(total, resource.getSize());
            } catch (ArithmeticException e) {
                total = Long.MAX_VALUE;
            }
            if (total > limits.getMaxTotalUncompressedBytes()) {
                report.add(error("TOTAL_SIZE_LIMIT", resource.getPath(),
                        "Skill resources exceed the configured total size limit.", null));
                break;
            }
        }
    }

    /**
     * 查找当前文件路径与已索引文件之间的祖先或后代冲突。
     *
     * @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 SkillValidationIssue error(String code, String path, String message, String suggestion) {
        return new SkillValidationIssue(code, SkillValidationSeverity.ERROR, path, null, null,
                message, suggestion);
    }

    private static SkillValidationIssue errorAt(String code, String path, SkillSourceLocation location,
                                                String message, String suggestion) {
        return new SkillValidationIssue(code, SkillValidationSeverity.ERROR, path,
                line(location), column(location), message, suggestion);
    }

    private static SkillSourceLocation sourceLocation(SkillDocument document, String key) {
        return document == null ? null : document.getFrontmatterLocation(key);
    }

    private static Integer line(SkillSourceLocation location) {
        return location == null ? null : location.getLine();
    }

    private static Integer column(SkillSourceLocation location) {
        return location == null ? null : location.getColumn();
    }

    private static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }

    private static String frontmatterString(SkillDocument document, String key) {
        if (document == null) {
            return null;
        }
        Object value = document.getFrontmatter().get(key);
        return value instanceof String text ? text : null;
    }
}
