"""Typed runtime boundaries for the Task22 canonical-launcher tests."""

from __future__ import annotations

import importlib.util
import json
import shutil
import subprocess
import sys
from pathlib import Path
from types import ModuleType
from collections.abc import Callable
from typing import Protocol, TypeGuard, cast, final

SnapshotEntries = dict[str, list[object]]


class LauncherProtocol(Protocol):
    """Validated public adapter around the launcher's dynamic surface."""

    bootstrap: bytes

    def sealed_memfd(self, name: str, content: bytes) -> object: ...

    def source_snapshots(self) -> object: ...

    def source_closure(self) -> object: ...

    def validate_source_allowlist(self, pins: dict[str, str]) -> object: ...

    def strict_environment(self) -> object: ...

    def run_snapshot_child(
        self,
        entries: SnapshotEntries,
        cli_descriptor: int,
        cli_digest: str,
        after_fork: Callable[[], None] | None,
        lock_handoff: tuple[Path, Path, int, dict[str, object]] | None = None,
    ) -> object: ...


@final
class _LauncherAdapter:
    def __init__(
        self,
        bootstrap: bytes,
        sealed: Callable[[str, bytes], object],
        snapshots: Callable[[], object],
        closure: Callable[[], object],
        validate_allowlist: Callable[[dict[str, str]], object],
        environment: Callable[[], object],
        runner: Callable[[SnapshotEntries, int, str, Callable[[], None] | None, tuple[Path, Path, int, dict[str, object]] | None], object],
    ) -> None:
        self.bootstrap: bytes = bootstrap
        self._sealed: Callable[[str, bytes], object] = sealed
        self._snapshots: Callable[[], object] = snapshots
        self._closure: Callable[[], object] = closure
        self._validate_allowlist = validate_allowlist
        self._environment: Callable[[], object] = environment
        self._runner: Callable[[SnapshotEntries, int, str, Callable[[], None] | None, tuple[Path, Path, int, dict[str, object]] | None], object] = runner

    def sealed_memfd(self, name: str, content: bytes) -> object:
        return self._sealed(name, content)

    def source_snapshots(self) -> object:
        return self._snapshots()

    def source_closure(self) -> object:
        return self._closure()

    def validate_source_allowlist(self, pins: dict[str, str]) -> object:
        return self._validate_allowlist(pins)

    def strict_environment(self) -> object:
        return self._environment()

    def run_snapshot_child(
        self,
        entries: SnapshotEntries,
        cli_descriptor: int,
        cli_digest: str,
        after_fork: Callable[[], None] | None,
        lock_handoff: tuple[Path, Path, int, dict[str, object]] | None = None,
    ) -> object:
        return self._runner(
            entries, cli_descriptor, cli_digest, after_fork, lock_handoff
        )


def _is_object_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 json_object(content: str | bytes, label: str) -> dict[str, object]:
    """Parse and runtime-check one external JSON object."""
    value = cast(object, json.loads(content))
    if not _is_object_dict(value):
        raise AssertionError(f"{label} must be a JSON object")
    return value


def required_object(value: object, label: str) -> dict[str, object]:
    if not _is_object_dict(value):
        raise AssertionError(f"{label} must be an object")
    return value


def required_str(value: object, label: str) -> str:
    if not isinstance(value, str):
        raise AssertionError(f"{label} must be a string")
    return value


def required_bool(value: object, label: str) -> bool:
    if not isinstance(value, bool):
        raise AssertionError(f"{label} must be a boolean")
    return value


def required_int(value: object, label: str) -> int:
    if not isinstance(value, int) or isinstance(value, bool):
        raise AssertionError(f"{label} must be an integer")
    return value


