#!/usr/bin/env python3
"""Verify this repository's Truth Machine adoption declaration is discharged.

Reads truth-machine.toml and checks that every adopted pattern names paths
that exist in the working tree, every declined pattern states a reason, and
every entry is a pattern this copy's catalog names. It mutates nothing; a
completed migration or bootstrap is an exit code, not a judgment.

This is a template to copy and own, not a library to import. Extend
PATTERNS when your copy of the catalog grows. The declaration's shape is
published as declaration.schema.json and the `--json` output as
read-result.schema.json, both under
https://kinra.ai/docs/truth-machine/schemas/.

Exit codes: 0 = match, 1 = mismatch, 2 = unknown.
"""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
SCHEMA_VERSION = 1
DECLARATION = "truth-machine.toml"
EXIT_CODES = {"match": 0, "mismatch": 1, "unknown": 2}

# The Kit catalog's pattern slugs, mirroring declaration.schema.json.
PATTERNS = frozenset(
    {
        "front_door",
        "status_lines",
        "status_document",
        "authority_boundary",
        "evidence_hygiene",
        "frozen_archive",
        "decision_records",
        "current_projection",
        "human_index",
        "governed_read",
        "sealed_baseline",
        "declared_versus_observed",
        "untrusted_intake",
        "evidence_packet",
        "change_packet",
        "atomic_transition",
        "exact_review",
        "publication_freeze",
        "generated_artifacts",
    }
)
ENTRY_KEYS = frozenset({"adopted", "path", "paths", "index", "reason", "notes"})


class DeclarationError(ValueError):
    """The declaration could not be loaded or is not well formed."""


def check(
    *,
    check_id: str,
    expected: object,
    observed: object,
    source: str,
    meaning: str,
    unknown: bool = False,
) -> dict:
    status = "unknown" if unknown else ("match" if observed == expected else "mismatch")
    return {
        "id": check_id,
        "status": status,
        "expected": expected,
        "observed": observed,
        "source": source,
        "meaning": meaning,
    }


def load_declaration(root: Path) -> dict:
    path = root / DECLARATION
    try:
        with path.open("rb") as handle:
            declaration = tomllib.load(handle)
    except FileNotFoundError as exc:
        raise DeclarationError(f"missing declaration: {path}") from exc
    except tomllib.TOMLDecodeError as exc:
        raise DeclarationError(f"invalid declaration TOML: {exc}") from exc
    version = declaration.get("spec_version")
    if not isinstance(version, str) or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
        raise DeclarationError("declaration must name a MAJOR.MINOR.PATCH spec_version")
    patterns = declaration.get("patterns")
    if not isinstance(patterns, dict) or not patterns:
        raise DeclarationError("declaration must carry a non-empty [patterns] table")
    return declaration


def declared_paths(entry: dict) -> list[str]:
    paths: list[str] = []
    if isinstance(entry.get("path"), str):
        paths.append(entry["path"])
    if isinstance(entry.get("paths"), list):
        paths.extend(item for item in entry["paths"] if isinstance(item, str))
    if isinstance(entry.get("index"), str):
        paths.append(entry["index"])
    return paths


def missing_paths(root: Path, paths: list[str]) -> list[str]:
    missing: list[str] = []
    for relative in paths:
        candidate = (root / relative).resolve()
        if not candidate.is_relative_to(root.resolve()) or not candidate.exists():
            missing.append(relative)
    return missing


def evaluate(declaration: dict, root: Path) -> list[dict]:
    checks: list[dict] = []
    for slug, entry in declaration["patterns"].items():
        source = f"{DECLARATION} [patterns.{slug}] versus the working tree"
        if slug not in PATTERNS:
            checks.append(
                check(
                    check_id=f"pattern:{slug}",
                    expected="a pattern slug this copy's catalog names",
                    observed=slug,
                    source=source,
                    meaning="the declared pattern exists in the catalog, so its rule is known; extend PATTERNS if the catalog grew",
                    unknown=True,
                )
            )
            continue
        problems: list[str] = []
        if not isinstance(entry, dict):
            problems.append(f"entry is {type(entry).__name__}, not a table")
        else:
            if not isinstance(entry.get("adopted"), bool):
                problems.append("missing boolean key 'adopted'")
            problems.extend(
                f"unexpected key {key!r}" for key in sorted(entry.keys() - ENTRY_KEYS)
            )
        if problems:
            checks.append(
                check(
                    check_id=f"pattern:{slug}",
                    expected=sorted(ENTRY_KEYS),
                    observed=problems,
                    source=f"{DECLARATION} [patterns.{slug}] versus declaration.schema.json",
                    meaning="the declaration entry is well formed, so its check can run at all",
                    unknown=True,
                )
            )
            continue
        if not entry["adopted"]:
            reason = entry.get("reason")
            stated = isinstance(reason, str) and bool(reason.strip())
            checks.append(
                check(
                    check_id=f"pattern:{slug}",
                    expected="a stated reason for declining",
                    observed=(
                        "a stated reason for declining"
                        if stated
                        else "declined without a stated reason"
                    ),
                    source=source,
                    meaning=f"declining {slug} was a deliberate decision, not a silent omission",
                )
            )
            continue
        paths = declared_paths(entry)
        if not paths:
            checks.append(
                check(
                    check_id=f"pattern:{slug}",
                    expected="at least one verifiable path",
                    observed="adoption declared without a path, paths, or index",
                    source=source,
                    meaning=f"the adoption of {slug} names a file the working tree can prove",
                    unknown=True,
                )
            )
            continue
        checks.append(
            check(
                check_id=f"pattern:{slug}",
                expected=[],
                observed=missing_paths(root, paths),
                source=source,
                meaning=f"every path declared for {slug} exists in the working tree",
            )
        )
    return checks


def aggregate_status(checks: list[dict]) -> str:
    statuses = {item["status"] for item in checks}
    if "mismatch" in statuses:
        return "mismatch"
    if "unknown" in statuses:
        return "unknown"
    return "match"


def render_human(status: str, checks: list[dict]) -> str:
    lines = [f"Adoption: {status.upper()}"]
    for item in checks:
        lines.append(f"[{item['status']}] {item['id']} — {item['meaning']}")
        if item["status"] != "match":
            lines.append(f"    expected: {item['expected']!r}")
            lines.append(f"    observed: {item['observed']!r}")
            lines.append(f"    source: {item['source']}")
    counts = {
        name: sum(1 for item in checks if item["status"] == name)
        for name in ("match", "mismatch", "unknown")
    }
    lines.append(
        f"{counts['match']} match, {counts['mismatch']} mismatch, {counts['unknown']} unknown"
    )
    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--json", action="store_true", help="emit a machine payload")
    parser.add_argument("--root", type=Path, default=ROOT, help="repository root")
    args = parser.parse_args(argv)

    try:
        declaration = load_declaration(args.root)
    except DeclarationError as error:
        if args.json:
            payload = {
                "schema_version": SCHEMA_VERSION,
                "status": "unknown",
                "error": str(error),
                "checks": [],
            }
            print(json.dumps(payload, indent=2))
        else:
            print(f"Adoption: UNKNOWN\n{error}", file=sys.stderr)
        return EXIT_CODES["unknown"]

    checks = evaluate(declaration, args.root)
    status = aggregate_status(checks)
    if args.json:
        payload = {
            "schema_version": SCHEMA_VERSION,
            "status": status,
            "checks": checks,
        }
        print(json.dumps(payload, indent=2, sort_keys=True, default=str))
    else:
        print(render_human(status, checks))
    return EXIT_CODES[status]


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