"""Bridge the generic r71b phase machine to the concrete sealed controller."""

from __future__ import annotations

import hashlib
from collections.abc import Sequence
from dataclasses import replace
from pathlib import Path
from typing import Protocol, final

from pydantic import JsonValue, TypeAdapter, ValidationError

from scripts.nutricoach_v150_sealed_target import HostPaths, Service

from scripts.nutricoach_v150_phase_journal import PhaseJournal
from scripts.nutricoach_v150_r71b_cron_watch import CronJobsEventWatcher
from scripts.nutricoach_v150_r71b_maintenance_host import (
    R71bMaintenanceFiles,
    R71bMaintenancePaths,
)
from scripts.nutricoach_v150_r71b_maintenance_transaction import (
    KNOWN_R70_CRON_ERROR,
    KNOWN_R70_CRON_ERROR_EVIDENCE_SHA256,
    KNOWN_R70_CRON_JOB_ID,
    KNOWN_R70_CRON_JOB_NAME,
    KNOWN_R70_CRON_SCHEDULE,
    KNOWN_R70_RUNTIME_CANDIDATE_DIGEST,
    R71bMaintenanceController,
    R71bMaintenanceProof,
)
from scripts.nutricoach_v150_r71b_maintenance_confirmation import (
    ArmedCronWatcher,
    MaintenanceConfirmation,
)

_OBJECT = TypeAdapter(dict[str, JsonValue])


class MaintenanceRuntimeHost(Protocol):
    """The post-migration concrete host operations used by this narrow adapter."""

    @property
    def provider_events(self) -> Sequence[str]: ...

    @property
    def network_events(self) -> Sequence[str]: ...

    def start(self) -> None: ...


class BoundMaintenanceRuntimeHost(MaintenanceRuntimeHost, Protocol):
    """The concrete host fields which bind r71b files to one profile and service."""

    @property
    def candidate_digest(self) -> str: ...

    @property
    def paths(self) -> HostPaths: ...

    @property
    def service(self) -> Service: ...


class MaintenanceFiles(Protocol):
    """The exact one-use hold/credential file transaction."""

    def arm(self) -> None: ...

    def confirm(
        self,
        watcher: ArmedCronWatcher,
        evidence: MaintenanceConfirmation,
    ) -> None: ...

    def recover_confirmed(self, evidence: MaintenanceConfirmation) -> None: ...

    def rollback(self) -> None: ...

    def finalize(self) -> None: ...


class StartWatcher(ArmedCronWatcher, Protocol):
    """An inotify subscription with an explicit deterministic cleanup operation."""

    def arm(self) -> None: ...

    def close(self) -> None: ...


@final
class R71bSealedMaintenanceHost:
    """Persist maintenance phases while delegating byte writes to focused modules."""

    def __init__(
        self,
        host: MaintenanceRuntimeHost,
        files: MaintenanceFiles,
        watcher: StartWatcher,
        evidence: MaintenanceConfirmation,
        journal: PhaseJournal,
    ) -> None:
        self._host: MaintenanceRuntimeHost = host
        self._files: MaintenanceFiles = files
        self._watcher: StartWatcher = watcher
        self._evidence: MaintenanceConfirmation = evidence
        self._journal: PhaseJournal = journal

    def reserve(self) -> None:
        """The outer sealed controller has already consumed the global reservation."""

    def arm_maintenance(self) -> None:
        """Install exact credential and active hold after the service stop snapshot."""
        self._files.arm()
        self._journal.advance("MAINTENANCE_ARMED")

    def arm_start_watch(self) -> None:
        """Subscribe to cron replacement before the successor can run its first tick."""
        self._watcher.arm()
        self._journal.advance("START_WATCH_ARMED")

    def start_successor(self) -> None:
        """Start only after the hold and watcher are both durable."""
        self._host.start()
        self._journal.advance("STARTED")

    def confirm_maintenance(self) -> None:
        """Verify completion against live event counters captured after successor start."""
        self._files.confirm(
            self._watcher,
            replace(
                self._evidence,
                provider_calls=len(self._host.provider_events),
                network_calls=len(self._host.network_events),
            ),
        )
        self._journal.advance("MAINTENANCE_CONFIRMED")

    def commit(self) -> None:
        """Remove temporary capability material, then close the event subscription."""
        self._files.finalize()
        self._watcher.close()

    def pause_predecessor_scheduler(self) -> None:
        """Restore the stopped snapshot then persist a disabled r70 scheduler job."""
        self._files.rollback()
        self._watcher.close()
        self._journal.advance("ROLLED_BACK_SAFE_CRON_PAUSED")

    def restore_predecessor(self) -> None:
        """The outer rollback guard restores the full predecessor snapshot afterward."""

    def recover_confirmed_maintenance(self) -> None:
        """Reverify the durable confirmation before consuming its pending reservation."""
        self._files.recover_confirmed(
            replace(
                self._evidence,
                provider_calls=len(self._host.provider_events),
                network_calls=len(self._host.network_events),
            )
        )

    def phase(self) -> str | None:
        """Expose the exact persisted phase for recovery routing and tests."""
        return self._journal.phase()


