#!/usr/bin/env python3
"""Manifest AST validator — proves every canonical path in ssot_manifest.yaml exists.

Prevents manifest rot: if a function is renamed or deleted without updating the
YAML, the validator fails and the harness gate aborts.

Usage:
    python recoil/architecture/tools/validate_manifest_ast.py \
        --manifest recoil/architecture/ssot_manifest.yaml

Exit codes:
    0 = all canonical paths found in codebase
    1 = one or more paths missing or unresolvable
    2 = manifest file invalid or unparseable
"""
from __future__ import annotations

import argparse
import ast
import glob
import sys
from pathlib import Path

# Exclusion patterns — never validate inside these
EXCLUDED_PATTERNS = [
    "*/tests/*",
    "*mock*.py",
    "*fixture*.py",
    "*_archive*",
    "*/.worktrees/*",
    "*conflicted copy*",
    "*_test.py",
    "*/test_*",
]

# Root from which module paths are resolved
REPO_ROOT = Path(__file__).resolve().parents[3]  # .../CLAUDE_PROJECTS


def _is_excluded(path: Path) -> bool:
    path_str = str(path)
    for pat in EXCLUDED_PATTERNS:
        if glob.fnmatch.fnmatch(path_str, pat):
            return True
    return False


def _check_conflict_files(manifest_dir: Path) -> list[str]:
    """Return list of Dropbox conflict file paths in the architecture dir."""
    conflicts = []
    for p in manifest_dir.parent.rglob("*conflicted copy*"):
        conflicts.append(str(p))
    return conflicts


def _parse_capability_path(cap_path: str) -> tuple[str, str | None] | None:
    """Parse 'module/path.py::function_or_class' → (file_path, symbol_name).
    Returns None if path is a note/comment (contains spaces or parens).
    """
    if " " in cap_path or "(" in cap_path:
        return None  # narrative note, not a real path
    if "::" not in cap_path:
        # Module-only path (e.g., ref_resolver.py itself)
        return cap_path, None
    parts = cap_path.split("::", 1)
    return parts[0], parts[1]


def _find_symbol_in_ast(file_path: Path, symbol: str) -> bool:
    """Return True if `symbol` is defined as a FunctionDef or ClassDef in file_path."""
    try:
        source = file_path.read_text(encoding="utf-8")
        tree = ast.parse(source, filename=str(file_path))
    except (SyntaxError, OSError):
        return False

    # Handle Class.method syntax
    if "." in symbol:
        class_name, method_name = symbol.split(".", 1)
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef) and node.name == class_name:
                for item in node.body:
                    if isinstance(item, ast.FunctionDef) and item.name == method_name:
                        return True
        return False

    # Simple function or class
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            if node.name == symbol:
                return True
    return False


def validate_manifest(manifest_path: Path, verbose: bool = False) -> bool:
    try:
        import yaml
    except ImportError:
        print("ERROR: pyyaml not installed. Run: pip install pyyaml", file=sys.stderr)
        sys.exit(2)

    # Check for Dropbox conflict files first
    conflicts = _check_conflict_files(manifest_path)
    if conflicts:
        print(f"ERROR: Dropbox conflict file(s) detected in architecture dir:", file=sys.stderr)
        for c in conflicts:
            print(f"  {c}", file=sys.stderr)
        print("Resolve conflicts before running the harness.", file=sys.stderr)
        sys.exit(1)

    try:
        manifest = yaml.safe_load(manifest_path.read_text())
    except Exception as e:
        print(f"ERROR: Cannot parse manifest: {e}", file=sys.stderr)
        sys.exit(2)

    capabilities = manifest.get("capabilities", {})
    if not capabilities:
        print("ERROR: Manifest has no capabilities", file=sys.stderr)
        sys.exit(2)

    errors = []
    for cap_name, cap_data in capabilities.items():
        # Skip tombstoned — the canonical function may have been deleted
        if cap_data.get("state") == "tombstoned":
            if verbose:
                print(f"  SKIP (tombstoned): {cap_name}")
            continue

        # Skip building — pre-registration stub for in-flight harness builds.
        # The canonical symbol is being established in the same build; a later
        # phase promotes state to active. See ssot_manifest stub-registration
        # convention (project-paths-refactor-v2 BUILD_SPEC Phase 0).
        if cap_data.get("state") == "building":
            if verbose:
                print(f"  SKIP (building): {cap_name}")
            continue

        # Skip runtime_only entries
        if cap_data.get("runtime_only"):
            if verbose:
                print(f"  SKIP (runtime_only): {cap_name}")
            continue

        canonical = cap_data.get("canonical", "")
        parsed = _parse_capability_path(canonical)
        if parsed is None:
            if verbose:
                print(f"  SKIP (note/comment): {cap_name} → {canonical}")
            continue

        file_rel, symbol = parsed
        file_path = REPO_ROOT / file_rel

        if not file_path.exists():
            errors.append(f"  MISSING FILE: {cap_name} → {file_rel}")
            continue

        if _is_excluded(file_path):
            if verbose:
                print(f"  SKIP (excluded): {cap_name} → {file_rel}")
            continue

        if symbol is None:
            # Module-only path — file existing is sufficient
            if verbose:
                print(f"  PASS (module): {cap_name} → {file_rel}")
            continue

        if not _find_symbol_in_ast(file_path, symbol):
            errors.append(f"  MISSING SYMBOL: {cap_name} → {file_rel}::{symbol}")
        else:
            if verbose:
                print(f"  PASS: {cap_name} → {file_rel}::{symbol}")

    if errors:
        print(f"Manifest validation FAILED — {len(errors)} error(s):", file=sys.stderr)
        for e in errors:
            print(e, file=sys.stderr)
        return False

    print(f"Manifest validation PASS — {len(capabilities)} capabilities checked")
    return True


def main() -> int:
    p = argparse.ArgumentParser(description="Validate ssot_manifest.yaml against codebase AST")
    p.add_argument("--manifest", default="recoil/architecture/ssot_manifest.yaml",
                   help="Path to manifest YAML")
    p.add_argument("--verbose", "-v", action="store_true")
    args = p.parse_args()

    manifest_path = Path(args.manifest)
    if not manifest_path.exists():
        print(f"ERROR: Manifest not found: {manifest_path}", file=sys.stderr)
        return 2

    ok = validate_manifest(manifest_path, verbose=args.verbose)
    return 0 if ok else 1


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