#!/usr/bin/env python3
"""
Validate a SophClaw skill for compliance.

Checks structural integrity (ERROR level) and content quality (WARNING level).
Can be run after generation or after editing SKILL.md for re-validation.

Usage:
    uv run {baseDir}/scripts/validate.py skills/<skill-name>
"""

import json
import re
import sys
from pathlib import Path

try:
    import yaml
except ImportError:
    yaml = None


MAX_DESCRIPTION_LENGTH = 200
MAX_CODE_BLOCK_LINES = 15


def validate_skill(skill_path):
    """Validate a skill directory. Returns (errors, warnings)."""
    errors = []
    warnings = []

    skill_path = Path(skill_path).resolve()
    skill_name = skill_path.name

    # ---- Structural checks ----

    # skill.json exists
    skill_json_path = skill_path / "skill.json"
    if not skill_json_path.exists():
        errors.append("skill.json not found in {}".format(skill_path))
        return errors, warnings

    # skill.json is valid JSON
    try:
        skill_json_content = skill_json_path.read_text(encoding="utf-8")
        skill_data = json.loads(skill_json_content)
    except json.JSONDecodeError as e:
        errors.append("skill.json is not valid JSON: {}".format(e))
        return errors, warnings

    # skill.json required fields
    required_fields = ["name", "version", "types", "changelog", "createdAt", "updatedAt"]
    for field in required_fields:
        if field not in skill_data:
            errors.append("skill.json missing required field: {}".format(field))

    if errors:
        return errors, warnings

    # name consistency: directory name == skill.json name
    json_name = skill_data.get("name", "")
    if json_name != skill_name:
        errors.append(
            "Name mismatch: directory is '{}' but skill.json name is '{}'".format(
                skill_name, json_name
            )
        )

    # types validation
    types = skill_data.get("types", [])
    if not isinstance(types, list) or len(types) == 0:
        errors.append("skill.json 'types' must be a non-empty array")
    else:
        for t in types:
            if t not in ("builtin", "store", "private", "tmp"):
                errors.append("Invalid type '{}' in skill.json. Must be: builtin, store, private, tmp".format(t))

    # store type requires displayName and description (Chinese)
    if "store" in types:
        display_name = skill_data.get("displayName", "")
        if not display_name.strip():
            errors.append("store type requires displayName (Chinese) in skill.json")
        skill_desc = skill_data.get("description", "")
        if not skill_desc.strip():
            errors.append("store type requires description (Chinese) in skill.json")

    # src/SKILL.md exists
    skill_md_path = skill_path / "src" / "SKILL.md"
    if not skill_md_path.exists():
        errors.append("src/SKILL.md not found")
        return errors, warnings

    # SKILL.md frontmatter
    skill_md_content = skill_md_path.read_text(encoding="utf-8")
    if not skill_md_content.startswith("---"):
        errors.append("SKILL.md missing YAML frontmatter")
        return errors, warnings

    frontmatter, fm_error = _parse_frontmatter(skill_md_content)
    if fm_error:
        errors.append(fm_error)
        return errors, warnings

    # SKILL.md required frontmatter fields
    if "name" not in frontmatter:
        errors.append("SKILL.md frontmatter missing 'name'")
    if "description" not in frontmatter:
        errors.append("SKILL.md frontmatter missing 'description'")

    # SKILL.md must NOT have version field
    if frontmatter and "version" in frontmatter:
        errors.append("SKILL.md frontmatter should not contain 'version' field (managed by skill.json)")

    # name consistency: SKILL.md name == skill.json name
    if frontmatter:
        md_name = frontmatter.get("name", "")
        if md_name and md_name != json_name:
            errors.append(
                "Name mismatch: skill.json name is '{}' but SKILL.md name is '{}'".format(
                    json_name, md_name
                )
            )

    # pyproject.toml exists
    pyproject_path = skill_path / "src" / "pyproject.toml"
    if not pyproject_path.exists():
        errors.append("src/pyproject.toml not found")
    else:
        # pyproject.toml version consistency
        try:
            pptoml_ver = _read_pyproject_version(pyproject_path)
            json_ver = skill_data.get("version", "")
            if pptoml_ver and json_ver and pptoml_ver != json_ver:
                errors.append(
                    "Version mismatch: skill.json is '{}' but pyproject.toml is '{}'".format(
                        json_ver, pptoml_ver
                    )
                )
        except (OSError, ValueError, IOError):
            pass  # can't parse pyproject.toml version, skip

    # ---- Content suggestions (warnings) ----

    if not frontmatter:
        return errors, warnings

    # Description length
    description = str(frontmatter.get("description", ""))
    if len(description) > MAX_DESCRIPTION_LENGTH:
        warnings.append(
            "description is {} chars (recommended <= {}). Long descriptions reduce LLM matching efficiency.".format(
                len(description), MAX_DESCRIPTION_LENGTH
            )
        )

    # Description contains trigger-indicating words (belongs in description, not body)
    body = _get_body(skill_md_content)

    # Check for src/ in paths (only flag when {baseDir}/src/ is used)
    if re.search(r"\{baseDir\}/src/", body):
        warnings.append(
            "SKILL.md may contain 'src/' in paths. Use runtime paths (e.g., {baseDir}/scripts/xxx.py without src/)."
        )

    # Check for python (not uv run) execution
    if re.search(r"\bpython\s+\{baseDir\}/scripts/", body):
        warnings.append("Use 'uv run' instead of 'python' to execute scripts.")

    # Check for references to other skill names (heuristic: "skill" + skill-like patterns)
    # We look for common patterns like "similar to X", "like X skill", "配合 X"
    cross_ref_patterns = [
        r"(?:类似|像|参考|配合|结合|参见|similar to|like the|works with)\s+\S+\s*(?:skill|Skill)",
        r"(?:参照|同|跟)\s*\S+\s*(?:skill|Skill)",
    ]
    for pattern in cross_ref_patterns:
        if re.search(pattern, body):
            warnings.append(
                "SKILL.md may reference another skill. Skills should be self-contained "
                "and not mention other skills."
            )
            break

    # Check for large code blocks (>15 lines) in SKILL.md
    code_blocks = re.findall(r"```[\s\S]*?```", body)
    for block in code_blocks:
        lines = block.count("\n")
        if lines > MAX_CODE_BLOCK_LINES:
            warnings.append(
                "SKILL.md contains a code block with {} lines (recommended <= {}). "
                "Implementation details should be in scripts/, not SKILL.md.".format(
                    lines, MAX_CODE_BLOCK_LINES
                )
            )
            break

    # Check for implementation detail indicators in SKILL.md
    impl_indicators = [
        r"\bdef\s+\w+\s*\(",       # function definitions
        r"\bclass\s+\w+",           # class definitions
        r"\bimport\s+\w+",          # import statements
    ]
    for pattern in impl_indicators:
        if re.search(pattern, body):
            warnings.append(
                "SKILL.md appears to contain implementation code (function/class/import). "
                "Move implementation details to scripts/."
            )
            break

    return errors, warnings


