"""Status collection engine.

Components implement ``StatusSource`` and register with ``StatusEngine``.
The engine periodically polls all sources and caches the result.
"""

import json
import logging
import threading
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional

log = logging.getLogger(__name__)


class StatusSource(ABC):
    """Interface for objects that provide status information."""

    def __init__(self, refresh_time_sec: int = 60) -> None:
        self._refresh_time_sec = refresh_time_sec
        self._elapsed_since_refresh: int = 0

    @abstractmethod
    def get_status(self) -> Dict[str, Any]:
        """Return current status as a JSON-serialisable dict."""
        ...

    @abstractmethod
    def get_name(self) -> str:
        """Return the name used as key in the collected status dict."""
        ...

    @property
    def refresh_time_sec(self) -> int:
        return self._refresh_time_sec


class StatusEngine:
    """Collects status from all registered ``StatusSource`` instances."""

    _instance: Optional["StatusEngine"] = None

    def __init__(self) -> None:
        self._sources: List[StatusSource] = []
        self._lock = threading.RLock()
        self._current: Dict[str, Any] = {}
        self._last: Dict[str, Any] = {}
        self._collection_interval: int = 60
        self._worker: Optional[threading.Thread] = None
        self._shutdown_event = threading.Event()

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

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def register_source(self, source: StatusSource) -> None:
        with self._lock:
            if source not in self._sources:
                self._sources.append(source)
                self._collection_interval = max(
                    5,
                    min(self._collection_interval, source.refresh_time_sec),
                )

    def collect(self) -> Dict[str, Any]:
        """Poll all sources whose refresh interval has elapsed."""
        for source in self._sources:
            source._elapsed_since_refresh += self._collection_interval
            if source._elapsed_since_refresh < source.refresh_time_sec:
                continue
            try:
                self._current[source.get_name()] = source.get_status()
            except Exception as exc:
                log.warning("Error collecting status from %s: %s", source.get_name(), exc)
            source._elapsed_since_refresh = 0

        with self._lock:
            self._last = dict(self._current)
        return self._current

    def get_last_status(self) -> Dict[str, Any]:
        """Return the most recently collected status (thread-safe read)."""
        with self._lock:
            return dict(self._last)

    def start(self, shutdown_handler=None) -> None:
        """Start the background collection thread.

        Parameters
        ----------
        shutdown_handler : ShutdownHandler, optional
            If provided, the thread will stop when shutdown is triggered.
        """
        self._shutdown_event.clear()

        def _run() -> None:
            log.info("Status engine started")
            # Initial delay
            if shutdown_handler:
                shutdown_handler.sleep(10)
            else:
                self._shutdown_event.wait(10)

            while not self._shutdown_event.is_set():
                if shutdown_handler and shutdown_handler.is_shutdown():
                    break
                result = self.collect()
                log.info(json.dumps(result, indent=4, default=str))

                if shutdown_handler:
                    shutdown_handler.sleep(self._collection_interval)
                else:
                    self._shutdown_event.wait(self._collection_interval)
            log.info("Status engine stopped")

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

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