#!/usr/bin/env python3
""" 
SFT Audit: Witness Obstruction (WOB)

Checks:
- required fields exist
- obstruction_code matches canonical pattern
- obstruction_code is one of the canonical catalog codes (declared-regime completeness contract)
- evidence and recommended_next_moves are non-empty lists
"""

import json
import os
import re
import sys
from pathlib import Path

PAT = re.compile(r"^WOB-[0-9]{2}(-[a-z0-9]+)*$")

def _ref_to_path(ref: str) -> str:
    """Return a candidate filesystem path for a reference string.

    Convention: references may include anchors like 'path/to/file:section' or
    'path/to/file#anchor'. This audit only checks existence of the left path.
    """
    r = (ref or "").strip()
    if not r:
        return ""
    if r.startswith("http://") or r.startswith("https://") or r.startswith("doi:"):
        return ""
    if ":" in r:
        r = r.split(":", 1)[0].strip()
    if "#" in r:
        r = r.split("#", 1)[0].strip()
    return r


def project_root() -> Path:
    here = Path(__file__).resolve()
    return (here.parent / "../..").resolve()


def _resolve_repo_path(root: Path, p: str) -> Path:
    cand = (root / p).resolve()
    if cand.exists():
        return cand
    if p.startswith("finish_ready/"):
        cand2 = (root / p[len("finish_ready/"):]).resolve()
        if cand2.exists():
            return cand2
    return cand


def check_ref_paths(refs: list[str], label: str) -> None:
    root = project_root()
    for ref in refs:
        if not isinstance(ref, str):
            fail(f"{label}: reference is not a string: {ref!r}")
        p = _ref_to_path(ref)
        if not p:
            continue
        if "/" not in p and "." not in p:
            continue
        cand = _resolve_repo_path(root, p)
        if not cand.exists():
            fail(f"{label}: referenced file not found: {p}")


def _anchor_of(ref: str) -> str:
    r = (ref or "").strip()
    if "#" in r:
        return r.split("#", 1)[1].strip()
    if ":" in r:
        return r.split(":", 1)[1].strip()
    return ""


def check_anchor_exists(ref: str) -> None:
    """Optional strict check: ensure the anchor exists in the referenced file."""
    p = _ref_to_path(ref)
    a = _anchor_of(ref)
    if not p or not a:
        fail(f"strict anchors: theorem_ref must include file plus anchor: {ref}")
    root = project_root()
    fp = _resolve_repo_path(root, p)
    try:
        txt = fp.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        fail(f"strict anchors: cannot read referenced file: {p}")
    # TeX label convention
    if p.endswith(".tex") and (a.startswith("thm:") or a.startswith("lem:") or a.startswith("cor:") or a.startswith("def:") or a.startswith("eq:")):
        if f"\\\\label{{{a}}}" not in txt:
            fail(f"strict anchors: missing \\\\label{{{a}}} in {p}")
    else:
        # generic substring check (Markdown headings or textual anchors)
        if a not in txt:
            fail(f"strict anchors: anchor '{a}' not found in {p}")



def fail(msg: str) -> None:
    print("FAIL:", msg)
    sys.exit(1)


def load_canonical_codes() -> set[str]:
    here = Path(__file__).resolve()
    codebook = (here.parent / "../contracts/wob_codebook.json").resolve()
    if not codebook.exists():
        fail(f"Missing codebook: {codebook}")
    data = json.loads(codebook.read_text(encoding="utf-8"))
    codes = set()
    for item in data.get("codes", []):
        c = str(item.get("code", "")).strip()
        if c:
            codes.add(c)
    if not codes:
        fail("Codebook has no codes")
    return codes


CANONICAL_CODES = load_canonical_codes()


def main() -> None:
    if len(sys.argv) != 2:
        fail("Usage: audit_wob.py path/to/WOB.json")
    p = Path(sys.argv[1])
    if not p.exists():
        fail(f"File not found: {p}")

    data = json.loads(p.read_text(encoding="utf-8"))

    for k in [
        "sft_version",
        "obstruction_id",
        "witness_tier",
        "obstruction_code",
        "description",
        "evidence",
        "recommended_next_moves",
        "artifacts",
    ]:
        if k not in data:
            fail(f"Missing required field: {k}")

    if not isinstance(data["evidence"], list) or len(data["evidence"]) == 0:
        fail("evidence must be a non-empty list")

    if not isinstance(data["recommended_next_moves"], list) or len(data["recommended_next_moves"]) == 0:
        fail("recommended_next_moves must be a non-empty list")

    art = data.get("artifacts", {}) or {}
    theorem_refs = art.get("theorem_refs", []) or []
    files = art.get("files", []) or []
    if not isinstance(theorem_refs, list) or not all(isinstance(x, str) for x in theorem_refs):
        fail("artifacts.theorem_refs must be a list of strings")
    if len([x for x in theorem_refs if str(x).strip()]) == 0:
        fail("artifacts.theorem_refs must be a non-empty list")
    if not isinstance(files, list) or not all(isinstance(x, str) for x in files):
        fail("artifacts.files must be a list of strings")
    if len([x for x in files if str(x).strip()]) == 0:
        fail("artifacts.files must be a non-empty list")

    check_ref_paths(theorem_refs, "artifacts.theorem_refs")
    check_ref_paths(files, "artifacts.files")

    strict = (os.environ.get("SFT_AUDIT_STRICT_ANCHORS", "").strip() == "1")
    if strict:
        for ref in theorem_refs:
            check_anchor_exists(ref)

    oc = str(data["obstruction_code"])
    if not PAT.match(oc):
        fail(f"obstruction_code not canonical format: {oc}")

    if oc not in CANONICAL_CODES:
        fail(
            "obstruction_code not in canonical catalog: "
            f"{oc}. Expected one of {sorted(CANONICAL_CODES)}"
        )

    print("OK: WOB checks succeeded")
    print("Summary")
    print("  obstruction_id:", data["obstruction_id"])
    print("  code:", oc)
    print("  tier:", data["witness_tier"])


if __name__ == "__main__":
    main()
