from __future__ import annotations

import hashlib
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from typing import Protocol

from k1link.simulation.contracts import QualificationRun
from k1link.simulation.orchestrator import WorkerStartResult, WorkerStopResult
from k1link.simulation.process_supervisor import (
    PosixProcessSupervisor,
    ProcessSpec,
)
from k1link.simulation.s0 import S0Profile, load_s0_profile


class WorkerAdmissionError(RuntimeError):
    """The worker no longer matches the accepted S0 generation or D boundary."""


@dataclass(frozen=True, slots=True)
class WorkerAdmission:
    profile: S0Profile
    profile_sha256: str
    artifact_run_root: PurePosixPath
    process_run_root: PurePosixPath


class SimulationWorldControl(Protocol):
    def pause(self, run_id: str) -> None: ...

    def resume(self, run_id: str) -> None: ...

    def step(self, run_id: str, step_count: int) -> int: ...


class S0WorkerGuard:
    """Bind every S1 start to the exact accepted S0 profile and D artifact root."""

    def __init__(self, profile_path: Path) -> None:
        self.profile_path = profile_path.expanduser().resolve()
        self.profile = load_s0_profile(self.profile_path)
        self.profile_sha256 = hashlib.sha256(self.profile_path.read_bytes()).hexdigest()

    def admit(
        self,
        run: QualificationRun,
        artifact_run_root: PurePosixPath,
        process_run_root: PurePosixPath,
    ) -> WorkerAdmission:
        if run.host_profile_id != self.profile.profile_id:
            raise WorkerAdmissionError("run host profile id does not match accepted S0")
        if run.host_profile_sha256 != self.profile_sha256:
            raise WorkerAdmissionError("run host profile digest does not match accepted S0")
        artifact_roots = tuple(
            item.wsl_path
            for item in self.profile.storage.mutable_paths
            if item.identifier == "artifacts"
        )
        if len(artifact_roots) != 1:
            raise WorkerAdmissionError("accepted S0 profile has no unique artifact root")
        runtime_roots = tuple(
            item.wsl_path
            for item in self.profile.storage.mutable_paths
            if item.identifier == "runtime"
        )
        if len(runtime_roots) != 1:
            raise WorkerAdmissionError("accepted S0 profile has no unique runtime root")
        expected_artifact_parent = artifact_roots[0] / "s1" / "runs"
        expected_runtime_parent = runtime_roots[0] / "s1" / "runs"
        if artifact_run_root != expected_artifact_parent / run.run_id:
            raise WorkerAdmissionError("S1 run root escapes the accepted D-only layout")
        if process_run_root != expected_runtime_parent / run.run_id:
            raise WorkerAdmissionError("S1 process root escapes the accepted D-only layout")
        if not all(
            component.qualification_state == "accepted" for component in self.profile.components
        ):
            raise WorkerAdmissionError("S0 component generation is no longer accepted")
        return WorkerAdmission(
            profile=self.profile,
            profile_sha256=self.profile_sha256,
            artifact_run_root=artifact_run_root,
            process_run_root=process_run_root,
        )


class LocalProcessWorkerAdapter:
    """Worker-local S1B adapter; transport and PX4 mapping remain separate ports."""

    def __init__(
        self,
        guard: S0WorkerGuard,
        supervisor: PosixProcessSupervisor,
        specs: tuple[ProcessSpec, ...],
        world_control: SimulationWorldControl,
    ) -> None:
        self.guard = guard
        self.supervisor = supervisor
        self.specs = specs
        self.world_control = world_control

    def start(self, run: QualificationRun) -> WorkerStartResult:
        artifact_root = next(
            item.wsl_path
            for item in self.guard.profile.storage.mutable_paths
            if item.identifier == "artifacts"
        )
        artifact_run_root = artifact_root / "s1" / "runs" / run.run_id
        process_run_root = PurePosixPath(self.supervisor.runtime_root.as_posix()) / run.run_id
        admission = self.guard.admit(run, artifact_run_root, process_run_root)
        records = self.supervisor.start(run.run_id, self.specs)
        return WorkerStartResult(
            provider_ids=tuple(record.process_id for record in records),
            profile_sha256=admission.profile_sha256,
        )

    def pause(self, run: QualificationRun) -> None:
        self.world_control.pause(run.run_id)

    def resume(self, run: QualificationRun) -> None:
        self.world_control.resume(run.run_id)

    def step(self, run: QualificationRun, step_count: int) -> int:
        return self.world_control.step(run.run_id, step_count)

    def stop(self, run: QualificationRun) -> WorkerStopResult:
        result = self.supervisor.stop()
        return WorkerStopResult(
            stopped_provider_ids=result.stopped_process_ids,
            residue_provider_ids=result.residue_process_ids,
        )

    def reconcile(self, run: QualificationRun) -> WorkerStopResult:
        return self.stop(run)
