#!/usr/bin/env python3
"""Offline governed read of this repository's declared current state.

This is a template to copy and own, not a library to import. It reads the
declared current projection (current.toml), cross-checks it against the human
index, and reports match, mismatch, or unknown without mutating anything.

Implements TM-OBS-001 (deterministic current read), TM-OBS-002 (read
provenance), and TM-OBS-003 (a read advances nothing). Every check carries a
`source` and a `meaning`; a failed observation is `unknown`, never a pass.
That rule reaches the declaration itself: a malformed entry in current.toml
is reported as an `unknown` check naming the entry, never silently skipped.

The declaration's shape is published as projection.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 hashlib
import json
import subprocess
import sys
import tomllib
from dataclasses import dataclass, field
from pathlib import Path


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


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


@dataclass(frozen=True)
class Check:
    id: str
    status: str
    expected: object
    observed: object
    source: str
    meaning: str

    def __post_init__(self) -> None:
        if self.status not in CHECK_STATUSES:
            raise ValueError(f"invalid check status: {self.status}")


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


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


def within_root(root: Path, relative: str) -> Path:
    candidate = (root / relative).resolve()
    if not candidate.is_relative_to(root.resolve()):
        raise DeclarationError(f"declared path escapes the repository: {relative}")
    return candidate


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
    if declaration.get("schema_version") != SCHEMA_VERSION:
        raise DeclarationError("declaration schema_version must be 1")
    repository = declaration.get("repository", {})
    if not repository.get("human_index"):
        raise DeclarationError("declaration must name repository.human_index")
    return declaration


# Required keys per declaration entry table, mirroring projection.schema.json.
ENTRY_KEYS = {
    "records": frozenset({"id", "path", "status_marker", "meaning"}),
    "sealed": frozenset({"scope", "revision", "reason"}),
    "artifacts": frozenset({"id", "path", "sha256", "meaning"}),
}


def vet_entries(
    declaration: dict, checks: list[Check]
) -> dict[str, list[dict]]:
    """Return only well-formed entries; malformed ones become unknown checks."""
    vetted: dict[str, list[dict]] = {}
    for section, required in ENTRY_KEYS.items():
        entries = declaration.get(section, [])
        vetted[section] = []
        if not isinstance(entries, list):
            checks.append(
                check(
                    check_id=f"declaration:{section}",
                    expected=f"[[{section}]] entries",
                    observed=f"{type(entries).__name__} value",
                    source=f"{DECLARATION} versus projection.schema.json",
                    meaning=f"the {section} declaration is a table array the read can evaluate",
                    unknown=True,
                )
            )
            continue
        for index, entry in enumerate(entries):
            problems: list[str] = []
            if not isinstance(entry, dict):
                problems.append(f"entry is {type(entry).__name__}, not a table")
            else:
                problems.extend(
                    f"missing key {key!r}" for key in sorted(required - entry.keys())
                )
                problems.extend(
                    f"unexpected key {key!r}" for key in sorted(entry.keys() - required)
                )
                problems.extend(
                    f"empty value for key {key!r}"
                    for key in sorted(required & entry.keys())
                    if not (isinstance(entry[key], str) and entry[key].strip())
                )
            if problems:
                checks.append(
                    check(
                        check_id=f"declaration:{section}[{index}]",
                        expected=sorted(required),
                        observed=problems,
                        source=f"{DECLARATION} [[{section}]] entry {index} versus projection.schema.json",
                        meaning=f"the declared {section} entry is well formed, so its check can run at all",
                        unknown=True,
                    )
                )
                continue
            vetted[section].append(entry)
    return vetted


def git(root: Path, *args: str) -> str | None:
    result = subprocess.run(
        ["git", "-C", str(root), *args],
        check=False,
        capture_output=True,
        text=True,
        timeout=20,
    )
    if result.returncode != 0:
        return None
    return result.stdout.strip()


def evaluate(declaration: dict, root: Path) -> list[Check]:
    checks: list[Check] = []
    repository = declaration.get("repository", {})
    vetted = vet_entries(declaration, checks)

    # Declared artifacts: every [[artifacts]] entry names a path and sha256.
    for artifact in vetted["artifacts"]:
        relative = artifact["path"]
        declared = artifact["sha256"]
        source = f"{DECLARATION} [[artifacts]] versus sha256 of {relative}"
        meaning = f"declared artifact {relative} is byte-identical to the working tree"
        try:
            observed = hashlib.sha256(
                within_root(root, relative).read_bytes()
            ).hexdigest()
        except (OSError, DeclarationError) as exc:
            checks.append(
                check(
                    check_id=f"artifact:{relative}",
                    expected=declared,
                    observed=str(exc),
                    source=source,
                    meaning=meaning,
                    unknown=True,
                )
            )
            continue
        checks.append(
            check(
                check_id=f"artifact:{relative}",
                expected=declared,
                observed=observed,
                source=source,
                meaning=meaning,
            )
        )

    # Status lines: each declared record's document carries its declared
    # status marker.
    for record in vetted["records"]:
        marker = record["status_marker"]
        record_path = record["path"]
        source = f"{DECLARATION} records versus {record_path}"
        meaning = f"{record_path} carries the declared status line"
        try:
            document = within_root(root, record_path).read_text(encoding="utf-8")
        except (OSError, DeclarationError) as exc:
            checks.append(
                check(
                    check_id=f"status:{record['id']}",
                    expected=marker,
                    observed=str(exc),
                    source=source,
                    meaning=meaning,
                    unknown=True,
                )
            )
            continue
        checks.append(
            check(
                check_id=f"status:{record['id']}",
                expected=marker,
                observed=marker if marker in document else "status line absent",
                source=source,
                meaning=meaning,
            )
        )

    # Human index: the markdown index must name every declared record.
    index_path = repository["human_index"]
    source = f"{DECLARATION} records versus {index_path}"
    try:
        index = within_root(root, index_path).read_text(encoding="utf-8")
    except (OSError, DeclarationError) as exc:
        checks.append(
            check(
                check_id="human-index",
                expected="readable index",
                observed=str(exc),
                source=source,
                meaning="the human index can be read at its declared path",
                unknown=True,
            )
        )
    else:
        missing = [
            record["id"] for record in vetted["records"] if record["id"] not in index
        ]
        checks.append(
            check(
                check_id="human-index",
                expected=[],
                observed=missing,
                source=source,
                meaning="the human index names every declared current record",
            )
        )

    # Sealed scopes: files under a sealed scope must remain byte-identical
    # to the seal revision. Tracked drift and untracked additions are both
    # mismatches; an unreadable seal revision is unknown, never a pass.
    for seal in vetted["sealed"]:
        scope = seal["scope"]
        seal_revision = seal["revision"]
        source = f"git diff {seal_revision[:12]} -- {scope}"
        meaning = f"{scope} is byte-identical to its seal revision"
        drifted = git(root, "diff", "--name-only", seal_revision, "--", scope)
        untracked = git(root, "ls-files", "--others", "--exclude-standard", "--", scope)
        if drifted is None or untracked is None:
            checks.append(
                check(
                    check_id=f"sealed:{scope}",
                    expected=[],
                    observed="seal revision could not be read",
                    source=source,
                    meaning=meaning,
                    unknown=True,
                )
            )
            continue
        changed = sorted(set(filter(None, f"{drifted}\n{untracked}".splitlines())))
        checks.append(
            check(
                check_id=f"sealed:{scope}",
                expected=[],
                observed=changed,
                source=source,
                meaning=meaning,
            )
        )

    # Canonical branch: the checkout matches the declared branch, when declared.
    branch = repository.get("canonical_branch")
    if branch:
        observed_branch = git(root, "branch", "--show-current")
        checks.append(
            check(
                check_id="canonical-branch",
                expected=branch,
                observed=observed_branch,
                source="git branch --show-current",
                meaning="the checkout is on the declared canonical branch",
                unknown=observed_branch is None,
            )
        )

    return checks


def render_human(status: str, checks: list[Check]) -> str:
    lines = [f"State: {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 CHECK_STATUSES}
    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"State: 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": [vars(item) for item in 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())