def _parse_frontmatter(content):
    """Parse YAML frontmatter from SKILL.md content."""
    match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
    if not match:
        return None, "Invalid frontmatter format in SKILL.md"

    frontmatter_text = match.group(1)

    if yaml:
        try:
            frontmatter = yaml.safe_load(frontmatter_text)
            if not isinstance(frontmatter, dict):
                return None, "SKILL.md frontmatter must be a YAML dictionary"
            return frontmatter, None
        except yaml.YAMLError as e:
            return None, "Invalid YAML in SKILL.md frontmatter: {}".format(e)
    else:
        # Fallback: basic key-value parsing without PyYAML
        frontmatter = {}
        for line in frontmatter_text.strip().split("\n"):
            kv = line.split(":", 1)
            if len(kv) == 2:
                key = kv[0].strip()
                value = kv[1].strip().strip('"').strip("'")
                frontmatter[key] = value
        return frontmatter, None


def _get_body(content):
    """Extract body from SKILL.md (everything after frontmatter)."""
    match = re.match(r"^---\n.*?\n---\n?(.*)", content, re.DOTALL)
    if match:
        return match.group(1)
    return content


def _read_pyproject_version(path):
    """Extract version from pyproject.toml."""
    content = path.read_text(encoding="utf-8")
    match = re.search(r'version\s*=\s*"([^"]+)"', content)
    if match:
        return match.group(1)
    return None


def main():
    if len(sys.argv) != 2:
        print("Usage: uv run {{baseDir}}/scripts/validate.py <skill-path>")
        print("Example: uv run {{baseDir}}/scripts/validate.py skills/my-skill")
        sys.exit(1)

    skill_path = sys.argv[1]

    if not Path(skill_path).exists():
        print("## 结构检查")
        print("ERROR: Path not found: {}".format(skill_path))
        print("STATUS=failed")
        print("ERRORS=1")
        print("WARNINGS=0")
        sys.exit(1)

    errors, warnings = validate_skill(skill_path)

    # Output results
    print("## 结构检查 (ERROR)")
    if errors:
        for e in errors:
            print("ERROR: {}".format(e))
    else:
        print("All structural checks passed.")

    print("")
    print("## 内容建议 (WARNING)")
    if warnings:
        for w in warnings:
            print("WARNING: {}".format(w))
    else:
        print("No content suggestions.")

    print("")
    error_count = len(errors)
    warning_count = len(warnings)
    print("STATUS={}".format("succeeded" if error_count == 0 else "failed"))
    print("ERRORS={}".format(error_count))
    print("WARNINGS={}".format(warning_count))

    sys.exit(0 if error_count == 0 else 1)


if __name__ == "__main__":
    main()