#!/usr/bin/env python3
"""Recompute the gateway half of the sealed Task22 recovery successor.

The profile package is verified by the immutable, independently reproduced wheel
attestation in the manifest.  Verify-only deliberately never opens any live
profile path.
"""
from __future__ import annotations

import hashlib
import json
import os
import subprocess
import sys
from collections import Counter
from pathlib import Path
from typing import Final, TypeGuard, cast

EVIDENCE = Path(sys.argv[2]).resolve() if len(sys.argv) == 3 and sys.argv[1] == "--evidence-root" else Path(__file__).resolve().parent
MANIFEST = EVIDENCE / "dualcoach-trainer-removal-successor-manifest.json"
FREEZE = EVIDENCE / "dualcoach-trainer-removal-successor-freeze-receipt.json"
PRE_ADOPTION = EVIDENCE / "dualcoach-task-22-pre-callback-adoption-evidence.json"
GATEWAY_ROOT: Final = Path("/home/cube/projects/richard/hermes-agent")
EXPECTED: Final = {'candidate': '18a54bfebc9e81c70da8c1a4829a84892ad93e0e346378685f5f21ad6b89685b', 'package': '3090dc7ca9e93236394dd4da88647b100ea70b22f00080950a24e1d254f575e6', 'manifest': '9b52c89bd483d64dc48f83dd5cd3d61f99c2bedc47f57f905508779e29de14f1', 'freeze': '9f7b80eced374146a5c8a18fa96cb0964a299404aabbeae23b0d8e7297bce81f', 'pre_adoption': 'c62fe20d005c64854bc8f9950c768e6b0595c519f4e1dca5824c7bbee2b3f04d', 'status': '030af7db98cf9011370cb81071a9aec91562ecbca20efc806575a6206a86aef7', 'diff': '65af83353627a492f2d5b4fcd813bc764ff963fb7f0ea5b66a155f866f3dd1e6', 'gateway_candidate': '97d32e87cf59f294664e6e322a29137fbc1cc0701305e64610b4e926ed1d2fd9', 'gateway_diff': '7de5650ba581bdf90397f65167ce29028e0d060a7f6644903ee64dba8e67a687'}
SUPERVISED_PINS: Final = {'task22_child_protocol.py': '1dbbc8295042dd9dc18d804d99975c3c533f90fd1f8cfb88bc529c652770c5d3', 'task22_dependency_closure.py': '9b04dd4749bc862d2805609a8191eb70f59164880b0f87dede256f4dee7cae95', 'task22_launcher_runtime.py': '7c8c7360231bc8ee645fd7a19756d877036cafdfdb1986a05434c43595cb9100', 'task22_launcher_test_support.py': '71b8abb950dd8c03ef1aba938ff76df27878804cbb7feb63b026edd99b1bcbb8', 'task22_lifecycle.py': '4c4500af727ac9b75688ff63382498ee5807cb20443da01dc15d72dd38e1f72a', 'task22_resource_ownership.py': 'e47cdcfc9fe16bfe34260987c6c85e288ce5b285e86d8a4cce1f78ed84669e5e', 'test_task22_launcher_complete_closure.py': '0ef7a088708a340ea9256ade97de032955669c29027fc84a53d1a255307f7a90', 'test_task22_launcher_venv_fork.py': '3c76cdfca1584a8ecd4e4b19363a927d65c5863b80e68e44e2269e15e48327a6', 'test_task22_parent_death_lifecycle.py': 'a6ef24c6ee65e065da8c37cc3bf0aef3c0d6ea555885a71f6f8638a1f52a65d3', 'test_task22_pidfd_contract.py': 'b55c34220537ce29858e2d2be369d20a0838d1482adf57fd462ba14d6bcbf483', 'test_task22_pidfd_timeout.py': 'd8d3787879ee5263732dc853e84e48e65731614eb67cb89bf61ef954a38dad91', 'test_task22_sealed_module_identity.py': 'ee75cf1c1b741dd41b93e1891a775a0b69684416ee14557342a63f41297ff88f', 'test_task22_trainer_removal_launcher_hardening.py': '2aa1d8f24898bdea031395e57ee93e5975ad5a0868f5a85b3b1a7dcc442dd8f0'}
CURRENT_WHEEL: Final = "fc4ca963176010769bb2bbd4aa2a22565a1c3234775aa97924b6f12916a7f59e"
CURRENT_GATEWAY_SOURCE: Final = "4a2005c8f4faddc177cdd17940db5b1c0dd2780212ed03d01e248e51e2c25b6f"
HISTORICAL_WHEEL: Final = "bc7136b8614fb766cfba6b6017b61ab532479790f4bf2c259899272ceafc1e0d"
PROFILE_WHEEL: Final = "104cfdda58eeb3487a075426598a3548fbb7a7d16f50eb81cd8a47e0fc090347"
SUCCESS: Final = frozenset({0})


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


