#!/usr/bin/env python3
"""Lightweight AGENTS.md validator.

Checks for common problems before an agent guide is treated as done:
missing recommended sections, placeholders, nonexistent referenced paths, and
package manager commands that do not map to package.json scripts.
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path


PLACEHOLDER_RE = re.compile(
    r"\b(TODO|TBD|FIXME|XXX|REVIEW_NEEDED)\b|"
    r"\{\{[^}]+\}\}|"
    r"<[^>\n]+>|"
    r"\[[^\]\n]*(exact command|path|project|真实|项目类型|关键)[^\]\n]*\]",
    re.IGNORECASE,
)

CODE_SPAN_RE = re.compile(r"`([^`\n]+)`")
HEADING_RE = re.compile(r"^##\s+(.+?)\s*$", re.MULTILINE)

ROOT_RECOMMENDED_HEADINGS = {
    "Project Overview",
    "Scope",
    "Quick Commands",
    "Key Paths",
    "Definition of Done",
    "Boundaries",
}

LOCAL_RECOMMENDED_HEADINGS = {
    "Local Purpose",
    "Inherits",
    "Local Conventions",
    "Local Risks",
}

PATH_HINT_RE = re.compile(r"(^\.{1,2}[\\/]|[\\/]|^[\w.-]+\.[A-Za-z0-9]+$)")
COMMAND_PREFIX_RE = re.compile(
    r"^(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)(?:\s|$)"
)


def load_package_scripts(repo: Path) -> dict[str, set[str]]:
    scripts: dict[str, set[str]] = {}
    for package_file in repo.rglob("package.json"):
        if "node_modules" in package_file.parts:
            continue
        try:
            data = json.loads(package_file.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        package_scripts = data.get("scripts")
        if isinstance(package_scripts, dict):
            scripts[str(package_file.relative_to(repo))] = set(package_scripts)
    return scripts


def normalize_path_token(token: str) -> str | None:
    if token.startswith(("http://", "https://", "$")):
        return None
    if any(part in token for part in ("&&", "||", " --", " -")):
        return None
    if any(char in token for char in "*?[]"):
        return None
    if token in {"AGENTS.md", "CLAUDE.md", "README.md"}:
        return token
    if PATH_HINT_RE.search(token):
        return token.rstrip("/")
    return None


def path_exists(repo: Path, token: str) -> bool:
    candidate = (repo / token).resolve()
    try:
        candidate.relative_to(repo.resolve())
    except ValueError:
        return False
    return candidate.exists()


def validate(args: argparse.Namespace) -> tuple[list[str], list[str]]:
    repo = Path(args.repo).resolve()
    guide = Path(args.file).resolve()
    errors: list[str] = []
    warnings: list[str] = []

    if not repo.exists():
        return [f"Repository path does not exist: {repo}"], warnings
    if not guide.exists():
        return [f"Guide file does not exist: {guide}"], warnings

    text = guide.read_text(encoding="utf-8")
    headings = set(HEADING_RE.findall(text))

    recommended = LOCAL_RECOMMENDED_HEADINGS if args.local else ROOT_RECOMMENDED_HEADINGS
    missing = sorted(recommended - headings)
    if missing:
        warnings.append("Missing recommended headings: " + ", ".join(missing))

    placeholders = sorted(set(match.group(0) for match in PLACEHOLDER_RE.finditer(text)))
    if placeholders:
        errors.append("Placeholder or review marker remains: " + ", ".join(placeholders[:10]))

    code_spans = CODE_SPAN_RE.findall(text)
    for token in code_spans:
        path_token = normalize_path_token(token.strip())
        if path_token and not path_exists(repo, path_token):
            warnings.append(f"Referenced path may not exist: `{token}`")

    package_scripts = load_package_scripts(repo)
    known_scripts = set().union(*package_scripts.values()) if package_scripts else set()
    for token in code_spans:
        match = COMMAND_PREFIX_RE.match(token.strip())
        if not match:
            continue
        script_name = match.group(1)
        if script_name in {"install", "add", "remove", "exec", "dlx", "create"}:
            continue
        if known_scripts and script_name not in known_scripts:
            warnings.append(
                f"Package command references missing package.json script: `{token}`"
            )

    return errors, warnings


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate AGENTS.md guardrails.")
    parser.add_argument("--repo", required=True, help="Repository root to validate against.")
    parser.add_argument("--file", required=True, help="AGENTS.md file to validate.")
    parser.add_argument("--local", action="store_true", help="Validate a child/local guide.")
    parser.add_argument(
        "--warnings-as-errors",
        action="store_true",
        help="Exit nonzero when warnings are present.",
    )
    args = parser.parse_args()

    errors, warnings = validate(args)
    for error in errors:
        print(f"ERROR: {error}", file=sys.stderr)
    for warning in warnings:
        print(f"WARNING: {warning}", file=sys.stderr)

    if errors or (args.warnings_as_errors and warnings):
        return 1
    print("AGENTS.md validation passed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
