"""Concrete filesystem, installer, migration, and service adapter."""

from __future__ import annotations

import json
import shutil
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Self, final

from pydantic import JsonValue, TypeAdapter

from scripts.nutricoach_v150_live_models import (
    HERMES_WHEEL_SHA256,
    PROFILE_WHEEL_SHA256,
)
from scripts.nutricoach_v150_host_operations import (
    apply_migrations,
    capacity_after,
    post_fence,
    protected_paths,
    service_state,
    snapshot_paths,
    switch_postimages,
    verify_postimages,
)
from scripts.nutricoach_v150_live_upgrade_boundary import clean_boundary
from scripts.nutricoach_v150_sealed_authority import atomic_write, digest
from scripts.nutricoach_v150_runtime_ops import (
    channel_inbox_enabled,
    install_fresh_runtime,
    make_writable,
    remove_tree,
    run_installed_weekly_startup_smoke,
)
from scripts.nutricoach_v150_sealed_target import (
    PROFILE_ROOT,
    DisposableService,
    HostError,
    HostPaths,
    Service,
    SystemdService,
)

_OBJECT = TypeAdapter(dict[str, JsonValue])


def _text(value: JsonValue | None, label: str) -> str:
    if not isinstance(value, str):
        raise HostError(f"sealed_target:{label}")
    return value


