"""Self-diagnosis engine for tracking application health.

Components report errors/warnings with numeric codes and severity.
Messages can have timeouts and are automatically cleared when resolved.
"""

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

from brinkhaustools.common.status import StatusSource

log = logging.getLogger(__name__)


class DiagnosisMessage:
    """A single diagnosis entry."""

    __slots__ = (
        "code", "message", "critical", "information",
        "created_at_ms", "timeout_at",
    )

    def __init__(
        self,
        code: int,
        message: str,
        critical: bool = False,
        information: bool = False,
        timeout_sec: float = 0,
    ) -> None:
        self.code = code
        self.message = message
        self.critical = critical
        self.information = information
        self.created_at_ms = int(time.time() * 1000)
        self.timeout_at: float = (
            time.monotonic() + timeout_sec if timeout_sec > 0 else 0
        )


class SelfDiagnosisEngine(StatusSource):
    """Application health tracker.

    Thread-safe.  No background thread needed — timeouts are checked lazily
    on ``get_status()`` / ``get_messages()`` calls.
    """

    _instance: Optional["SelfDiagnosisEngine"] = None

    def __init__(self) -> None:
        super().__init__(refresh_time_sec=60)
        self._messages: List[DiagnosisMessage] = []
        self._lock = threading.RLock()

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

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def notify(
        self,
        code: int,
        message: str,
        critical: bool = False,
        information: bool = False,
        timeout_sec: float = 0,
    ) -> None:
        """Report a diagnostic message.

        If a message with the same *code* already exists, it is **not**
        duplicated.
        """
        with self._lock:
            for msg in self._messages:
                if msg.code == code:
                    return
            entry = DiagnosisMessage(code, message, critical, information, timeout_sec)
            self._messages.append(entry)
            if critical:
                log.critical("Diagnosis %d: %s", code, message)
            else:
                log.info("Diagnosis %d: %s", code, message)

    def clear(self, code: int) -> None:
        """Clear (resolve) a diagnostic message by its code."""
        with self._lock:
            for msg in self._messages:
                if msg.code == code:
                    self._messages.remove(msg)
                    log.info("Clearing diagnosis %d", code)
                    return

    def get_messages(self) -> List[DiagnosisMessage]:
        """Return a snapshot of active messages (expired ones are pruned)."""
        self._prune_expired()
        with self._lock:
            return list(self._messages)

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

        Compatible with the original ``pythonuiapphelper`` format.
        """
        self._prune_expired()
        with self._lock:
            errors: List[dict] = []
            critical = False
            for msg in self._messages:
                errors.append({
                    "code": msg.code,
                    "critical": msg.critical,
                    "information": msg.information,
                    "messageForUI": msg.message,
                    "timestamp": msg.created_at_ms,
                })
                if msg.critical:
                    critical = True

            return {
                "Source name": "RunTimeTests",
                "System Status": not critical,
                "messages": errors,
            }

    def get_name(self) -> str:
        return "RunTimeTests"

    # ------------------------------------------------------------------
    # Internals
    # ------------------------------------------------------------------
    def _prune_expired(self) -> None:
        now = time.monotonic()
        with self._lock:
            self._messages = [
                m for m in self._messages
                if m.timeout_at == 0 or m.timeout_at > now
            ]

    def reset_for_testing(self) -> None:
        """Clear all messages.  **Only use in tests.**"""
        with self._lock:
            self._messages.clear()
