from __future__ import annotations

import threading
import time
from dataclasses import dataclass
from typing import Protocol

from k1link.device_plugins.xgrids_k1.protocol.application_bootstrap import (
    ApplicationControlAuthority,
    ShadowApplicationBootstrapOrchestrator,
)
from k1link.device_plugins.xgrids_k1.protocol.application_publish import (
    WriteDisabledOneShotPublisher,
)

MIN_AUTHORITY_LEASE_SECONDS = 15.0
MAX_AUTHORITY_LEASE_SECONDS = 300.0


class ApplicationControlExecutionError(RuntimeError):
    """The dormant application-control execution gate failed closed."""


class ApplicationAuthorityLoader(Protocol):
    def load(self) -> ApplicationControlAuthority: ...


class MonotonicClock(Protocol):
    def __call__(self) -> float: ...


@dataclass(frozen=True, slots=True)
class ApplicationAuthorityLeaseSnapshot:
    state: str
    remaining_seconds: float
    authority_cached: bool
    exportable: bool = False

    def as_dict(self) -> dict[str, object]:
        return {
            "state": self.state,
            "remaining_seconds": self.remaining_seconds,
            "authority_cached": self.authority_cached,
            "exportable": self.exportable,
        }


class ApplicationAuthorityLease:
    """Keep authority in process memory only for one short control attempt."""

    def __init__(
        self,
        loader: ApplicationAuthorityLoader,
        *,
        ttl_seconds: float,
        monotonic: MonotonicClock = time.monotonic,
    ) -> None:
        if not isinstance(ttl_seconds, (int, float)) or isinstance(ttl_seconds, bool):
            raise ApplicationControlExecutionError("authority lease TTL must be numeric")
        if not MIN_AUTHORITY_LEASE_SECONDS <= ttl_seconds <= MAX_AUTHORITY_LEASE_SECONDS:
            raise ApplicationControlExecutionError(
                "authority lease TTL is outside the reviewed 15-300 second range"
            )
        self._lock = threading.Lock()
        self._monotonic = monotonic
        self._authority: ApplicationControlAuthority | None = loader.load()
        self._expires_at = monotonic() + float(ttl_seconds)
        self._state = "armed"

    def authority(self) -> ApplicationControlAuthority:
        with self._lock:
            self._expire_locked()
            if self._state != "armed" or self._authority is None:
                raise ApplicationControlExecutionError("application authority lease is not armed")
            return self._authority

    def snapshot(self) -> ApplicationAuthorityLeaseSnapshot:
        with self._lock:
            self._expire_locked()
            remaining = max(0.0, self._expires_at - self._monotonic())
            return ApplicationAuthorityLeaseSnapshot(
                state=self._state,
                remaining_seconds=round(remaining, 3),
                authority_cached=self._authority is not None,
            )

    def close(self) -> None:
        with self._lock:
            self._authority = None
            if self._state != "expired":
                self._state = "closed"

    def _expire_locked(self) -> None:
        if self._state == "armed" and self._monotonic() >= self._expires_at:
            self._authority = None
            self._state = "expired"


@dataclass(frozen=True, slots=True)
class DormantApplicationControlSnapshot:
    state: str
    lease: ApplicationAuthorityLeaseSnapshot | None
    orchestrator: dict[str, object] | None
    publisher: dict[str, object]
    live_transport_installed: bool = False
    can_emit_requests: bool = False

    def as_dict(self) -> dict[str, object]:
        return {
            "mode": "dormant-write-disabled",
            "state": self.state,
            "lease": self.lease.as_dict() if self.lease is not None else None,
            "orchestrator": self.orchestrator,
            "publisher": self.publisher,
            "live_transport_installed": self.live_transport_installed,
            "can_emit_requests": self.can_emit_requests,
        }


class DormantApplicationControlCoordinator:
    """Facade-owned authority/orchestrator lifecycle with no emission method."""

    def __init__(
        self,
        loader: ApplicationAuthorityLoader,
        publisher: WriteDisabledOneShotPublisher,
        *,
        monotonic: MonotonicClock = time.monotonic,
    ) -> None:
        self._lock = threading.Lock()
        self._loader = loader
        self._publisher = publisher
        self._monotonic = monotonic
        self._lease: ApplicationAuthorityLease | None = None
        self._orchestrator: ShadowApplicationBootstrapOrchestrator | None = None
        self._state = "disarmed"

    def arm(
        self,
        *,
        ttl_seconds: float,
        epoch_seconds: int,
        timezone_name: str,
    ) -> DormantApplicationControlSnapshot:
        with self._lock:
            self._expire_locked()
            if self._lease is not None or self._orchestrator is not None:
                raise ApplicationControlExecutionError(
                    "application control coordinator is already armed"
                )
            lease = ApplicationAuthorityLease(
                self._loader,
                ttl_seconds=ttl_seconds,
                monotonic=self._monotonic,
            )
            try:
                orchestrator = ShadowApplicationBootstrapOrchestrator(
                    lease.authority(),
                    epoch_seconds=epoch_seconds,
                    timezone_name=timezone_name,
                )
            except Exception:
                lease.close()
                raise
            self._lease = lease
            self._orchestrator = orchestrator
            self._state = "armed-shadow-only"
            return self._snapshot_locked()

    def disarm(self) -> DormantApplicationControlSnapshot:
        with self._lock:
            self._close_locked("disarmed")
            return self._snapshot_locked()

    def snapshot(self) -> DormantApplicationControlSnapshot:
        with self._lock:
            self._expire_locked()
            return self._snapshot_locked()

    def close(self) -> None:
        with self._lock:
            self._close_locked("closed")

    def _expire_locked(self) -> None:
        if self._lease is None:
            return
        lease_snapshot = self._lease.snapshot()
        if lease_snapshot.state == "expired":
            self._close_locked("expired")

    def _close_locked(self, state: str) -> None:
        if self._lease is not None:
            self._lease.close()
        self._lease = None
        self._orchestrator = None
        self._state = state

    def _snapshot_locked(self) -> DormantApplicationControlSnapshot:
        lease_snapshot = self._lease.snapshot() if self._lease is not None else None
        if lease_snapshot is not None and lease_snapshot.state == "expired":
            self._close_locked("expired")
            lease_snapshot = None
        orchestrator_snapshot = (
            self._orchestrator.snapshot().as_dict() if self._orchestrator is not None else None
        )
        return DormantApplicationControlSnapshot(
            state=self._state,
            lease=lease_snapshot,
            orchestrator=orchestrator_snapshot,
            publisher=self._publisher.snapshot().as_dict(),
        )
