#!/usr/bin/env python3
"""Independently verify an immutable repaired-source Task26 successor candidate."""
from __future__ import annotations

import argparse
import hashlib
import json
import stat
import zipfile
from pathlib import Path
from typing import Any


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


def sha256(raw: bytes) -> str:
    return hashlib.sha256(raw).hexdigest()


def sha256_file(path: Path) -> str:
    return sha256(path.read_bytes())


def load_object(path: Path) -> dict[str, Any]:
    value = json.loads(path.read_bytes())
    if not isinstance(value, dict):
        raise AssertionError(f"{path} is not a JSON object")
    return value


def assert_file(path: Path) -> None:
    info = path.lstat()
    if path.is_symlink() or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
        raise AssertionError(f"unsafe candidate file: {path}")
    if stat.S_IMODE(info.st_mode) != 0o400:
        raise AssertionError(f"candidate file mode drift: {path}")


def assert_tree_modes(root: Path) -> None:
    info = root.lstat()
    if root.is_symlink() or not stat.S_ISDIR(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o500:
        raise AssertionError("candidate root is unsafe or mutable")
    for path in sorted(root.rglob("*")):
        info = path.lstat()
        if path.is_symlink():
            raise AssertionError(f"candidate contains symlink: {path}")
        if stat.S_ISDIR(info.st_mode):
            if stat.S_IMODE(info.st_mode) != 0o500:
                raise AssertionError(f"candidate directory mode drift: {path}")
        elif stat.S_ISREG(info.st_mode):
            assert_file(path)
        else:
            raise AssertionError(f"unsupported candidate entry: {path}")


def list_digest(value: object) -> str:
    if not isinstance(value, list):
        raise AssertionError("expected a list")
    return sha256(canonical(value))


def verify_wheel(path: Path, expected: dict[str, Any]) -> None:
    assert_file(path)
    if sha256_file(path) != expected.get("sha256") or path.stat().st_size != expected.get("bytes"):
        raise AssertionError("wheel bytes drifted")
    with zipfile.ZipFile(path) as archive:
        members = [
            {"path": item.filename, "bytes": len(raw), "sha256": sha256(raw)}
            for item in sorted(archive.infolist(), key=lambda value: value.filename)
            if not item.is_dir()
            for raw in (archive.read(item.filename),)
        ]
        timestamps = {item.date_time for item in archive.infolist()}
    if list_digest(members) != expected.get("members_sha256"):
        raise AssertionError("wheel member closure drifted")
    if timestamps != {(2000, 1, 1, 0, 0, 0)}:
        raise AssertionError("wheel metadata is not reproducible")


def verify(input_path: Path) -> dict[str, object]:
    assert_file(input_path)
    verifier_input = load_object(input_path)
    if verifier_input.get("schema") != "task26-repaired-archive-successor-verifier-input-v1":
        raise AssertionError("verifier input schema mismatch")
    root = Path(str(verifier_input["candidate_root"]))
    assert_tree_modes(root)
    manifest_path = root / "candidate-manifest.json"
    checkpoint_path = root / "candidate-checkpoint.json"
    manifest = load_object(manifest_path)
    checkpoint = load_object(checkpoint_path)
    if sha256_file(manifest_path) != verifier_input.get("manifest_sha256"):
        raise AssertionError("manifest hash mismatch")
    if sha256_file(checkpoint_path) != verifier_input.get("checkpoint_sha256"):
        raise AssertionError("checkpoint hash mismatch")

    full_digest = manifest.pop("full_candidate_digest", None)
    core_digest = manifest.pop("core_candidate_digest", None)
    calculated_core = sha256(canonical(manifest))
    calculated_full = sha256(canonical({"core_candidate_digest": calculated_core, "manifest_core": manifest}))
    if core_digest != calculated_core or full_digest != calculated_full:
        raise AssertionError("candidate digest mismatch")
    if verifier_input.get("full_candidate_digest") != calculated_full:
        raise AssertionError("verifier input candidate mismatch")
    manifest["core_candidate_digest"] = core_digest
    manifest["full_candidate_digest"] = full_digest
    expected_checkpoint = {
        "schema": "task26-repaired-archive-successor-checkpoint-v1",
        "status": "SEALED_SUCCESSOR_CANDIDATE_NOT_TASK26_PASS",
        "full_candidate_digest": full_digest,
        "core_candidate_digest": core_digest,
        "manifest_sha256": verifier_input["manifest_sha256"],
        "wheel_sha256": manifest["wheel"]["sha256"],
        "repository_status_sha256": manifest["repository_status"]["sha256"],
        "repository_status_entry_count": manifest["repository_status"]["entry_count"],
        "archive_binding_sha256": manifest["historical_archive_binding"]["binding_sha256"],
    }
    supersession = manifest.get("supersession")
    if isinstance(supersession, dict):
        expected_checkpoint["predecessor_full_digest"] = supersession["predecessor_full_digest"]
        expected_checkpoint["exact_source_delta_sha256"] = supersession["exact_source_delta_sha256"]
    if checkpoint != expected_checkpoint:
        raise AssertionError("checkpoint content mismatch")

    for name in ("pre.nul", "post.nul"):
        path = root / "status" / name
        assert_file(path)
        if sha256_file(path) != manifest["repository_status"]["sha256"]:
            raise AssertionError("status snapshot mismatch")
    if (root / "status/pre.nul").read_bytes() != (root / "status/post.nul").read_bytes():
        raise AssertionError("pre/post status snapshots differ")

    bindings = (
        (root / "bindings/executable-closure.json", manifest["executable_closure"]),
        (root / "bindings/historical-archive-binding.json", manifest["historical_archive_binding"]),
        (root / "bindings/task22-25-reconciliation-index.json", manifest["task22_25_reconciliation"]),
    )
    for path, section in bindings:
        if sha256_file(path) != section["artifact_sha256"]:
            raise AssertionError(f"binding artifact drifted: {path.name}")

    closure = load_object(bindings[0][0])
    if list_digest(closure.get("entries")) != closure.get("entries_sha256"):
        raise AssertionError("executable closure digest mismatch")
    hashes = {entry["path"]: entry["sha256"] for entry in closure["entries"]}
    repaired = manifest["repaired_source"]
    if hashes.get(repaired["controller_path"]) != repaired["controller_sha256"]:
        raise AssertionError("repaired controller is absent from closure")
    if hashes.get(repaired["test_path"]) != repaired["test_sha256"]:
        raise AssertionError("repaired test is absent from closure")
    if isinstance(supersession, dict):
        predecessor_manifest_path = root / str(supersession["predecessor_manifest_path"])
        predecessor_checkpoint_path = root / str(supersession["predecessor_checkpoint_path"])
        predecessor_closure_path = root / str(supersession["predecessor_closure_path"])
        if sha256_file(predecessor_manifest_path) != supersession["predecessor_manifest_sha256"]:
            raise AssertionError("predecessor manifest copy mismatch")
        if sha256_file(predecessor_checkpoint_path) != supersession["predecessor_checkpoint_sha256"]:
            raise AssertionError("predecessor checkpoint copy mismatch")
        if sha256_file(predecessor_closure_path) != supersession["predecessor_closure_sha256"]:
            raise AssertionError("predecessor closure copy mismatch")
        predecessor_manifest = load_object(predecessor_manifest_path)
        predecessor_checkpoint = load_object(predecessor_checkpoint_path)
        predecessor_closure = load_object(predecessor_closure_path)
        predecessor_digest = supersession["predecessor_full_digest"]
        if predecessor_manifest.get("full_candidate_digest") != predecessor_digest or predecessor_checkpoint.get("full_candidate_digest") != predecessor_digest:
            raise AssertionError("predecessor digest mismatch")
        old_entries = predecessor_closure.get("entries")
        new_entries = closure.get("entries")
        if not isinstance(old_entries, list) or not isinstance(new_entries, list):
            raise AssertionError("successor closure entries are malformed")
        old = {entry["path"]: entry for entry in old_entries}
        new = {entry["path"]: entry for entry in new_entries}
        calculated_delta = [
            {"path": path, "before": old.get(path), "after": new.get(path)}
            for path in sorted(set(old) | set(new)) if old.get(path) != new.get(path)
        ]
        if calculated_delta != supersession.get("exact_source_delta"):
            raise AssertionError("declared successor delta is not exact")
        if sha256(canonical(calculated_delta)) != supersession.get("exact_source_delta_sha256"):
            raise AssertionError("successor delta digest mismatch")
        if len(calculated_delta) != 1 or calculated_delta[0]["path"] != supersession.get("expected_delta_path"):
            raise AssertionError("successor has additional source drift")
        if hashes.get(supersession["status_test_path"]) != supersession.get("status_test_sha256"):
            raise AssertionError("status test pin drift")

    archive_binding = load_object(bindings[1][0])
    copied_manifest_path = root / "historical/archive-manifest.json"
    copied_receipt_path = root / "historical/archive-receipt.json"
    copied_manifest = load_object(copied_manifest_path)
    copied_receipt = load_object(copied_receipt_path)
    if sha256_file(copied_manifest_path) != archive_binding["manifest_sha256"]:
        raise AssertionError("archive manifest copy mismatch")
    if sha256_file(copied_receipt_path) != archive_binding["receipt_sha256"]:
        raise AssertionError("archive receipt copy mismatch")
    if sha256(canonical(copied_manifest)) != copied_receipt.get("archive_digest"):
        raise AssertionError("archive canonical digest mismatch")
    if len(copied_manifest.get("scopes", [])) != 23:
        raise AssertionError("archive does not contain 23 scopes")
    if sha256(canonical(copied_manifest["scopes"])) != archive_binding["scope_digest_sha256"]:
        raise AssertionError("23-scope binding mismatch")
    binding_core = dict(archive_binding)
    binding_digest = binding_core.pop("binding_sha256", None)
    if binding_digest != sha256(canonical(binding_core)):
        raise AssertionError("archive binding digest mismatch")

    reconciliation = load_object(bindings[2][0])
    if reconciliation.get("historical_execution_retargeted_to_successor") is not False:
        raise AssertionError("historical evidence was impermissibly retargeted")
    for entry in reconciliation.get("indexes", []):
        copied = root / "historical/reconciliation-indexes" / Path(entry["path"]).name
        if sha256_file(copied) != entry["sha256"]:
            raise AssertionError("historical index mismatch")
    verify_wheel(root / str(manifest["wheel"]["path"]), manifest["wheel"])
    return {
        "status": "PASS", "full_candidate_digest": full_digest, "core_candidate_digest": core_digest,
        "manifest_sha256": verifier_input["manifest_sha256"],
        "checkpoint_sha256": verifier_input["checkpoint_sha256"],
        "wheel_sha256": manifest["wheel"]["sha256"],
        "status_sha256": manifest["repository_status"]["sha256"],
        "status_entry_count": manifest["repository_status"]["entry_count"],
        "archive_binding_sha256": manifest["historical_archive_binding"]["binding_sha256"],
        "historical_execution_retargeted_to_successor": False,
        "predecessor_full_digest": supersession.get("predecessor_full_digest") if isinstance(supersession, dict) else None,
        "exact_source_delta_sha256": supersession.get("exact_source_delta_sha256") if isinstance(supersession, dict) else None,
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("input", type=Path)
    args = parser.parse_args()
    print(json.dumps(verify(args.input), sort_keys=True, separators=(",", ":")))
    return 0


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