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

# ─── How to run ───
#   uv run scripts/seal_nutricoach_v150_candidate.py --help
# ──────────────────

"""Materialize the immutable NutriCoach v1.5 combined candidate seal."""

from __future__ import annotations

import argparse
import shutil
import sys
from pathlib import Path

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

from scripts.verify_nutricoach_v140_candidate_core import (
    JsonValue,
    canonical,
    inventory_digest,
    load_json,
    require_list,
    require_object,
    require_string,
    sha256_bytes,
    sha256_file,
)


def _inventory(root: Path, paths: list[Path]) -> list[dict[str, JsonValue]]:
    return [
        {"path": path.relative_to(root).as_posix(), "sha256": sha256_file(path)}
        for path in sorted(paths)
    ]


def _copy(source: Path, destination: Path) -> Path:
    _ = destination.parent.mkdir(parents=True, exist_ok=True)
    _ = shutil.copy2(source, destination)
    return destination


def _v17_input(v17: Path, direct_name: str, sealed_name: str) -> Path:
    direct = v17 / direct_name
    if direct.is_file():
        return direct
    return v17 / "inputs" / sealed_name


def candidate_source_paths(
    base_manifest: dict[str, JsonValue], v17: Path
) -> list[Path]:
    source_root = Path(__file__).resolve().parents[1]
    paths: set[Path] = set()
    for raw in require_list(
        base_manifest.get("source_inventory"), "base source inventory"
    ):
        entry = require_object(raw, "base source inventory")
        relative = Path(require_string(entry.get("path"), "base source path"))
        paths.add(Path(*relative.parts[2:]))
    patch_manifest = _v17_input(v17, "patch-tree.sha256", "v17-patch-tree.sha256")
    for line in patch_manifest.read_text(encoding="utf-8").splitlines():
        _, absolute = line.split("  ", maxsplit=1)
        parts = Path(absolute).parts
        patch_index = parts.index("patch-tree")
        paths.add(Path(*parts[patch_index + 1 :]))
    paths.update({
        Path("cron/physique_inline_card.py"),
        Path("cron/scheduler.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_finalization.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_authority.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_clarification_policy.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_clarification_rules.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_contract.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_finalize_service.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_models.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_input_help.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_projection.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_reconciliation.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_onboarding_session.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_readiness_contract.py"),
        Path("dualcoach/profile/checkin_cli/nutrition_readiness_validators.py"),
        Path("dualcoach/profile/checkin_cli/wizard.py"),
        Path("dualcoach/profile/checkin_cli/wizard_models.py"),
        Path("dualcoach/profile/checkin_cli/wizard_storage.py"),
        Path("gateway/platforms/dualcoach_activation_cutover.py"),
        Path("gateway/platforms/dualcoach_admin.py"),
        Path("gateway/platforms/dualcoach_customer_invite.py"),
        Path("gateway/platforms/nutrition_coaching.py"),
        Path("gateway/platforms/nutrition_onboarding_reconciliation.py"),
        Path("gateway/platforms/nutrition_onboarding_reconciliation_copy.py"),
        Path("gateway/platforms/physique_checkin.py"),
        Path("gateway/platforms/physique_checkin_bindings.py"),
        Path("gateway/platforms/telegram.py"),
        Path("gateway/platforms/telegram_checkin_stepper_transport.py"),
        Path("gateway/platforms/telegram_customer_bootstrap.py"),
        Path("gateway/platforms/telegram_physique_checkin_stepper.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_copy.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_customer_v2.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime_callback.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime_collection.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime_notice.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime_publication.py"),
        Path("gateway/platforms/telegram_nutrition_onboarding_runtime_publication_transport.py"),
        Path("gateway/platforms/task26_candidate_authority.py"),
        Path("gateway/platforms/task26_runtime_authority.py"),
        Path("gateway/platforms/nutrition_weekly_reminder_authority.py"),
        Path("gateway/platforms/nutrition_weekly_reminder_bootstrap_customers.py"),
        Path("gateway/platforms/nutrition_weekly_reminder_owner_factory.py"),
        Path("scripts/nutricoach_v150_concrete_host.py"),
        Path("scripts/nutricoach_v150_controller_worker.py"),
        Path("scripts/execute_nutricoach_v150_sealed_live.py"),
        Path("scripts/nutricoach_v150_detached_bootstrap.py"),
        Path("scripts/nutricoach_v150_host_operations.py"),
        Path("scripts/nutricoach_v150_live_upgrade_boundary.py"),
        Path("scripts/nutricoach_v150_live_upgrade_common.py"),
        Path("scripts/nutricoach_v150_live_authority.py"),
        Path("scripts/nutricoach_v150_live_models.py"),
        Path("scripts/nutricoach_v150_live_transaction.py"),
        Path("scripts/nutricoach_v150_runtime_ops.py"),
        Path("scripts/nutricoach_v150_phase_journal.py"),
        Path("scripts/prepare_nutricoach_v150_v15_preseal.py"),
        Path("scripts/rehearse_nutricoach_v150_v15.py"),
        Path("scripts/nutricoach_v150_sealed_authority.py"),
        Path("scripts/nutricoach_v150_sealed_target.py"),
        Path("scripts/nutricoach_v150_sealed_controller.py"),
        Path("scripts/nutricoach_v150_weekly_authority.py"),
        Path("scripts/nutricoach_v150_weekly_startup_smoke.py"),
        Path("scripts/seal_nutricoach_v150_candidate.py"),
        Path("scripts/verify_nutricoach_v150_candidate.py"),
        Path("scripts/verify_nutricoach_v150_candidate_inputs.py"),
        Path("scripts/verify_nutricoach_v150_preseal_v15.py"),
        Path("tests/gateway/_nutrition_weekly_reminder_support.py"),
        Path("tests/nutricoach_v150_transaction_support.py"),
        Path("tests/test_nutricoach_v150_sealed_controller.py"),
        Path("tests/test_nutricoach_v150_v3_hardening.py"),
        Path("tests/test_nutricoach_v150_v14_safety.py"),
        Path("tests/test_nutricoach_v150_v15_authority_adversarial.py"),
        Path("tests/test_nutricoach_v150_v15_closure.py"),
        Path("tests/test_nutricoach_v150_v15_safety.py"),
        Path("tests/test_nutricoach_v150_weekly_authority.py"),
        Path("tests/test_verify_nutricoach_v150_candidate.py"),
    })
    missing = sorted(str(path) for path in paths if not (source_root / path).is_file())
    if missing:
        raise FileNotFoundError(f"candidate source members missing: {missing}")
    return sorted(paths)


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    _ = parser.add_argument("--base", required=True, type=Path)
    _ = parser.add_argument("--v17-evidence", required=True, type=Path)
    _ = parser.add_argument("--successor", required=True, type=Path)
    _ = parser.add_argument("--build-1-hermes", required=True, type=Path)
    _ = parser.add_argument("--build-1-profile", required=True, type=Path)
    _ = parser.add_argument("--build-2-hermes", required=True, type=Path)
    _ = parser.add_argument("--build-2-profile", required=True, type=Path)
    _ = parser.add_argument("--receipts-root", required=True, type=Path)
    _ = parser.add_argument("--successor-overlay", 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.v17_evidence: Path = Path()
        self.successor: Path = Path()
        self.build_1_hermes: Path = Path()
        self.build_1_profile: Path = Path()
        self.build_2_hermes: Path = Path()
        self.build_2_profile: Path = Path()
        self.receipts_root: Path = Path()
        self.successor_overlay: Path = Path()


def main() -> int:
    args = _parser().parse_args(namespace=CliNamespace())
    source_root = Path(__file__).resolve().parents[1]
    successor = args.successor.resolve()
    if successor.exists() or successor.is_symlink():
        raise RuntimeError("successor_exists")
    _ = successor.mkdir(parents=True)

    base_manifest = require_object(load_json(args.base), "base manifest")
    source_files = [
        _copy(source_root / relative, successor / "snapshot/source" / relative)
        for relative in candidate_source_paths(base_manifest, args.v17_evidence)
    ]
    input_sources = {
        "inputs/base-manifest.json": args.base,
        "inputs/v17-evidence-files.sha256": _v17_input(
            args.v17_evidence,
            "evidence-files.sha256",
            "v17-evidence-files.sha256",
        ),
        "inputs/v17-exact-source-set.sha256": _v17_input(
            args.v17_evidence,
            "exact-current-source-set.final.sha256",
            "v17-exact-source-set.sha256",
        ),
        "inputs/v17-patch-tree.sha256": _v17_input(
            args.v17_evidence,
            "patch-tree.sha256",
            "v17-patch-tree.sha256",
        ),
        "inputs/v17-sandbox.diff": _v17_input(
            args.v17_evidence,
            "sandbox.diff",
            "v17-sandbox.diff",
        ),
        "inputs/successor-overlay.sha256": args.successor_overlay,
    }
    input_files = [
        _copy(source, successor / relative)
        for relative, source in input_sources.items()
    ]
    wheel_sources = {
        "artifacts/build-1/hermes_agent-0.17.0-py3-none-any.whl": args.build_1_hermes,
        "artifacts/build-1/physique_checkin_cli-0.1.0-py3-none-any.whl": args.build_1_profile,
        "artifacts/build-2/hermes_agent-0.17.0-py3-none-any.whl": args.build_2_hermes,
        "artifacts/build-2/physique_checkin_cli-0.1.0-py3-none-any.whl": args.build_2_profile,
    }
    wheel_files = [
        _copy(source, successor / relative)
        for relative, source in wheel_sources.items()
    ]
    receipt_files = [
        _copy(path, successor / "receipts" / path.relative_to(args.receipts_root))
        for path in sorted(args.receipts_root.rglob("*"))
        if path.is_file()
    ]

    source_inventory = _inventory(successor, source_files)
    input_inventory = _inventory(successor, input_files)
    wheel_inventory = _inventory(successor, wheel_files)
    evidence_inventory = _inventory(successor, receipt_files)
    hermes_digest = sha256_file(wheel_files[0])
    profile_digest = sha256_file(wheel_files[1])
    interpreter = Path(sys.executable).resolve()
    derivation_inputs: dict[str, JsonValue] = {
        "base_candidate_digest": require_string(
            require_object(base_manifest["identities"], "base identities").get(
                "candidate_digest"
            ),
            "base candidate digest",
        ),
        "base_manifest_sha256": sha256_file(args.base),
        "evidence_digest": inventory_digest(evidence_inventory),
        "hermes_wheel_sha256": hermes_digest,
        "interpreter_sha256": sha256_file(interpreter),
        "profile_wheel_sha256": profile_digest,
        "source_tree_digest": inventory_digest(source_inventory),
        "successor_overlay_sha256": sha256_file(args.successor_overlay),
        "v17_evidence_manifest_sha256": sha256_file(
            _v17_input(
                args.v17_evidence,
                "evidence-files.sha256",
                "v17-evidence-files.sha256",
            )
        ),
        "v17_patch_tree_sha256": sha256_file(
            _v17_input(
                args.v17_evidence,
                "patch-tree.sha256",
                "v17-patch-tree.sha256",
            )
        ),
        "v17_sandbox_diff_sha256": sha256_file(
            _v17_input(
                args.v17_evidence,
                "sandbox.diff",
                "v17-sandbox.diff",
            )
        ),
        "v17_source_set_sha256": sha256_file(
            _v17_input(
                args.v17_evidence,
                "exact-current-source-set.final.sha256",
                "v17-exact-source-set.sha256",
            )
        ),
    }
    candidate_digest = sha256_bytes(canonical(derivation_inputs))
    source_values: list[JsonValue] = list(source_inventory)
    input_values: list[JsonValue] = list(input_inventory)
    wheel_values: list[JsonValue] = list(wheel_inventory)
    evidence_values: list[JsonValue] = list(evidence_inventory)
    manifest: dict[str, JsonValue] = {
        "schema": "nutricoach-v150-combined-candidate-manifest-v1",
        "status": "QUALIFIED_PENDING_LIVE_AUTHORIZATION",
        "candidate_digest": candidate_digest,
        "capabilities": {
            "nutricoach_channel_inbox_v1": {
                "compiled": True,
                "configured": False,
                "authorized": False,
            },
            "nutricoach_multi_customer_v1": {
                "compiled": True,
                "configured": False,
                "authorized": False,
                "authorized_capacity": 0,
                "post_migration_capacity": 5,
            },
        },
        "derivation_inputs": derivation_inputs,
        "source_inventory": source_values,
        "input_inventory": input_values,
        "wheel_inventory": wheel_values,
        "evidence_inventory": evidence_values,
        "builds": [
            {
                "hermes": wheel_inventory[0]["path"],
                "profile": wheel_inventory[1]["path"],
            },
            {
                "hermes": wheel_inventory[2]["path"],
                "profile": wheel_inventory[3]["path"],
            },
        ],
        "interpreter": {
            "path": str(interpreter),
            "sha256": sha256_file(interpreter),
            "version": sys.version,
        },
    }
    _ = (successor / "manifest.json").write_bytes(canonical(manifest) + b"\n")
    qualification: dict[str, JsonValue] = {
        "candidate_digest": candidate_digest,
        "status": "QUALIFIED_PENDING_LIVE_AUTHORIZATION",
    }
    _ = (successor / "qualification.json").write_bytes(canonical(qualification) + b"\n")
    for path in successor.rglob("*"):
        if path.is_file() and not path.is_symlink():
            if path.suffix in {".pyc", ".pyo"} or "__pycache__" in path.parts:
                raise RuntimeError("candidate bytecode is forbidden")
            path.chmod(0o444)
    for path in sorted(
        (entry for entry in successor.rglob("*") if entry.is_dir()),
        reverse=True,
    ):
        path.chmod(0o555)
    successor.chmod(0o555)
    print(candidate_digest)
    return 0


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