from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
import time
from typing import TYPE_CHECKING, Callable

from .pairing import PairingCoordinator
from .reconnect import ReconnectPolicy

if TYPE_CHECKING:
    from ..desktop.active.pipeline import ActiveCommandPipeline


StatusCallback = Callable[[str], None]


@dataclass
class RuntimeSnapshot:
    status: str
    failure_count: int = 0
    last_error: str = ""
    last_heartbeat_at: str = ""


class PassiveHeartbeatRuntime:
    """Runs signed heartbeat loop and stays resident while awaiting pairing."""

    def __init__(
        self,
        *,
        pairing: PairingCoordinator,
        reconnect: ReconnectPolicy,
        app_version: str,
        device_type: str,
        heartbeat_interval_sec: int,
        nonce_factory: Callable[[], str],
        heartbeat_metadata_factory: Callable[[], dict[str, str]] | None = None,
        heartbeat_response_handler: Callable[[dict[str, object]], None] | None = None,
        sleep_fn: Callable[[float], None] | None = None,
        status_callback: StatusCallback | None = None,
        active_pipeline: ActiveCommandPipeline | None = None,
    ) -> None:
        self._pairing = pairing
        self._reconnect = reconnect
        self._app_version = app_version
        self._device_type = device_type
        self._heartbeat_interval_sec = heartbeat_interval_sec
        self._nonce_factory = nonce_factory
        self._heartbeat_metadata_factory = heartbeat_metadata_factory
        self._heartbeat_response_handler = heartbeat_response_handler
        self._sleep = sleep_fn or time.sleep
        self._status_callback = status_callback
        self._active_pipeline = active_pipeline
        self._snapshot = RuntimeSnapshot(status="Offline")

    def snapshot(self) -> RuntimeSnapshot:
        return self._snapshot

    def set_active_pipeline(self, pipeline: ActiveCommandPipeline | None) -> None:
        self._active_pipeline = pipeline

    def run_once(self) -> int:
        try:
            response = self._pairing.send_heartbeat_once(
                nonce=self._nonce_factory(),
                app_version=self._app_version,
                device_type=self._device_type,
                heartbeat_metadata=self._heartbeat_metadata_factory() if self._heartbeat_metadata_factory else None,
            )
            if self._heartbeat_response_handler is not None and isinstance(response, dict):
                try:
                    self._heartbeat_response_handler(response)
                except Exception:
                    pass
            self._snapshot.status = "Connected"
            self._snapshot.last_error = ""
            self._snapshot.failure_count = 0
            self._snapshot.last_heartbeat_at = datetime.now(tz=timezone.utc).isoformat()
            try:
                self._pairing.maybe_sync_device_avatar()
            except Exception:
                pass
            self._process_one_command_if_available()
            self._emit_status("Connected")
            return self._heartbeat_interval_sec
        except Exception as exc:
            self._snapshot.failure_count += 1
            self._snapshot.status = "Error"
            self._snapshot.last_error = str(exc)
            self._emit_status("Error")
            return self._reconnect.next_delay(self._snapshot.failure_count)

    def run_loop(self, *, iterations: int | None = None) -> RuntimeSnapshot:
        run_count = 0
        while iterations is None or run_count < iterations:
            if self._pairing.storage.load_state() is None:
                self._snapshot.status = "WaitingForPairing"
                self._snapshot.last_error = "Device is not paired"
                self._emit_status("WaitingForPairing")
                delay = self._heartbeat_interval_sec
            else:
                delay = self.run_once()
            run_count += 1
            if iterations is not None and run_count >= iterations:
                break
            self._sleep(delay)

        return self._snapshot

    def _process_one_command_if_available(self) -> None:
        if self._active_pipeline is not None:
            if self._active_pipeline.try_resume_deferred_confirmation():
                return
        response = self._pairing.poll_next_command()
        if self._active_pipeline is not None:
            self._active_pipeline.process_poll_response(response)
            return

        if str(response.get("status", "")).strip().lower() != "ok":
            return

        command = response.get("command") if isinstance(response.get("command"), dict) else {}
        command_id = str(command.get("id") or "").strip()
        command_type = str(command.get("command_type") or "").strip().lower()
        payload = command.get("payload") if isinstance(command.get("payload"), dict) else {}
        if not command_id or not command_type:
            return

        self._pairing.submit_command_result(
            command_id=command_id,
            success=False,
            result={
                "execution_result": "active_pipeline_not_configured",
                "evidence": {},
                "policy_decision": "denied",
            },
            error_message="permission_denied:active_pipeline_not_configured",
        )

    def _emit_status(self, value: str) -> None:
        if self._status_callback:
            self._status_callback(value)
