from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, Optional

from django.contrib import messages
from django.conf import settings
from apps.audio.utils import has_audio_capture_device
from apps.clocks.utils import has_clock_device

if False:  # pragma: no cover - typing imports only
    from .models import Node, NodeFeature


@dataclass(frozen=True)
class FeatureCheckResult:
    """Outcome of a feature validation."""

    success: bool
    message: str
    level: int = messages.INFO


FeatureCheck = Callable[["NodeFeature", Optional["Node"]], Any]


class FeatureCheckRegistry:
    """Registry for feature validation callbacks."""

    def __init__(self) -> None:
        self._checks: Dict[str, FeatureCheck] = {}
        self._default_check: Optional[FeatureCheck] = None

    def register(self, slug: str) -> Callable[[FeatureCheck], FeatureCheck]:
        """Register ``func`` as the validator for ``slug``."""

        def decorator(func: FeatureCheck) -> FeatureCheck:
            self._checks[slug] = func
            return func

        return decorator

    def register_default(self, func: FeatureCheck) -> FeatureCheck:
        """Register ``func`` as the fallback validator."""

        self._default_check = func
        return func

    def get(self, slug: str) -> Optional[FeatureCheck]:
        return self._checks.get(slug)

    def items(self) -> Iterable[tuple[str, FeatureCheck]]:
        return self._checks.items()

    def run(
        self, feature: "NodeFeature", *, node: Optional["Node"] = None
    ) -> Optional[FeatureCheckResult]:
        check = self._checks.get(feature.slug)
        if check is None:
            check = self._default_check
            if check is None:
                return None
        result = check(feature, node)
        return self._normalize_result(feature, result)

    def _normalize_result(
        self, feature: "NodeFeature", result: Any
    ) -> FeatureCheckResult:
        if isinstance(result, FeatureCheckResult):
            return result
        if result is None:
            return FeatureCheckResult(
                True,
                f"{feature.display} check completed successfully.",
                messages.SUCCESS,
            )
        if isinstance(result, tuple) and len(result) >= 2:
            success, message, *rest = result
            level = rest[0] if rest else (
                messages.SUCCESS if success else messages.ERROR
            )
            return FeatureCheckResult(bool(success), str(message), int(level))
        if isinstance(result, bool):
            message = (
                f"{feature.display} check {'passed' if result else 'failed'}."
            )
            level = messages.SUCCESS if result else messages.ERROR
            return FeatureCheckResult(result, message, level)
        raise TypeError(
            f"Unsupported feature check result type: {type(result)!r}"
        )


feature_checks = FeatureCheckRegistry()


@feature_checks.register("audio-capture")
def _check_audio_capture(feature: "NodeFeature", node: Optional["Node"]):
    from .models import Node

    target: Optional["Node"] = node or Node.get_local()
    if target is None:
        return FeatureCheckResult(
            False,
            f"No local node is registered; cannot verify {feature.display}.",
            messages.WARNING,
        )
    if not has_audio_capture_device():
        return FeatureCheckResult(
            False,
            f"No audio recording device detected on {target.hostname} for {feature.display}.",
            messages.WARNING,
        )
    if target.has_feature("audio-capture"):
        return FeatureCheckResult(
            True,
            f"{feature.display} is enabled on {target.hostname} and a recording device is available.",
            messages.SUCCESS,
        )
    return FeatureCheckResult(
        True,
        f"{feature.display} is eligible on {target.hostname}; a recording device is available.",
        messages.INFO,
    )


@feature_checks.register("gpio-rtc")
def _check_gpio_rtc(feature: "NodeFeature", node: Optional["Node"]):
    from .models import Node

    target: Optional["Node"] = node or Node.get_local()
    if target is None:
        return FeatureCheckResult(
            False,
            f"No local node is registered; cannot verify {feature.display}.",
            messages.WARNING,
        )
    if not has_clock_device():
        return FeatureCheckResult(
            False,
            f"No I2C clock device detected on {target.hostname} for {feature.display}.",
            messages.WARNING,
        )
    if target.has_feature("gpio-rtc"):
        return FeatureCheckResult(
            True,
            f"{feature.display} is enabled on {target.hostname} and an RTC is available.",
            messages.SUCCESS,
        )
    return FeatureCheckResult(
        True,
        f"{feature.display} is eligible on {target.hostname}; an RTC is available.",
        messages.INFO,
    )


@feature_checks.register_default
def _default_feature_check(
    feature: "NodeFeature", node: Optional["Node"]
) -> FeatureCheckResult:
    from .models import Node

    target: Optional["Node"] = node or Node.get_local()
    if target is None:
        return FeatureCheckResult(
            False,
            f"No local node is registered; cannot verify {feature.display}.",
            messages.WARNING,
        )
    try:
        enabled = feature.is_enabled
    except Exception as exc:  # pragma: no cover - defensive
        return FeatureCheckResult(
            False,
            f"{feature.display} check failed: {exc}",
            messages.ERROR,
        )
    if enabled:
        return FeatureCheckResult(
            True,
            f"{feature.display} is enabled on {target.hostname}.",
            messages.SUCCESS,
        )
    return FeatureCheckResult(
        False,
        f"{feature.display} is not enabled on {target.hostname}.",
        messages.WARNING,
    )


@feature_checks.register("llm-summary")
def _check_llm_summary(feature: "NodeFeature", node: Optional["Node"]):
    from .models import Node
    from apps.summary.node_features import get_llm_summary_prereq_state
    from apps.summary.services import get_summary_config, resolve_model_path

    target: Optional["Node"] = node or Node.get_local()
    if target is None:
        return FeatureCheckResult(
            False,
            f"No local node is registered; cannot verify {feature.display}.",
            messages.WARNING,
        )

    base_dir = Path(settings.BASE_DIR)
    base_path = target.get_base_path()
    prereqs = get_llm_summary_prereq_state(
        base_dir=base_dir, base_path=base_path
    )
    config = get_summary_config()
    model_path = resolve_model_path(config)
    model_path_exists = model_path.exists()
    model_command = (
        config.model_command
        or getattr(settings, "LLM_SUMMARY_COMMAND", "")
        or None
    )

    details = [
        f"LCD lock: {'ok' if prereqs['lcd_enabled'] else 'missing'}",
        f"Celery lock: {'ok' if prereqs['celery_enabled'] else 'missing'}",
        f"Config active: {'yes' if config.is_active else 'no'}",
        f"Model path: {model_path} ({'found' if model_path_exists else 'missing'})",
        "Model command: "
        + (model_command if model_command else "unset (fallback summarizer)"),
    ]

    success = (
        prereqs["lcd_enabled"] and prereqs["celery_enabled"] and config.is_active
    )
    if success and model_path_exists:
        level = messages.SUCCESS
    else:
        level = messages.WARNING
    return FeatureCheckResult(
        success,
        f"{feature.display} prerequisites checked: " + "; ".join(details),
        level,
    )


__all__ = [
    "FeatureCheck",
    "FeatureCheckRegistry",
    "FeatureCheckResult",
    "feature_checks",
]