def is_dict(value: object) -> TypeGuard[dict[str, object]]:
    if not isinstance(value, dict):
        return False
    return all(isinstance(key, str) for key in cast(dict[object, object], value))


def canonical(value: object) -> bytes:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()


def run(argv: list[str], cwd: Path, accepted: frozenset[int] = SUCCESS) -> bytes:
    result = subprocess.run(argv, cwd=cwd, env={**os.environ, "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"}, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if result.returncode not in accepted or result.stderr:
        raise RuntimeError((argv, result.returncode, result.stderr.decode(errors="replace")))
    return result.stdout


def _leaf_identity(leaf: dict[str, object], index: int) -> tuple[str, str, str]:
    root, path, category = leaf.get("root"), leaf.get("path"), leaf.get("diff_category")
    if not all(isinstance(value, str) for value in (root, path, category)):
        raise RuntimeError(f"candidate leaf {index} identity must contain strings")
    return cast(tuple[str, str, str], (root, path, category))


def main() -> int:
    require(len(sys.argv) in {1, 3} and (len(sys.argv) == 1 or sys.argv[1] == "--evidence-root"), "usage: verifier [--evidence-root PATH]")
    manifest_bytes = MANIFEST.read_bytes()
    raw_manifest = cast(object, json.loads(manifest_bytes))
    if not is_dict(raw_manifest):
        raise RuntimeError("manifest must be a JSON object")
    manifest = raw_manifest
    roots = manifest.get("roots")
    leaves_value = manifest.get("candidate_files")
    if not is_dict(roots) or not isinstance(leaves_value, list):
        raise RuntimeError("manifest roots and candidate_files are required")
    require(roots.get("gateway") == str(GATEWAY_ROOT), "gateway root changed")
    require(isinstance(roots.get("profile_package"), str), "profile package root changed")
    leaves = cast(list[object], leaves_value)
    gateway_candidate, gateway_diff = hashlib.sha256(), hashlib.sha256()
    categories: Counter[str] = Counter()
    patch_bytes: Counter[str] = Counter()
    patch_codes: Counter[tuple[str, int]] = Counter()
    previous: tuple[bytes, bytes] | None = None
    profile_leaves = 0
    for index, value in enumerate(leaves):
        if not is_dict(value):
            raise RuntimeError(f"candidate leaf {index} must be an object")
        leaf = value
        root, path, category = _leaf_identity(leaf, index)
        key = root.encode(), path.encode()
        require(previous is None or key > previous, "candidate leaves are not strictly ordered")
        previous = key
        require(root in {"gateway", "profile_package"}, f"candidate leaf {index} root changed")
        if root == "profile_package":
            profile_leaves += 1
            require(category == "profile-package-external", "profile leaf category changed")
            require(isinstance(leaf.get("bytes"), int) and isinstance(leaf.get("sha256"), str), "profile leaf attestation changed")
            continue
        source = GATEWAY_ROOT / path
        content = source.read_bytes()
        require(len(content) == leaf.get("bytes"), f"candidate leaf {root}:{path} length changed")
        require(hashlib.sha256(content).hexdigest() == leaf.get("sha256"), f"candidate leaf {root}:{path} digest changed")
        tracked = subprocess.run(["git", "ls-files", "--error-unmatch", "--", path], cwd=GATEWAY_ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
        expected_category = "gateway-tracked" if tracked else "gateway-untracked"
        require(category == expected_category, f"candidate leaf {root}:{path} category changed")
        argv = (["git", "diff", "--binary", "--no-ext-diff", "--no-textconv", "HEAD", "--", path] if tracked else ["git", "diff", "--no-index", "--binary", "--no-ext-diff", "--", "/dev/null", "./" + path])
        result = subprocess.run(argv, cwd=GATEWAY_ROOT, env={**os.environ, "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8"}, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        require(result.returncode in ({0} if tracked else {0, 1}) and not result.stderr, f"candidate leaf {root}:{path} patch command failed")
        delta = result.stdout
        require(leaf.get("patch_return_code") == result.returncode, f"candidate leaf {root}:{path} patch return code changed")
        require(leaf.get("patch_bytes") == len(delta), f"candidate leaf {root}:{path} patch length changed")
        require(leaf.get("patch_sha256") == hashlib.sha256(delta).hexdigest(), f"candidate leaf {root}:{path} patch digest changed")
        gateway_candidate.update(key[0] + b"\0" + key[1] + b"\0" + content + b"\0")
        for frame in (*key, category.encode(), delta):
            gateway_diff.update(frame + b"\0")
        categories[category] += 1
        patch_bytes[category] += len(delta)
        patch_codes[(category, result.returncode)] += 1
    require(len(leaves) == manifest.get("candidate_path_count") == 177 and profile_leaves == 59, "candidate leaf count changed")
    require(manifest.get("candidate_digest") == EXPECTED["candidate"], "frozen candidate digest changed")
    require(manifest.get("profile_package_digest") == EXPECTED["package"], "frozen profile package attestation changed")
    require(manifest.get("candidate_diff_sha256") == EXPECTED["diff"], "frozen candidate diff digest changed")
    require(gateway_candidate.hexdigest() == manifest.get("gateway_candidate_digest") == EXPECTED["gateway_candidate"], "gateway candidate digest changed")
    require(gateway_diff.hexdigest() == manifest.get("gateway_diff_sha256") == EXPECTED["gateway_diff"], "gateway diff digest changed")
    require(dict(categories) == {"gateway-tracked": 31, "gateway-untracked": 87}, "gateway diff categories changed")
    require(hashlib.sha256(manifest_bytes).hexdigest() == EXPECTED["manifest"], "successor manifest digest changed")
    status = run(["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], GATEWAY_ROOT)
    records = [record for record in status.split(b"\0") if record]
    require(len(status) == manifest.get("gateway_status_byte_length") == 46606, "gateway status byte length changed")
    require(len(records) == manifest.get("gateway_status_entry_count") == 1001, "gateway status entry count changed")
    require(hashlib.sha256(status).hexdigest() == manifest.get("gateway_status_snapshot_sha256") == EXPECTED["status"], "gateway status digest changed")
    require(manifest.get("incident_recovery_provenance") == {
        "provisional_task_id": "st_019ff2a2", "incident_evidence_index_sha256": "c0662f154a54aa0aaa05afa45eaca32ebdc99226eaf68d39fb23bf89d345a4af", "transaction_id": "86d979a1d83dab0eab6ff71c0688d5c84cccfdd51b2843663c79c30fc4876eff", "recovery_module_sha256": "4a2005c8f4faddc177cdd17940db5b1c0dd2780212ed03d01e248e51e2c25b6f", "forward_only_cli_sha256": "37450829b21f33b339eb804ee4ab5bb3aab16d3c9bd612b3f40f44b1ec62d292", "read_only_incident_evidence": True,
    }, "incident recovery provenance changed")

    pre = cast(object, json.loads(PRE_ADOPTION.read_bytes()))
    if not is_dict(pre):
        raise RuntimeError("pre-adoption evidence must be an object")
    unsigned_pre = {key: value for key, value in pre.items() if key != "evidence_digest"}
    require(hashlib.sha256(canonical(unsigned_pre)).hexdigest() == pre.get("evidence_digest") == EXPECTED["pre_adoption"], "pre-adoption evidence digest changed")
    require(manifest.get("pre_adoption_evidence_digest") == EXPECTED["pre_adoption"], "manifest pre-adoption binding changed")
    fixture = GATEWAY_ROOT / "tests/gateway/fixtures/task22_protected_infrastructure_13.json"
    require(hashlib.sha256(fixture.read_bytes()).hexdigest() == "8f70327d4d2876389222e1a6cae8073c03460fd4ee0aa1d0935a823e9e2c59f7", "captured protected infrastructure fixture changed")
    supervised = manifest.get("supervised_launcher_provenance")
    require(is_dict(supervised) and supervised.get("pins") == SUPERVISED_PINS, "supervised launcher artifact pins changed")
    wheel = manifest.get("wheel_provenance")
    if not is_dict(wheel) or not all(is_dict(wheel.get(name)) for name in ("current_gateway", "historical_gateway", "profile")):
        raise RuntimeError("wheel provenance records changed")
    require(cast(dict[str, object], wheel["current_gateway"]).get("sha256") == CURRENT_WHEEL and cast(dict[str, object], wheel["current_gateway"]).get("members") == 993 and cast(dict[str, object], wheel["current_gateway"]).get("source_sha256") == CURRENT_GATEWAY_SOURCE, "current gateway wheel changed")
    require(cast(dict[str, object], wheel["historical_gateway"]).get("sha256") == HISTORICAL_WHEEL, "historical gateway wheel changed")
    require(cast(dict[str, object], wheel["profile"]).get("sha256") == PROFILE_WHEEL and cast(dict[str, object], wheel["profile"]).get("members") == 50, "profile wheel changed")
    require(wheel.get("source_epochs") == [1700000000, 1800000000] and wheel.get("fixed_timestamp") == "2000-01-01T00:00:00Z" and wheel.get("offline_install_import") is True, "wheel reproducibility contract changed")

    freeze = cast(object, json.loads(FREEZE.read_bytes()))
    if not is_dict(freeze):
        raise RuntimeError("freeze receipt must be an object")
    unsigned_freeze = {key: value for key, value in freeze.items() if key != "receipt_digest"}
    require(hashlib.sha256(canonical(unsigned_freeze)).hexdigest() == freeze.get("receipt_digest") == EXPECTED["freeze"], "freeze receipt digest changed")
    require(freeze.get("release_candidate_digest") == EXPECTED["candidate"] and freeze.get("manifest_digest") == EXPECTED["manifest"] and freeze.get("profile_package_digest") == EXPECTED["package"], "freeze successor binding changed")
    require(freeze.get("wheel_provenance") == {"current_gateway_members": 993, "current_gateway_sha256": CURRENT_WHEEL, "fixed_timestamp": "2000-01-01T00:00:00Z", "historical_gateway_sha256": HISTORICAL_WHEEL, "profile_members": 50, "profile_sha256": PROFILE_WHEEL, "source_epochs": [1700000000, 1800000000]}, "freeze wheel binding changed")
    output: dict[str, object] = {
        key: EXPECTED[key]
        for key in ("candidate", "package", "manifest", "freeze", "pre_adoption", "status", "diff")
    }
    output.update({"leaves": 177, "wheel": CURRENT_WHEEL})
    print(json.dumps(output, sort_keys=True))
    return 0


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