def load_launcher(path: Path) -> LauncherProtocol:
    """Load the dynamic module and validate its complete test surface."""
    spec = importlib.util.spec_from_file_location("task22_hardened_launcher", path)
    if spec is None or spec.loader is None:
        raise AssertionError("canonical launcher has no import loader")
    module: ModuleType = importlib.util.module_from_spec(spec)
    _ = spec.loader.exec_module(module)
    bootstrap = cast(object, getattr(module, "_BOOTSTRAP", None))
    sealed = cast(object, getattr(module, "_sealed_memfd", None))
    snapshots = cast(object, getattr(module, "_source_snapshots", None))
    closure = cast(object, getattr(module, "_source_closure", None))
    validate_allowlist = cast(object, getattr(module, "_validate_source_allowlist", None))
    environment = cast(object, getattr(module, "_strict_environment", None))
    runner = cast(object, getattr(module, "_run_snapshot_child", None))
    if not isinstance(bootstrap, bytes):
        raise AssertionError("canonical launcher bootstrap must be bytes")
    if not all(callable(item) for item in (sealed, snapshots, closure, validate_allowlist, environment, runner)):
        raise AssertionError("canonical launcher does not expose the required surface")
    typed_sealed = cast(Callable[[str, bytes], object], sealed)
    typed_snapshots = cast(Callable[[], object], snapshots)
    typed_closure = cast(Callable[[], object], closure)
    typed_validate_allowlist = cast(Callable[[dict[str, str]], object], validate_allowlist)
    typed_environment = cast(Callable[[], object], environment)
    typed_runner = cast(
        Callable[[SnapshotEntries, int, str, Callable[[], None] | None, tuple[Path, Path, int, dict[str, object]] | None], object], runner
    )

    def call_sealed(name: str, content: bytes) -> object:
        return typed_sealed(name, content)

    def call_snapshots() -> object:
        return typed_snapshots()

    def call_environment() -> object:
        return typed_environment()

    return _LauncherAdapter(
        bootstrap, call_sealed, call_snapshots, typed_closure,
        typed_validate_allowlist, call_environment, typed_runner
    )


def bootstrap(launcher: LauncherProtocol) -> bytes:
    return launcher.bootstrap


def sealed_memfd(launcher: LauncherProtocol, name: str, content: bytes) -> int:
    descriptor = launcher.sealed_memfd(name, content)
    if not isinstance(descriptor, int) or isinstance(descriptor, bool):
        raise AssertionError("launcher descriptor must be an integer")
    return descriptor


def source_snapshots(launcher: LauncherProtocol) -> tuple[SnapshotEntries, list[int]]:
    """Validate every dynamic snapshot entry and descriptor before use."""
    value = launcher.source_snapshots()
    pair = cast(tuple[object, ...], value)
    if not isinstance(value, tuple) or len(pair) != 2:
        raise AssertionError("launcher snapshots must be a pair")
    raw_entries, raw_descriptors = pair
    if not _is_object_dict(raw_entries) or not isinstance(raw_descriptors, list):
        raise AssertionError("launcher snapshot containers are invalid")
    descriptors: list[int] = []
    for item in cast(list[object], raw_descriptors):
        if not isinstance(item, int) or isinstance(item, bool):
            raise AssertionError("launcher snapshot descriptor must be an integer")
        descriptors.append(item)
    entries: SnapshotEntries = {}
    for module, raw_entry in raw_entries.items():
        if not isinstance(raw_entry, list):
            raise AssertionError(f"snapshot entry {module} must be a descriptor triple")
        fields = cast(list[object], raw_entry)
        if len(fields) != 3:
            raise AssertionError(f"snapshot entry {module} must be a descriptor triple")
        path, digest, package = fields
        if not isinstance(path, str) or not isinstance(digest, str) or not isinstance(package, bool):
            raise AssertionError(f"snapshot entry {module} has invalid field types")
        entries[module] = [path, digest, package]
    return entries, descriptors