@final
class ConcreteLiveHost:
    """Actual target adapter with no provider, Telegram, or network calls."""

    def __init__(
        self,
        paths: HostPaths,
        service: Service,
        *,
        live: bool,
        fault: BaseException | None = None,
        fault_stage: str = "install_exact_wheels",
        cleanup_fault: BaseException | None = None,
    ) -> None:
        self.paths = paths
        self.service = service
        self.live = live
        self.fault = fault
        self.fault_stage = fault_stage
        self.cleanup_fault = cleanup_fault
        self.stages: list[str] = []
        self.rollback_failures: list[str] = []
        self.clean_boundary_checks = 0
        self.network_events: list[str] = []
        self.telegram_events: list[str] = []
        self.provider_events: list[str] = []
        self._preflight: dict[Path, str] | None = None
        self._migration_after: bytes | None = None
        self._postimages: dict[Path, str] = {}

    @classmethod
    def disposable(
        cls,
        root: Path,
        service: DisposableService,
        *,
        fault: BaseException | None = None,
        fault_stage: str = "install_exact_wheels",
        cleanup_fault: BaseException | None = None,
    ) -> Self:
        profile = root / "profile"
        paths = HostPaths(
            profile,
            profile / "customers/registry.json",
            profile / "config.yaml",
            root / "gateway.service",
            root / "authority.conf",
            profile / ".strict-runtime/current/venv",
            profile / ".strict-runtime/successor/venv",
            root / "sealed-execution",
            root / "global-authorization",
        )
        return cls(
            paths,
            service,
            live=False,
            fault=fault,
            fault_stage=fault_stage,
            cleanup_fault=cleanup_fault,
        )

    @classmethod
    def live_target(cls, sealed_target: Path) -> Self:
        binding = _OBJECT.validate_json(sealed_target.read_bytes())
        paths = HostPaths(
            Path(_text(binding.get("profile_root"), "profile_root")),
            PROFILE_ROOT / "customers/registry.json",
            PROFILE_ROOT / "config.yaml",
            Path(_text(binding.get("unit"), "unit")),
            Path(_text(binding.get("dropin"), "dropin")),
            Path(_text(binding.get("current_runtime"), "current_runtime")),
            Path(_text(binding.get("successor_runtime"), "successor_runtime")),
            Path(_text(binding.get("execution_root"), "execution_root")),
            Path(_text(binding.get("global_approval_ledger"), "ledger")),
            Path(_text(binding.get("protected_inventory"), "inventory")),
        )
        return cls(paths, SystemdService(), live=True)

    @property
    def successor_root(self) -> Path:
        return self.paths.successor_runtime.parent

    @property
    def weekly_authority(self) -> Path:
        return self.paths.profile / "data/weekly-operations-authority"

    def _stage(self, name: str) -> None:
        self.stages.append(name)
        if self.fault is not None and self.fault_stage == name:
            error, self.fault = self.fault, None
            raise error

    def capture_preflight(self) -> None:
        self._preflight = {
            path: digest(path.read_bytes()) for path in protected_paths(self.paths)
        }

    def verify_preflight(self) -> None:
        if self._preflight is None:
            raise HostError("preflight_missing")
        if any(
            digest(path.read_bytes()) != expected
            for path, expected in self._preflight.items()
        ):
            raise HostError("protected_drift")

    def stop(self) -> None:
        self.service.stop()
        if self.service.running:
            raise HostError("service_stop")
        self._stage("stop")

    def record_snapshot(self) -> None:
        self._stage("post_stop_snapshot")

    def stopped_probe(self) -> None:
        self.verify_preflight()
        if not (self.paths.current_runtime / "bin/python").is_file():
            raise HostError("current_runtime")
        self._stage("stopped_probe")

    def install(self) -> None:
        self.verify_created_absent()
        if self.successor_root.exists():
            raise HostError("successor_exists")
        if self.live:
            _ = install_fresh_runtime(self.paths.successor_runtime)
        else:
            _ = shutil.copytree(self.paths.current_runtime.parent, self.successor_root)
            make_writable(self.successor_root)
            receipt = {
                "hermes": HERMES_WHEEL_SHA256,
                "profile": PROFILE_WHEEL_SHA256,
            }
            atomic_write(
                self.successor_root / "installed-wheels.json",
                json.dumps(receipt, sort_keys=True).encode() + b"\n",
            )
        self._stage("install_exact_wheels")

    def off_smoke(self) -> None:
        if channel_inbox_enabled(self.paths.config.read_text(encoding="utf-8")):
            raise HostError("channel_inbox_on")
        self._stage("off_smoke_channel_inbox_off")

    def semantic_clean_boundary(self) -> None:
        if self.live:
            _ = clean_boundary(self.paths.profile, datetime.now(UTC))
        self.clean_boundary_checks += 1

    def snapshot_paths(self) -> tuple[Path, ...]:
        return snapshot_paths(self.paths)

    def verify_created_absent(self) -> None:
        for path in (self.successor_root, self.weekly_authority):
            if path.exists() or path.is_symlink():
                raise HostError("created_path")

    def verify_created_owned(self) -> None:
        expected_uid = self.paths.profile.stat().st_uid
        expected_gid = self.paths.profile.stat().st_gid
        for path in (self.successor_root, self.weekly_authority):
            info = path.stat(follow_symlinks=False)
            if (
                path.is_symlink()
                or not path.is_dir()
                or info.st_uid != expected_uid
                or info.st_gid != expected_gid
            ):
                raise HostError("created_path_ownership")

    @staticmethod
    def install_fresh_runtime(runtime: Path) -> dict[str, str]:
        return install_fresh_runtime(runtime)

    def migration_dry_run(self) -> None:
        self._migration_after = capacity_after(self.paths.registry)
        self._stage("capacity_dry_run")

    def migration_apply(self) -> None:
        if self._migration_after is None:
            raise HostError("capacity_dry_run_missing")
        apply_migrations(self.paths, self._migration_after)
        self.verify_created_owned()
        self._stage("weekly_and_capacity_apply")

    def weekly_startup_smoke(self) -> None:
        runtime = (
            self.paths.successor_runtime
            if self.live
            else Path(sys.executable).resolve().parent.parent
        )
        run_installed_weekly_startup_smoke(
            runtime,
            self.paths.profile,
            self.paths.config,
            source_dependencies=not self.live,
        )
        self._stage("weekly_startup_smoke")

    def switch_systemd(self) -> None:
        self._postimages = switch_postimages(self.paths)
        self._stage("switch_unit_dropin")

    def reload(self) -> None:
        self.service.reload()
        self._stage("reload")

    def start(self) -> None:
        self.service.start()
        self._stage("start")

    def post_fence(self) -> None:
        post_fence(self.paths, self.service_state(), self._postimages)
        self._stage("post_fence")

    def remove_created(self) -> None:
        for path in (self.successor_root, self.weekly_authority):
            remove_tree(path)
        if self.cleanup_fault is not None:
            error, self.cleanup_fault = self.cleanup_fault, None
            raise error

    def service_state(self) -> dict[str, str]:
        return service_state(self.service, self.paths)

    def verify_exact_postimages(self) -> bool:
        return verify_postimages(self._postimages)

    def ledger_reserved(self) -> bool:
        return (self.paths.ledger_root / "authorization-reserved.json").exists()

    def ledger_consumed(self) -> bool:
        return (self.paths.ledger_root / "authorization-consumed.json").exists()
