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

from __future__ import annotations

import hashlib
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 (
    CANDIDATE_DIGEST,
    HERMES_WHEEL,
    HERMES_WHEEL_SHA256,
    PROFILE_WHEEL,
    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 (
    DisposableService,
    HostError,
    HostPaths,
    Service,
    SystemdService,
)
from scripts.nutricoach_v150_weekly_authority import (
    missing_canonical_weekly_files,
)

_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


def _wheel(value: JsonValue | None, label: str) -> tuple[Path, str]:
    if not isinstance(value, dict):
        raise HostError(f"sealed_target:{label}")
    return (
        Path(_text(value.get("path"), f"{label}_path")),
        _text(value.get("sha256"), f"{label}_sha256"),
    )


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

    def __init__(
        self,
        paths: HostPaths,
        service: Service,
        *,
        live: bool,
        candidate_digest: str = CANDIDATE_DIGEST,
        hermes_wheel: Path = HERMES_WHEEL,
        hermes_wheel_sha256: str = HERMES_WHEEL_SHA256,
        profile_wheel: Path = PROFILE_WHEEL,
        profile_wheel_sha256: str = PROFILE_WHEEL_SHA256,
        registry_sha256: str | None = None,
        protected_inventory_sha256: str | None = None,
        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.candidate_digest = candidate_digest
        self.hermes_wheel = hermes_wheel
        self.hermes_wheel_sha256 = hermes_wheel_sha256
        self.profile_wheel = profile_wheel
        self.profile_wheel_sha256 = profile_wheel_sha256
        self.registry_sha256 = registry_sha256
        self.protected_inventory_sha256 = protected_inventory_sha256
        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._created_files: tuple[Path, ...] = ()
        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())
        candidate_digest = _text(binding.get("candidate_digest"), "candidate_digest")
        wheels = binding.get("wheels")
        if not isinstance(wheels, list) or len(wheels) != 2:
            raise HostError("sealed_target:wheels")
        hermes_wheel, hermes_sha256 = _wheel(wheels[0], "hermes_wheel")
        profile_wheel, profile_sha256 = _wheel(wheels[1], "profile_wheel")
        profile = Path(_text(binding.get("profile_root"), "profile_root"))
        service_name = _text(binding.get("service_name"), "service_name")
        registry_sha256 = _text(binding.get("registry_sha256"), "registry_sha256")
        protected_inventory_sha256 = _text(
            binding.get("protected_inventory_sha256"),
            "protected_inventory_sha256",
        )
        paths = HostPaths(
            profile,
            profile / "customers/registry.json",
            profile / "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(service_name),
            live=True,
            candidate_digest=candidate_digest,
            hermes_wheel=hermes_wheel,
            hermes_wheel_sha256=hermes_sha256,
            profile_wheel=profile_wheel,
            profile_wheel_sha256=profile_sha256,
            registry_sha256=registry_sha256,
            protected_inventory_sha256=protected_inventory_sha256,
        )

    @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:
        if (
            self.protected_inventory_sha256 is not None
            and self.paths.protected_inventory is not None
            and hashlib.sha256(
                self.paths.protected_inventory.read_bytes()
            ).hexdigest()
            != self.protected_inventory_sha256
        ):
            raise HostError("protected_inventory_drift")
        if (
            self.registry_sha256 is not None
            and hashlib.sha256(self.paths.registry.read_bytes()).hexdigest()
            != self.registry_sha256
        ):
            raise HostError("registry_drift")
        self._preflight = {
            path: digest(path.read_bytes()) for path in protected_paths(self.paths)
        }

    def write_recovery_manifest(self) -> None:
        """Persist pre-stop restoration inputs for a fresh recovery process."""
        if self._preflight is None:
            raise HostError("preflight_missing")
        self._created_files = missing_canonical_weekly_files(self.paths)
        payload = {
            "created_files": [str(path) for path in self._created_files],
            "preflight": {
                str(path): expected for path, expected in self._preflight.items()
            },
            "schema": "nutricoach-v150-recovery-manifest-v1",
        }
        atomic_write(
            self.paths.execution_root / "recovery-manifest.json",
            json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + b"\n",
            0o400,
        )

    def load_recovery_manifest(self) -> None:
        """Reconstruct rollback state without relying on the original process."""
        path = self.paths.execution_root / "recovery-manifest.json"
        document = _OBJECT.validate_json(path.read_bytes())
        raw_preflight = document.get("preflight")
        raw_created = document.get("created_files")
        if not isinstance(raw_preflight, dict) or not isinstance(raw_created, list):
            raise HostError("recovery_manifest")
        preflight: dict[Path, str] = {}
        for raw_path, raw_digest in raw_preflight.items():
            if not isinstance(raw_path, str) or not isinstance(raw_digest, str):
                raise HostError("recovery_manifest")
            preflight[Path(raw_path)] = raw_digest
        created = tuple(Path(item) for item in raw_created)
        self._preflight = preflight
        self._created_files = created

    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:
            if (
                hashlib.sha256(self.hermes_wheel.read_bytes()).hexdigest()
                != self.hermes_wheel_sha256
                or hashlib.sha256(self.profile_wheel.read_bytes()).hexdigest()
                != self.profile_wheel_sha256
            ):
                raise HostError("wheel_integrity")
            _ = install_fresh_runtime(
                self.paths.successor_runtime,
                self.hermes_wheel,
                self.profile_wheel,
            )
        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")
        for path in self._created_files:
            info = path.stat(follow_symlinks=False)
            if (
                path.is_symlink()
                or not path.is_file()
                or info.st_uid != expected_uid
                or info.st_gid != expected_gid
                or info.st_mode & 0o777 != 0o600
            ):
                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:
        if not self._created_files:
            self._created_files = missing_canonical_weekly_files(self.paths)
        self._migration_after = capacity_after(
            self.paths.registry,
            self.candidate_digest,
        )
        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.candidate_digest,
            self._created_files,
        )
        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,
            candidate_digest=self.candidate_digest,
            hermes_wheel=self.hermes_wheel,
            profile_wheel=self.profile_wheel,
            source_dependencies=not self.live,
        )
        self._stage("weekly_startup_smoke")

    def switch_systemd(self) -> None:
        self._postimages = switch_postimages(
            self.paths,
            self.candidate_digest,
        )
        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.candidate_digest,
        )
        self._stage("post_fence")

    def remove_created(self) -> None:
        for path in self._created_files:
            if path.is_file() and not path.is_symlink():
                path.unlink()
        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()
