#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12,<3.14"
# dependencies = ["pydantic>=2,<3"]
# ///

# ─── How to run ───
#   uv run scripts/verify_nutricoach_v150_candidate.py --base BASE.json \
#     --successor CANDIDATE_DIR --manifest CANDIDATE_DIR/manifest.json
# ──────────────────

"""Verify the immutable NutriCoach v1.5 combined candidate."""

from __future__ import annotations

import argparse
import json
import sys
import zipfile
from pathlib import Path
from typing import Final

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from scripts.verify_nutricoach_v140_candidate_core import (
    CandidateContractError,
    JsonValue,
    canonical,
    inventory_digest,
    load_json,
    require_digest,
    require_object,
    require_string,
    sha256_bytes,
    sha256_file,
    verify_hash_entries,
)
from scripts.nutricoach_v150_candidate_wheel_sources import (
    verify_candidate_file_set,
    verify_wheel_sources,
)
from scripts.nutricoach_v150_r71b_task10_evidence import (
    Task10EvidenceError,
    verify_candidate_task10_evidence,
)
from scripts.verify_nutricoach_v150_candidate_inputs import verify_v17_overlay

_V17_EVIDENCE_MANIFEST_SHA256: Final = (
    "17e1909289829d035305703cb8a0e1b5313de21ad7a7ecb825cb7093f71d4389"
)
_V17_SANDBOX_DIFF_SHA256: Final = (
    "e2aa170a245054abf92cadc34663f9820f88023d597d72be1becb9978043e9b8"
)
_V17_SOURCE_SET_SHA256: Final = (
    "e163bf25d4d56dea9ce0d495446f4bbf9b1061c8ed0d602c885b68b1bb1c15cb"
)
_R71B_EVIDENCE_INPUTS: Final = {
    "r70_error": "inputs/r70-canonical-authority-drift.json",
    "observer": "inputs/r71b-observer-final.json",
    "health_recovery": "inputs/r71b-fixed-collector.json",
}
_R71B_EVIDENCE_INPUT_PATHS: Final = frozenset(_R71B_EVIDENCE_INPUTS.values())


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    _ = parser.add_argument("--base", required=True, type=Path)
    _ = parser.add_argument("--successor", required=True, type=Path)
    _ = parser.add_argument("--manifest", required=True, type=Path)
    return parser


class CliNamespace(argparse.Namespace):
    """Mutable argparse destination with statically known path fields."""

    def __init__(self) -> None:
        super().__init__()
        self.base: Path = Path()
        self.successor: Path = Path()
        self.manifest: Path = Path()


def verify_capabilities(manifest: dict[str, JsonValue]) -> None:
    capabilities = require_object(manifest.get("capabilities"), "capabilities")
    channel = require_object(
        capabilities.get("nutricoach_channel_inbox_v1"), "channel inbox"
    )
    multi = require_object(
        capabilities.get("nutricoach_multi_customer_v1"), "multi customer"
    )
    if channel != {"compiled": True, "configured": False, "authorized": False}:
        raise CandidateContractError("Channel Inbox is not compiled default-OFF")
    if multi != {
        "compiled": True,
        "configured": False,
        "authorized": False,
        "authorized_capacity": 0,
        "post_migration_capacity": 5,
    }:
        raise CandidateContractError("multi-customer capability contract is invalid")


def r71b_evidence_digest(entries: list[dict[str, JsonValue]]) -> str:
    """Return the digest of the three Task10 reports required by r71b preseal."""
    evidence = [
        entry
        for entry in entries
        if require_string(entry.get("path"), "input path")
        in _R71B_EVIDENCE_INPUT_PATHS
    ]
    paths = {
        require_string(entry.get("path"), "r71b evidence path")
        for entry in evidence
    }
    if paths != set(_R71B_EVIDENCE_INPUT_PATHS):
        raise CandidateContractError("r71b maintenance evidence input set is invalid")
    return inventory_digest(evidence)


def _verify_authority_receipt(
    root: Path,
    wheel_entries: list[dict[str, JsonValue]],
) -> None:
    receipt = require_object(
        load_json(root / "receipts/authority-verification.json"),
        "authority verification receipt",
    )
    wheel_builds = require_object(
        receipt.get("wheel_builds"),
        "authority verification wheels",
    )
    expected_hermes = sha256_file(
        root / require_string(wheel_entries[0].get("path"), "receipt Hermes wheel")
    )
    expected_profile = sha256_file(
        root / require_string(wheel_entries[1].get("path"), "receipt profile wheel")
    )
    if (
        receipt.get("status") != "PASS"
        or receipt.get("external_events") != 0
        or wheel_builds.get("reproducible") is not True
        or wheel_builds.get("hermes_sha256") != expected_hermes
        or wheel_builds.get("profile_sha256") != expected_profile
    ):
        raise CandidateContractError("authority verification receipt is stale")


