#!/usr/bin/env python3
"""Validate this portable skill's structure; no network or third-party packages."""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path
from urllib.parse import unquote, urlsplit

ROOT = Path(__file__).resolve().parents[1]
REQUIRED = (
    "SKILL.md", "README.md", "VERIFICATION.md", "evals/scenarios.json",
    "references/design-and-interaction.md", "references/architecture-and-effects.md",
    "references/layout-and-rendering.md", "references/versions-and-components.md",
    "references/testing-and-release.md", "references/case-studies.md",
    "references/sources.md", "examples/contracts/README.md",
    "examples/contracts/go.mod", "examples/contracts/patterns.go",
    "examples/contracts/patterns_test.go",
    "examples/WORKED-EXAMPLES.md", "examples/worked/README.md",
    "examples/worked/go.mod", "scripts/test_offline_examples.py",
    "examples/worked/cmd/picker/main.go", "examples/worked/cmd/picker/model.go",
    "examples/worked/cmd/search/main.go", "examples/worked/cmd/search/model.go",
    "examples/worked/cmd/tail/main.go", "examples/worked/cmd/tail/model.go",
    "examples/worked/internal/display/render.go",
)


def heading_ids(text: str) -> set[str]:
    """Handle the ordinary Markdown headings used by this bundle."""
    result: set[str] = set()
    for line in text.splitlines():
        match = re.match(r"^#{1,6}\s+(.+)$", line)
        if match:
            value = re.sub(r"[^\w\s-]", "", match.group(1).strip().lower())
            result.add(re.sub(r"\s", "-", value))
    return result


def validate() -> tuple[list[str], int, int, int]:
    errors: list[str] = []
    for rel in REQUIRED:
        if not (ROOT / rel).is_file():
            errors.append(f"Missing required file: {rel}")
    skill_path = ROOT / "SKILL.md"
    if not skill_path.is_file():
        return errors, 0, 0, 0
    skill = skill_path.read_text(encoding="utf-8")
    match = re.match(r"\A---\n(.*?)\n---\n", skill, re.S)
    if not match:
        errors.append("SKILL.md requires a leading YAML frontmatter block")
    else:
        fields = dict(re.findall(r"^([a-z-]+): (.+)$", match.group(1), re.M))
        name, description = fields.get("name", ""), fields.get("description", "")
        if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64:
            errors.append("Invalid skill name")
        if name != ROOT.name:
            errors.append("Skill name and directory name differ")
        if not description or len(description) > 1024:
            errors.append("Description must have 1–1024 characters")
    md_files = sorted(ROOT.rglob("*.md"))
    checked_links = 0
    for path in md_files:
        text = path.read_text(encoding="utf-8")
        if len(re.findall(r"^```", text, re.M)) % 2:
            errors.append(f"Unbalanced code fences: {path.relative_to(ROOT)}")
        for target in re.findall(r"\[[^\]\n]+\]\(([^)\s]+)\)", text):
            url = urlsplit(target)
            if url.scheme or url.netloc:
                continue  # Remote availability is intentionally not checked.
            dest = (path.parent / unquote(url.path)).resolve() if url.path else path
            checked_links += 1
            if not dest.is_relative_to(ROOT):
                errors.append(f"Link escapes skill root: {target}")
            elif not dest.is_file():
                errors.append(f"Broken local link in {path.name}: {target}")
            elif url.fragment and dest.suffix == ".md":
                if unquote(url.fragment) not in heading_ids(dest.read_text(encoding="utf-8")):
                    errors.append(f"Unknown Markdown anchor in {path.name}: {target}")
    scenario_count = 0
    try:
        data = json.loads((ROOT / "evals/scenarios.json").read_text(encoding="utf-8"))
        if data.get("skill") != "charm-tui" or data.get("format_version") != 1:
            errors.append("Evaluation metadata mismatch")
        ids: set[str] = set()
        for case in data["scenarios"]:
            if not isinstance(case.get("id"), str) or case["id"] in ids:
                errors.append("Invalid or duplicate evaluation ID")
            ids.add(case["id"])
            for key in ("setup", "prompt"):
                if not isinstance(case.get(key), str) or not case[key].strip():
                    errors.append(f"Missing evaluation {key}: {case['id']}")
            for key in ("must_pass", "must_not"):
                value = case.get(key)
                if not isinstance(value, list) or not value or not all(
                    isinstance(item, str) and item.strip() for item in value
                ):
                    errors.append(f"Invalid evaluation {key}: {case['id']}")
            scenario_count += 1
    except (OSError, ValueError, KeyError, TypeError) as exc:
        errors.append(f"Cannot validate evaluation scenarios: {exc}")
    return errors, len(md_files), checked_links, scenario_count


if __name__ == "__main__":
    problems, documents, links, scenarios = validate()
    if problems:
        for problem in problems:
            print(f"FAIL: {problem}", file=sys.stderr)
        raise SystemExit(1)
    print(f"PASS: {documents} Markdown files; {links} local links; {scenarios} evaluation scenarios")
    print("PASS: required files, frontmatter, code fences, and evaluation structure")
    print("Not checked: remote URLs, agent performance, Charm compilation, or real-terminal behavior")
