"""Heartbeat / watchdog engine for detecting hung threads.

Components register with an ID and timeout.  If a heartbeat is not
refreshed before the timeout expires, the engine logs a critical warning.
"""

import logging
import time
import threading
from typing import Any, Dict, List, Optional

from brinkhaustools.common.status import StatusSource

log = logging.getLogger(__name__)


class _HeartbeatEntry:
    __slots__ = ("id", "timeout_at")

    def __init__(self, id: str, timeout_at: float) -> None:
        self.id = id
        self.timeout_at = timeout_at


class HeartbeatEngine(StatusSource):
    """Watchdog that tracks component heartbeats."""

    _instance: Optional["HeartbeatEngine"] = None

    def __init__(self, default_timeout_sec: int = 300) -> None:
        super().__init__(refresh_time_sec=60)
        self._entries: List[_HeartbeatEntry] = []
        self._lock = threading.RLock()
        self._default_timeout = default_timeout_sec
        self._alarm = threading.Event()
        self._worker: Optional[threading.Thread] = None
        self._shutdown_event = threading.Event()

    # ------------------------------------------------------------------
    # Singleton
    # ------------------------------------------------------------------
    @staticmethod
    def get_instance() -> "HeartbeatEngine":
        if HeartbeatEngine._instance is None:
            HeartbeatEngine._instance = HeartbeatEngine()
        return HeartbeatEngine._instance

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def register(self, id: str, timeout_sec: float = 0) -> None:
        """Register or refresh a heartbeat.

        If *timeout_sec* is 0, the default timeout is used.
        """
        deadline = time.monotonic() + (timeout_sec if timeout_sec > 0 else self._default_timeout)
        with self._lock:
            for entry in self._entries:
                if entry.id == id:
                    entry.timeout_at = deadline
                    return
            self._entries.append(_HeartbeatEntry(id, deadline))

    def cancel(self, id: str) -> None:
        """Stop watching a heartbeat."""
        with self._lock:
            self._entries = [e for e in self._entries if e.id != id]

    def heartbeats_ok(self) -> bool:
        """Return *True* if all heartbeats are within timeout."""
        if self._alarm.is_set():
            return False
        now = time.monotonic()
        with self._lock:
            for entry in self._entries:
                if entry.timeout_at < now:
                    self._alarm.set()
                    return False
        return True

    def get_status(self) -> Dict[str, Any]:
        now = time.monotonic()
        result: Dict[str, Any] = {}
        with self._lock:
            for i, entry in enumerate(self._entries, 1):
                remaining = round(entry.timeout_at - now, 1)
                result[f"Entry {i}"] = f"'#{entry.id}' has timeout of {remaining} sec"
        return result

    def get_name(self) -> str:
        return "Heartbeat manager"

    # ------------------------------------------------------------------
    # Background worker
    # ------------------------------------------------------------------
    def start(self, shutdown_handler=None) -> None:
        """Start the background watchdog thread."""
        self._shutdown_event.clear()

        def _run() -> None:
            log.info("Heartbeat engine started")
            while not self._shutdown_event.is_set():
                if shutdown_handler and shutdown_handler.is_shutdown():
                    break
                if not self.heartbeats_ok():
                    log.critical("Heartbeat timeout detected")
                    with self._lock:
                        now = time.monotonic()
                        for entry in self._entries:
                            if entry.timeout_at < now:
                                log.critical(
                                    "Heartbeat '%s' over timeout by %.1f sec",
                                    entry.id,
                                    now - entry.timeout_at,
                                )
                if shutdown_handler:
                    shutdown_handler.sleep(1)
                else:
                    self._shutdown_event.wait(1)
            log.info("Heartbeat engine stopped")

        self._worker = threading.Thread(target=_run, name="HeartbeatEngine", daemon=True)
        self._worker.start()

    def stop(self) -> None:
        self._shutdown_event.set()
        if self._worker:
            self._worker.join(timeout=2)
