from __future__ import annotations

import time
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

from ..ha_client import HAClient
from ..models import CheckResult, HAEventType, Severity
from ..storage import Storage
from .base import Check

if TYPE_CHECKING:
    from ..config import Settings

_BAD_STATES = frozenset({"unavailable", "unknown"})


def _parse_last_changed_ts(value: str | None) -> float | None:
    """Parse HA last_changed ISO string → Unix timestamp.

    Returns None on missing or malformed input so callers can fall back
    to the baseline first_seen without special-casing.
    """
    if not value:
        return None
    try:
        return datetime.fromisoformat(value).timestamp()
    except (ValueError, TypeError):
        return None


class UnavailableEntitiesCheck(Check):
    """Detects entities stuck in unavailable/unknown state.

    Logic:
    1. Fetch all entity states from HA.
    2. Maintain SQLite baseline: INSERT OR IGNORE to preserve first-seen timestamp.
    3. Handle recoveries: clear baseline + alert dedup for entities back online.
    4. Alert on entities unavailable > unavailable_threshold_hours.
    5. Root-cause grouping: if >= integration_failure_threshold_pct of an
       integration's entities are unavailable (and count >= min_entities), emit
       ha_integration_failed instead of N individual ha_entity_unavailable_long
       events.
    6. Alert dedup: skip re-emitting the same alert within alert_cooldown_hours.
    """

    name = "unavailable_entities"

    def __init__(
        self,
        ha_client: HAClient,
        storage: Storage,
        settings: "Settings",
    ) -> None:
        self._client = ha_client
        self._storage = storage
        self._settings = settings

    # ------------------------------------------------------------------
    # Public entry point
    # ------------------------------------------------------------------

    async def run(self) -> list[CheckResult]:
        now = time.time()

        try:
            all_states = await self._client.get_states()
        except Exception as exc:
            return [CheckResult(
                healthy=False,
                event_type=HAEventType.ha_websocket_dead,
                severity=Severity.error,
                message=f"Failed to fetch entity states: {exc}",
                payload={"error": str(exc)},
            )]

        integration_map, area_map = await self._load_registry()

        unavailable: dict[str, dict[str, Any]] = {
            s["entity_id"]: s for s in all_states if s["state"] in _BAD_STATES
        }
        available_ids: set[str] = {
            s["entity_id"] for s in all_states if s["state"] not in _BAD_STATES
        }

        # Handle recoveries first
        tracked = await self._storage.get_all_tracked_entity_ids()
        for eid in tracked:
            if eid in available_ids:
                await self._handle_recovery(eid)

        # Record new/continuing unavailable entities (INSERT OR IGNORE preserves timestamp)
        for eid, state_data in unavailable.items():
            await self._storage.set_entity_unavailable_since(
                eid, state_data["state"], now
            )

        # Determine which entities have exceeded the alert threshold
        to_alert: list[dict[str, Any]] = []
        cooldown_s = self._settings.alert_cooldown_hours * 3600
        threshold_h = self._settings.unavailable_threshold_hours

        for eid, state_data in unavailable.items():
            first_at = await self._storage.get_entity_first_unavailable_at(eid)
            if first_at is None:
                continue
            # Phase 3 Flag #1: if HA reports an earlier last_changed (entity was
            # already unavailable before the agent started/reconnected), use that
            # as the authoritative "since" so duration is accurate.
            last_changed_ts = _parse_last_changed_ts(state_data.get("last_changed"))
            effective_since = (
                min(last_changed_ts, first_at)
                if last_changed_ts is not None
                else first_at
            )
            duration_h = (now - effective_since) / 3600
            if duration_h < threshold_h:
                continue
            alert_key = f"entity_unavailable:{eid}"
            if await self._storage.was_alert_sent(alert_key, cooldown_s):
                continue
            to_alert.append({
                "entity_id": eid,
                "state": state_data["state"],
                "first_at": effective_since,
                "duration_h": duration_h,
                "domain": eid.split(".")[0],
                "integration": integration_map.get(eid),
                "area_id": area_map.get(eid),
            })

        if not to_alert:
            return []

        return await self._build_results(to_alert, all_states, integration_map)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    async def _load_registry(
        self,
    ) -> tuple[dict[str, str], dict[str, str]]:
        """Fetch entity registry; return (integration_map, area_map).

        Falls back to empty dicts when the endpoint is unavailable.
        """
        try:
            registry = await self._client.get_entity_registry()
            integration_map = {
                e["entity_id"]: e.get("platform") or ""
                for e in registry
                if "entity_id" in e
            }
            area_map = {
                e["entity_id"]: e.get("area_id") or ""
                for e in registry
                if "entity_id" in e
            }
            return integration_map, area_map
        except Exception:
            return {}, {}

    async def _handle_recovery(self, entity_id: str) -> None:
        await self._storage.clear_entity_unavailable(entity_id)
        # Clear dedup so the next unavailability triggers an alert immediately
        await self._storage.clear_alert(f"entity_unavailable:{entity_id}")

    async def _build_results(
        self,
        to_alert: list[dict[str, Any]],
        all_states: list[dict[str, Any]],
        integration_map: dict[str, str],
    ) -> list[CheckResult]:
        results: list[CheckResult] = []
        handled: set[str] = set()

        # Build per-integration stats across ALL entities (not just to_alert)
        total_per_integ: dict[str, int] = {}
        unav_per_integ: dict[str, list[str]] = {}

        for state in all_states:
            eid = state["entity_id"]
            integ = integration_map.get(eid)
            if not integ:
                continue
            total_per_integ[integ] = total_per_integ.get(integ, 0) + 1
            if state["state"] in _BAD_STATES:
                unav_per_integ.setdefault(integ, []).append(eid)

        min_ent = self._settings.integration_failure_min_entities
        threshold_pct = self._settings.integration_failure_threshold_pct
        cooldown_s = self._settings.alert_cooldown_hours * 3600

        # Integration-level events
        for integ, unav_ids in unav_per_integ.items():
            total = total_per_integ.get(integ, 0)
            pct = len(unav_ids) / total if total else 0

            alerted_from_integ = [e for e in to_alert if e["integration"] == integ]
            if not alerted_from_integ:
                continue
            if pct < threshold_pct or len(unav_ids) < min_ent:
                continue

            alert_key = f"integration_failed:{integ}"
            if await self._storage.was_alert_sent(alert_key, cooldown_s):
                handled.update(e["entity_id"] for e in alerted_from_integ)
                continue

            results.append(CheckResult(
                healthy=False,
                event_type=HAEventType.ha_integration_failed,
                severity=Severity.error,
                message=(
                    f"Integration '{integ}' appears down: "
                    f"{len(unav_ids)}/{total} entities unavailable"
                ),
                payload={
                    "integration": integ,
                    "affected_entities": unav_ids,
                    "unavailable_count": len(unav_ids),
                    "total_count": total,
                    "unavailable_pct": round(pct, 2),
                },
            ))
            await self._storage.mark_alert_sent(alert_key)
            handled.update(e["entity_id"] for e in alerted_from_integ)

        # Per-entity events for entities not covered by an integration event
        for entity in to_alert:
            eid = entity["entity_id"]
            if eid in handled:
                continue

            since_iso = (
                datetime.fromtimestamp(entity["first_at"], tz=timezone.utc)
                .isoformat()
                .replace("+00:00", "Z")
            )

            payload: dict[str, Any] = {
                "entity_id": eid,
                "state": entity["state"],
                "since": since_iso,
                "duration_hours": round(entity["duration_h"], 1),
                "domain": entity["domain"],
            }
            if entity["integration"]:
                payload["integration"] = entity["integration"]
            if entity["area_id"]:
                payload["area"] = entity["area_id"]

            results.append(CheckResult(
                healthy=False,
                event_type=HAEventType.ha_entity_unavailable_long,
                severity=Severity.warning,
                message=(
                    f"Entity {eid} unavailable for "
                    f"{entity['duration_h']:.1f}h"
                ),
                payload=payload,
            ))
            await self._storage.mark_alert_sent(f"entity_unavailable:{eid}")

        return results