def source_closure(launcher: LauncherProtocol) -> dict[str, str]:
    value = launcher.source_closure()
    if not _is_object_dict(value) or not all(
        isinstance(item, str) for item in value.values()
    ):
        raise AssertionError("launcher source closure must be a string mapping")
    return cast(dict[str, str], value)


def validate_source_allowlist(
    launcher: LauncherProtocol, pins: dict[str, str]
) -> None:
    _ = launcher.validate_source_allowlist(pins)


def snapshot_entry(entry: list[object], label: str) -> tuple[str, str, bool]:
    if len(entry) != 3:
        raise AssertionError(f"snapshot entry {label} must contain three fields")
    path, digest, package = entry
    if not isinstance(path, str) or not isinstance(digest, str) or not isinstance(package, bool):
        raise AssertionError(f"snapshot entry {label} has invalid field types")
    return path, digest, package


def strict_environment(launcher: LauncherProtocol) -> dict[str, str]:
    value = launcher.strict_environment()
    if not _is_object_dict(value):
        raise AssertionError("launcher environment must be an object")
    environment: dict[str, str] = {}
    for name, setting in value.items():
        if not isinstance(setting, str):
            raise AssertionError(f"launcher environment value {name} must be a string")
        environment[name] = setting
    return environment


def run_snapshot_child(
    launcher: LauncherProtocol,
    entries: SnapshotEntries,
    cli_descriptor: int,
    cli_digest: str,
    after_fork: Callable[[], None] | None = None,
    lock_handoff: tuple[Path, Path, int, dict[str, object]] | None = None,
) -> int:
    value = launcher.run_snapshot_child(
        entries, cli_descriptor, cli_digest, after_fork, lock_handoff
    )
    if not isinstance(value, int) or isinstance(value, bool):
        raise AssertionError("snapshot child status must be an integer")
    return value


def run_launcher(
    launcher: Path,
    evidence: Path,
    environment: dict[str, str],
    *,
    env: dict[str, str] | None = None,
    flags: tuple[str, ...] = (),
) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [sys.executable, *flags, str(launcher)],
        cwd=evidence, env=environment if env is None else env, text=True,
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False,
    )


def mutate_manifest(root: Path, evidence: Path, mutation: str) -> None:
    manifest_path = root / "dualcoach-trainer-removal-successor-manifest.json"
    if mutation == "obsolete-symlink":
        manifest_path.unlink()
        manifest_path.symlink_to(evidence / "dualcoach-callback-cleanup-successor-manifest.json")
        return
    manifest = json_object(manifest_path.read_text(), "successor manifest")
    if mutation == "leaf":
        candidate_files = manifest.get("candidate_files")
        if not isinstance(candidate_files, list) or not candidate_files:
            raise AssertionError("successor manifest candidate_files must be non-empty")
        typed_files = cast(list[object], candidate_files)
        first = required_object(typed_files[0], "first candidate leaf")
        first["sha256"] = "0" * 64
    else:
        manifest["gateway_status_snapshot_sha256"] = "0" * 64
    _ = manifest_path.write_text(json.dumps(manifest), encoding="utf-8")


def copy_artifacts(evidence: Path, destination: Path, names: tuple[str, ...]) -> None:
    for name in names:
        _ = shutil.copy2(evidence / name, destination / name)


def verify_success_output(content: str, leaves: int) -> None:
    output = json_object(content, "launcher output")
    if required_bool(output.get("ready"), "launcher ready") is not True:
        raise AssertionError("launcher is not ready")
    if required_str(output.get("mode"), "launcher mode") != "verify-only":
        raise AssertionError("launcher mode changed")
    successor = required_object(output.get("successor"), "launcher successor")
    if required_int(successor.get("leaves"), "successor leaves") != leaves:
        raise AssertionError("successor leaf count changed")


def prepare_mutated_artifacts(
    evidence: Path, destination: Path, names: tuple[str, ...], mutation: str
) -> None:
    copy_artifacts(evidence, destination, names)
    mutate_manifest(destination, evidence, mutation)