class R71bSealedBindingError(RuntimeError):
    """The sealed target does not bind every required maintenance artifact."""


def build_r71b_maintenance_controller(
    host: BoundMaintenanceRuntimeHost,
    binding: dict[str, JsonValue],
    journal: PhaseJournal,
    *,
    require_r70_proof: bool = True,
) -> R71bMaintenanceController:
    """Read exact authority bytes and construct the only permitted maintenance host."""
    authority_path = Path(_text(binding, "maintenance_authority_path"))
    authority_sha256 = _text(binding, "maintenance_authority_sha256")
    authority_bytes = authority_path.read_bytes()
    if hashlib.sha256(authority_bytes).hexdigest() != authority_sha256:
        raise R71bSealedBindingError("maintenance_authority")
    files = R71bMaintenanceFiles(
        R71bMaintenancePaths(
            host.paths.profile,
            host.paths.successor_runtime,
            host.paths.profile / "cron/jobs.json",
            host.paths.dropin,
        ),
        host.service,
        authority_bytes,
    )
    if (
        files.authority.package_binding_digest != _text(binding, "package_binding_digest")
        or files.authority.hold.candidate_digest != host.candidate_digest
    ):
        raise R71bSealedBindingError("maintenance_binding")
    evidence = MaintenanceConfirmation(
        Path(_text(binding, "maintenance_ledger_path")),
        _text(binding, "maintenance_ledger_sha256"),
        Path(_text(binding, "maintenance_output_directory")),
        0,
        0,
    )
    proof = _read_r70_proof(host, binding, evidence) if require_r70_proof else None
    adapter = R71bSealedMaintenanceHost(
        host,
        files,
        CronJobsEventWatcher((host.paths.profile / "cron").resolve()),
        evidence,
        journal,
    )
    return R71bMaintenanceController(
        adapter,
        proof,
    )


def _read_r70_proof(
    host: BoundMaintenanceRuntimeHost,
    binding: dict[str, JsonValue],
    evidence: MaintenanceConfirmation,
) -> R71bMaintenanceProof:
    """Read every exception fact from its bound file; no proof field is fabricated."""
    cron_path = host.paths.profile / "cron/jobs.json"
    try:
        document = _OBJECT.validate_json(cron_path.read_bytes())
    except (OSError, ValidationError) as error:
        raise R71bSealedBindingError("r70_cron") from error
    rows = document.get("jobs")
    if not isinstance(rows, list):
        raise R71bSealedBindingError("r70_cron")
    matches = [row for row in rows if isinstance(row, dict) and row.get("id") == KNOWN_R70_CRON_JOB_ID]
    if len(matches) != 1:
        raise R71bSealedBindingError("r70_cron")
    job = matches[0]
    if (
        job.get("name") != KNOWN_R70_CRON_JOB_NAME
        or job.get("schedule_display") != KNOWN_R70_CRON_SCHEDULE
        or job.get("last_error") != KNOWN_R70_CRON_ERROR
    ):
        raise R71bSealedBindingError("r70_cron")
    baseline = binding.get("authority_baseline")
    if not isinstance(baseline, dict):
        raise R71bSealedBindingError("r70_runtime")
    candidate_path = Path(_text(baseline, "candidate_path"))
    evidence_path = Path(_text(binding, "r70_error_evidence_path"))
    try:
        runtime_candidate = candidate_path.read_text(encoding="utf-8").strip()
        evidence_sha256 = hashlib.sha256(evidence_path.read_bytes()).hexdigest()
        ledger_sha256 = hashlib.sha256(evidence.ledger_path.read_bytes()).hexdigest()
    except OSError as error:
        raise R71bSealedBindingError("r70_proof_file") from error
    if (
        runtime_candidate != KNOWN_R70_RUNTIME_CANDIDATE_DIGEST
        or evidence_sha256 != KNOWN_R70_CRON_ERROR_EVIDENCE_SHA256
        or evidence_sha256 != _text(binding, "r70_error_evidence_sha256")
        or ledger_sha256 != evidence.ledger_sha256
    ):
        raise R71bSealedBindingError("r70_exception_drift")
    return R71bMaintenanceProof(
        candidate_digest=host.candidate_digest,
        cron_job_id=_text(job, "id"),
        cron_job_name=_text(job, "name"),
        cron_schedule=_text(job, "schedule_display"),
        cron_error=_text(job, "last_error"),
        cron_error_evidence_sha256=evidence_sha256,
        cron_ledger_sha256=ledger_sha256,
        expected_cron_ledger_sha256=evidence.ledger_sha256,
        runtime_candidate_digest=runtime_candidate,
        expected_candidate_digest=_text(binding, "candidate_digest"),
        final_package_digest=_text(binding, "package_digest"),
    )


def _text(binding: dict[str, JsonValue], label: str) -> str:
    value = binding.get(label)
    if not isinstance(value, str):
        raise R71bSealedBindingError(f"sealed_target:{label}")
    return value
