#!/usr/bin/env python3
"""Fail-closed validator for an Overdeck K3s Phase 2 receipt."""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any, Mapping, Sequence

HERE = Path(__file__).resolve().parent
LIB = HERE / "lib"
if str(LIB) not in sys.path:
    sys.path.insert(0, str(LIB))

from phase1_common import read_json, sha256_file  # noqa: E402
from phase2_common import Phase2Error, assert_secret_free, canonical_json_bytes, sha256_json  # noqa: E402


def require(condition: bool, message: str) -> None:
    if not condition:
        raise Phase2Error(message)


def load_object(path: Path) -> dict[str, Any]:
    value = read_json(path)
    if not isinstance(value, dict):
        raise Phase2Error(f"expected JSON object: {path}")
    return value


def validate(receipt_dir: Path, *, expected_candidate: str | None = None) -> dict[str, Any]:
    root = receipt_dir.resolve()
    require(root.is_dir() and not root.is_symlink(), f"receipt directory is unsafe: {root}")
    result = load_object(root / "phase2-result.json")
    plan = load_object(root / "plan.json")
    fleet = load_object(root / "registry-preview/fleet.json")
    hosts = load_object(root / "registry-preview/buildbox-hosts.json")
    pair = load_object(root / "registry-preview/pair.json")
    ledger = load_object(root / "transaction-ledger.json")
    assert_secret_free({"result": result, "plan": plan, "fleet": fleet, "hosts": hosts, "pair": pair, "ledger": ledger})

    require(result.get("schema_version") == 1 and result.get("phase") == 2, "unsupported Phase 2 result")
    require(result.get("status") == "success", "Phase 2 result did not succeed")
    require(result.get("mode") in {"plan", "dry-run"}, "receipt was not produced by a read-only mode")
    candidate = str(result.get("candidate") or "")
    require(candidate != "", "receipt candidate is missing")
    if expected_candidate is not None:
        require(candidate == expected_candidate, "receipt candidate differs from expected candidate")
    for key in (
        "live_mutation_performed",
        "candidate_mutation_performed",
        "cluster_mutation_performed",
        "registry_mutation_performed",
        "phase3_authorized",
    ):
        require(result.get(key) is False, f"receipt must record {key}=false")
    require(result.get("git_publication_allowed") is True, "receipt does not authorize Git publication")
    require(result.get("plan_deterministic") is True, "plan is not declared deterministic")
    require(result.get("secret_scan_passed") is True, "secret scan did not pass")
    require(result.get("source_digests_before") == result.get("source_digests_after"), "tracked registries changed")
    require(result.get("plan_step_count") == 17, "expected exactly 17 enrollment steps")

    require(plan.get("phase") == 2 and plan.get("mode") == "dry-run", "unsupported enrollment plan")
    require(plan.get("live_mutation_allowed") is False, "plan permits live mutation")
    steps = plan.get("steps")
    require(isinstance(steps, list) and len(steps) == 17, "plan must contain 17 steps")
    require([step.get("ordinal") for step in steps if isinstance(step, Mapping)] == list(range(1, 18)), "step ordinals are invalid")
    expected_digest = sha256_json({k: v for k, v in plan.items() if k != "plan_sha256"})
    require(plan.get("plan_sha256") == expected_digest, "plan SHA-256 self-check failed")
    require(result.get("plan_sha256") == expected_digest, "result and plan SHA-256 differ")
    require(result.get("transaction_id") == plan.get("transaction_id"), "result and plan transaction differ")
    token = plan.get("bootstrap_token") if isinstance(plan.get("bootstrap_token"), Mapping) else {}
    require(token.get("value_recorded") is False, "bootstrap token value may not be recorded")
    require(token.get("ttl_seconds") == 600, "bootstrap token TTL contract changed")

    require(ledger.get("transaction_id") == plan.get("transaction_id"), "ledger transaction differs")
    require(ledger.get("plan_sha256") == expected_digest, "ledger plan binding differs")
    require(ledger.get("candidate") == candidate, "ledger candidate differs")
    require(ledger.get("status") == "planned" and ledger.get("events") == [], "Phase 2 ledger must remain unexecuted")

    require(pair.get("candidate") == candidate, "registry preview candidate differs")
    require(pair.get("execution") == "none", "candidate preview must use execution=none")
    require(pair.get("in_build_order") is False, "candidate entered build order")
    require(pair.get("in_e2e_order") is False, "candidate entered E2E order")
    require(pair.get("in_fallback_dependencies") is False, "candidate entered fallback dependencies")
    require(pair.get("pair_commit_required") is True, "registry previews are not transaction-paired")
    require(pair.get("fleet_sha256") == sha256_json(fleet), "fleet preview digest differs")
    require(pair.get("hosts_sha256") == sha256_json(hosts), "host preview digest differs")
    nodes = fleet.get("nodes") if isinstance(fleet.get("nodes"), Mapping) else {}
    require(isinstance(nodes.get(candidate), Mapping), "candidate is absent from fleet preview")
    require(nodes[candidate].get("execution") == "none", "fleet preview candidate can dispatch")
    host_items = hosts.get("hosts") if isinstance(hosts.get("hosts"), list) else []
    matches = [item for item in host_items if isinstance(item, Mapping) and item.get("name") == candidate]
    require(len(matches) == 1, "host preview must contain exactly one candidate")
    for order_name, order in (hosts.get("orders") or {}).items():
        require(candidate not in order, f"candidate entered host order {order_name}")
    require(candidate not in (fleet.get("fallback") or {}).get("requires_all_unavailable", []), "candidate entered fallback")

    summary = {
        "schema_version": 1,
        "status": "passed",
        "candidate": candidate,
        "transaction_id": plan.get("transaction_id"),
        "plan_sha256": expected_digest,
        "step_count": 17,
        "zero_mutation": True,
        "secret_free": True,
        "git_publication_allowed": True,
        "files": {
            "result_sha256": sha256_file(root / "phase2-result.json"),
            "plan_sha256_file": sha256_file(root / "plan.json"),
            "fleet_preview_sha256_file": sha256_file(root / "registry-preview/fleet.json"),
            "host_preview_sha256_file": sha256_file(root / "registry-preview/buildbox-hosts.json"),
        },
    }
    return summary


def parser() -> argparse.ArgumentParser:
    result = argparse.ArgumentParser(description=__doc__)
    result.add_argument("--receipt", required=True)
    result.add_argument("--candidate")
    return result


def main(argv: Sequence[str] | None = None) -> int:
    args = parser().parse_args(argv)
    try:
        print(json.dumps(validate(Path(args.receipt), expected_candidate=args.candidate), indent=2, sort_keys=True))
        return 0
    except (Exception, KeyboardInterrupt) as exc:
        print(json.dumps({"schema_version": 1, "status": "failed", "error": str(exc) or type(exc).__name__}, sort_keys=True))
        return 2


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