def verify(base_path: Path, root: Path, manifest_path: Path) -> str:
    manifest = require_object(load_json(manifest_path), "manifest")
    if manifest.get("schema") != "nutricoach-v150-combined-candidate-manifest-v1":
        raise CandidateContractError("candidate manifest schema is invalid")
    if manifest.get("status") != "QUALIFIED_PENDING_LIVE_AUTHORIZATION":
        raise CandidateContractError("candidate status is not qualified")
    verify_capabilities(manifest)
    source_entries = verify_hash_entries(
        root, manifest.get("source_inventory"), "source_inventory"
    )
    input_entries = verify_hash_entries(
        root, manifest.get("input_inventory"), "input_inventory"
    )
    wheel_entries = verify_hash_entries(
        root, manifest.get("wheel_inventory"), "wheel_inventory"
    )
    evidence_entries = verify_hash_entries(
        root, manifest.get("evidence_inventory"), "evidence_inventory"
    )
    try:
        _ = verify_candidate_task10_evidence(root, _R71B_EVIDENCE_INPUTS)
    except Task10EvidenceError as error:
        raise CandidateContractError("r71b Task10 evidence is invalid") from error
    _verify_authority_receipt(root, wheel_entries)
    verify_candidate_file_set(root, manifest)
    verify_v17_overlay(root, source_entries)

    derivation = require_object(manifest.get("derivation_inputs"), "derivation_inputs")
    base = require_object(load_json(base_path), "base manifest")
    base_identities = require_object(base.get("identities"), "base identities")
    expected: dict[str, JsonValue] = {
        "base_candidate_digest": require_digest(
            base_identities.get("candidate_digest"), "base candidate digest"
        ),
        "base_manifest_sha256": sha256_file(base_path),
        "evidence_digest": inventory_digest(evidence_entries),
        "hermes_wheel_sha256": sha256_file(
            root / require_string(wheel_entries[0].get("path"), "build-1 Hermes")
        ),
        "interpreter_sha256": sha256_file(Path(sys.executable).resolve()),
        "profile_wheel_sha256": sha256_file(
            root / require_string(wheel_entries[1].get("path"), "build-1 profile")
        ),
        "r71b_evidence_digest": r71b_evidence_digest(input_entries),
        "source_tree_digest": inventory_digest(source_entries),
        "successor_overlay_sha256": sha256_file(
            root / "inputs/successor-overlay.sha256"
        ),
        "v17_evidence_manifest_sha256": sha256_file(
            root / "inputs/v17-evidence-files.sha256"
        ),
        "v17_patch_tree_sha256": sha256_file(root / "inputs/v17-patch-tree.sha256"),
        "v17_sandbox_diff_sha256": sha256_file(root / "inputs/v17-sandbox.diff"),
        "v17_source_set_sha256": sha256_file(
            root / "inputs/v17-exact-source-set.sha256"
        ),
    }
    if derivation != expected:
        raise CandidateContractError("canonical product derivation inputs differ")
    if (
        expected["v17_evidence_manifest_sha256"] != _V17_EVIDENCE_MANIFEST_SHA256
        or expected["v17_sandbox_diff_sha256"] != _V17_SANDBOX_DIFF_SHA256
        or expected["v17_source_set_sha256"] != _V17_SOURCE_SET_SHA256
    ):
        raise CandidateContractError("immutable v17 qualification input mismatch")
    candidate = sha256_bytes(canonical(derivation))
    claimed = require_digest(manifest.get("candidate_digest"), "candidate digest")
    component_digests = {
        require_digest(expected["hermes_wheel_sha256"], "derived Hermes wheel digest"),
        require_digest(
            expected["profile_wheel_sha256"], "derived profile wheel digest"
        ),
        require_digest(expected["source_tree_digest"], "derived source digest"),
    }
    if candidate != claimed or candidate in component_digests:
        raise CandidateContractError("full-product candidate identity is invalid")
    if sha256_file(root / "inputs/base-manifest.json") != sha256_file(base_path):
        raise CandidateContractError("snapshot base manifest mismatch")
    if len(wheel_entries) != 4:
        raise CandidateContractError("exactly two detached wheel pairs are required")
    wheels = [
        root / require_string(entry.get("path"), "wheel path")
        for entry in wheel_entries
    ]
    if sha256_file(wheels[0]) != sha256_file(wheels[2]):
        raise CandidateContractError("Hermes wheel builds are not reproducible")
    if sha256_file(wheels[1]) != sha256_file(wheels[3]):
        raise CandidateContractError("profile wheel builds are not reproducible")
    source_root = Path(__file__).resolve().parents[1]
    for entry in source_entries:
        relative = Path(require_string(entry.get("path"), "source path"))
        product_relative = Path(*relative.parts[2:])
        if sha256_file(source_root / product_relative) != require_digest(
            entry.get("sha256"), "source hash"
        ):
            raise CandidateContractError(f"working source mismatch: {product_relative}")
    verify_wheel_sources(source_root, wheels[0], wheels[1])
    verify_wheel_sources(source_root, wheels[2], wheels[3])
    qualification = require_object(
        load_json(root / "qualification.json"), "qualification"
    )
    if qualification != {
        "candidate_digest": candidate,
        "status": "QUALIFIED_PENDING_LIVE_AUTHORIZATION",
    }:
        raise CandidateContractError("qualification binding is invalid")
    return candidate


def main() -> int:
    args = _parser().parse_args(namespace=CliNamespace())
    try:
        candidate = verify(args.base, args.successor, args.manifest)
    except (CandidateContractError, OSError, ValueError, zipfile.BadZipFile) as error:
        print(f"NUTRICOACH_V150_CANDIDATE_FAIL:{error}")
        return 1
    print(json.dumps({"candidate_digest": candidate}, sort_keys=True))
    print("NUTRICOACH_V150_CANDIDATE_PASS")
    return 0


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