#!/usr/bin/env python3
"""Validate skill folders and required frontmatter."""

from __future__ import annotations

import os
import re
import subprocess
import sys

REPO_ROOT = os.environ.get("APX_REPO_ROOT") or os.path.dirname(
    os.path.dirname(os.path.abspath(__file__))
)
SKILLS_DIR = os.path.join(REPO_ROOT, "skills")
REQUIRED_FRONTMATTER_FIELDS = ["name", "description"]
DISALLOWED_TOP_LEVEL_SECTION_TAGS = [
    "Purpose",
    "Workflow",
    "Use_When",
    "Do_Not_Use_When",
    "Why_This_Exists",
    "PRD_Mode",
    "Execution_Policy",
    "Steps",
    "Escalation_And_Stop_Conditions",
    "Final_Checklist",
]
DISALLOWED_TOP_LEVEL_SECTION_RE = re.compile(
    r"^\s*</?(?P<tag>"
    + "|".join(re.escape(tag) for tag in DISALLOWED_TOP_LEVEL_SECTION_TAGS)
    + r")(?:\s[^>]*)?>\s*$",
    re.MULTILINE,
)
RECOMMENDED_SECTIONS = [
    "Purpose",
    "When to Use",
    "Inputs",
    "Workflow",
    "Output",
    "Verification",
    "Failure Modes",
]

errors: list[str] = []
warnings: list[str] = []
STYLE_WARNINGS = "--style" in sys.argv


def repo_rel_path(path: str) -> str:
    return os.path.relpath(path, REPO_ROOT).replace(os.sep, "/")


def is_gitignored(path: str) -> bool:
    rel_path = repo_rel_path(path)
    try:
        result = subprocess.run(
            ["git", "check-ignore", "-q", "--", rel_path],
            cwd=REPO_ROOT,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            check=False,
        )
    except OSError:
        return False
    return result.returncode == 0


def referenced_support_files(content: str) -> list[str]:
    refs: set[str] = set()
    for match in re.finditer(
        r"`([^`\s\r\n]+\.(?:md|ps1|sh|ts|js|py|json|toml|ya?ml))`",
        content,
        re.IGNORECASE,
    ):
        ref = match.group(1).replace("\\", "/")
        ref = re.sub(r"^\./", "", ref)
        if ref.startswith(("references/", "examples/")) or "/" not in ref:
            refs.add(ref)
    return sorted(refs)


def support_ref_exists(skill_dir: str, ref: str) -> bool:
    if "/" in ref:
        candidates = [os.path.join(skill_dir, *ref.split("/"))]
    else:
        candidates = [
            os.path.join(skill_dir, ref),
            os.path.join(skill_dir, "references", ref),
            os.path.join(skill_dir, "examples", ref),
        ]
    return any(os.path.exists(candidate) for candidate in candidates)


def strip_fenced_code(content: str) -> str:
    return re.sub(r"```.*?```", "", content, flags=re.DOTALL)


def parse_frontmatter(content: str) -> dict[str, str] | None:
    if not content.startswith("---"):
        return None
    match = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", content, re.DOTALL)
    if not match:
        return None

    fields: dict[str, str] = {}
    for line in match.group(1).splitlines():
        if ":" not in line:
            continue
        key, _, value = line.partition(":")
        fields[key.strip()] = value.strip()
    return fields


def check_skill(skill_dir: str) -> None:
    skill_name = os.path.basename(skill_dir)
    skill_md = os.path.join(skill_dir, "SKILL.md")

    if not os.path.isfile(skill_md):
        errors.append(f"[{skill_name}] missing SKILL.md")
        return

    with open(skill_md, encoding="utf-8") as handle:
        content = handle.read()

    if not content.strip():
        errors.append(f"[{skill_name}] SKILL.md is empty")
        return

    frontmatter = parse_frontmatter(content)
    if frontmatter is None:
        errors.append(f"[{skill_name}] SKILL.md missing YAML frontmatter")
        return

    for field in REQUIRED_FRONTMATTER_FIELDS:
        if not frontmatter.get(field, "").strip():
            errors.append(f"[{skill_name}] frontmatter missing required field: {field}")

    if not re.sub(r"\s", "", content):
        errors.append(f"[{skill_name}] SKILL.md has no content")

    prose = strip_fenced_code(content)
    if not prose.strip():
        errors.append(f"[{skill_name}] SKILL.md has no prose content")

    disallowed_tags = sorted(
        {match.group("tag") for match in DISALLOWED_TOP_LEVEL_SECTION_RE.finditer(prose)}
    )
    for tag in disallowed_tags:
        errors.append(
            f"[{skill_name}] SKILL.md uses XML-like top-level section tag <{tag}>; use Markdown headings instead"
        )

    if frontmatter.get("name") and frontmatter["name"] != skill_name:
        errors.append(
            f"[{skill_name}] frontmatter name does not match directory name: {frontmatter['name']}"
        )

    if STYLE_WARNINGS:
        for section in RECOMMENDED_SECTIONS:
            pattern = rf"^##\s+{re.escape(section)}\s*$"
            if not re.search(pattern, content, re.MULTILINE):
                warnings.append(f"[{skill_name}] missing recommended section: ## {section}")

    for ref in referenced_support_files(content):
        if not support_ref_exists(skill_dir, ref):
            errors.append(f"[{skill_name}] missing referenced support file: {ref}")


def main() -> int:
    if not os.path.isdir(SKILLS_DIR):
        print(f"ERROR: skills directory not found at {SKILLS_DIR}")
        return 1

    skill_dirs = [
        os.path.join(SKILLS_DIR, entry)
        for entry in sorted(os.listdir(SKILLS_DIR))
        if os.path.isdir(os.path.join(SKILLS_DIR, entry))
        and not is_gitignored(os.path.join(SKILLS_DIR, entry))
    ]

    if not skill_dirs:
        print("ERROR: no skill directories found")
        return 1

    for skill_dir in skill_dirs:
        check_skill(skill_dir)

    if warnings:
        print(f"Warnings ({len(warnings)}):")
        for warning in warnings:
            print(f"  WARN  {warning}")
        print()

    if errors:
        print(f"Errors ({len(errors)}):")
        for error in errors:
            print(f"  ERROR {error}")
        print()
        print(
            f"Checked {len(skill_dirs)} skills. {len(errors)} error(s), {len(warnings)} warning(s). FAILED."
        )
        return 1

    print(f"Checked {len(skill_dirs)} skills. {len(warnings)} warning(s). OK.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
