"""Home Assistant WebSocket runtime — maintains a persistent connection and caches entity states.

Follows the MQTT service pattern: async background task with auto-reconnect.
Entity state cache is thread-safe for synchronous reads (automation engine).
"""

import asyncio
import json
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Set

from wled_controller.storage.home_assistant_source import HomeAssistantSource
from wled_controller.utils import get_logger

logger = get_logger(__name__)


@dataclass(frozen=True)
class HAEntityState:
    """Immutable snapshot of a Home Assistant entity state."""

    entity_id: str
    state: str  # e.g. "23.5", "on", "off", "unavailable"
    attributes: Dict[str, Any] = field(default_factory=dict)
    last_changed: str = ""  # ISO timestamp from HA
    fetched_at: float = 0.0  # time.monotonic()


class HARuntime:
    """Persistent WebSocket connection to a Home Assistant instance.

    - Authenticates via Long-Lived Access Token
    - Subscribes to state_changed events
    - Caches entity states for synchronous reads
    - Auto-reconnects on connection loss
    """

    _RECONNECT_DELAY = 5.0  # seconds between reconnect attempts

    def __init__(self, source: HomeAssistantSource) -> None:
        self._source_id = source.id
        self._ws_url = source.ws_url
        self._token = source.token
        self._entity_filters: List[str] = list(source.entity_filters)

        # Entity state cache (thread-safe)
        self._states: Dict[str, HAEntityState] = {}
        self._lock = threading.Lock()

        # Callbacks: entity_id -> set of callbacks
        self._callbacks: Dict[str, Set[Callable]] = {}

        # Async task management
        self._task: Optional[asyncio.Task] = None
        self._ws: Any = None  # live websocket connection (set during _connection_loop)
        self._connected = False
        self._msg_id = 0

    @property
    def is_connected(self) -> bool:
        return self._connected

    @property
    def source_id(self) -> str:
        return self._source_id

    def get_state(self, entity_id: str) -> Optional[HAEntityState]:
        """Get cached entity state (synchronous, thread-safe)."""
        with self._lock:
            return self._states.get(entity_id)

    def get_all_states(self) -> Dict[str, HAEntityState]:
        """Get all cached entity states (synchronous, thread-safe)."""
        with self._lock:
            return dict(self._states)

    def subscribe(self, entity_id: str, callback: Callable) -> None:
        """Register a callback for entity state changes."""
        if entity_id not in self._callbacks:
            self._callbacks[entity_id] = set()
        self._callbacks[entity_id].add(callback)

    def unsubscribe(self, entity_id: str, callback: Callable) -> None:
        """Remove a callback for entity state changes."""
        if entity_id in self._callbacks:
            self._callbacks[entity_id].discard(callback)
            if not self._callbacks[entity_id]:
                del self._callbacks[entity_id]

    async def call_service(
        self, domain: str, service: str, service_data: dict, target: dict
    ) -> bool:
        """Call a HA service (e.g. light.turn_on). Fire-and-forget.

        Returns True if the message was sent, False if not connected.
        """
        if not self._connected or self._ws is None:
            return False
        try:
            msg_id = self._next_id()
            await self._ws.send(
                json.dumps(
                    {
                        "id": msg_id,
                        "type": "call_service",
                        "domain": domain,
                        "service": service,
                        "service_data": service_data,
                        "target": target,
                    }
                )
            )
            return True
        except Exception as e:
            logger.debug(f"HA call_service failed ({domain}.{service}): {e}")
            return False

    async def start(self) -> None:
        """Start the WebSocket connection loop."""
        if self._task is not None:
            return
        self._task = asyncio.create_task(self._connection_loop())
        logger.info(f"HA runtime started: {self._source_id} -> {self._ws_url}")

    async def stop(self) -> None:
        """Stop the WebSocket connection."""
        if self._task is None:
            return
        self._task.cancel()
        try:
            await self._task
        except asyncio.CancelledError:
            pass
        self._task = None
        self._connected = False
        logger.info(f"HA runtime stopped: {self._source_id}")

    def update_config(self, source: HomeAssistantSource) -> None:
        """Hot-update config (token, filters). Connection will use new values on next reconnect."""
        self._ws_url = source.ws_url
        self._token = source.token
        self._entity_filters = list(source.entity_filters)

    def _next_id(self) -> int:
        self._msg_id += 1
        return self._msg_id

    def _matches_filter(self, entity_id: str) -> bool:
        """Check if an entity matches the configured filters (empty = allow all)."""
        if not self._entity_filters:
            return True
        import fnmatch

        return any(fnmatch.fnmatch(entity_id, pattern) for pattern in self._entity_filters)

    async def _connection_loop(self) -> None:
        """Persistent connection with auto-reconnect."""
        try:
            import websockets
        except ImportError:
            logger.error(
                "websockets package not installed — Home Assistant integration unavailable"
            )
            return

        while True:
            try:
                async with websockets.connect(self._ws_url) as ws:
                    # Step 1: Wait for auth_required
                    msg = json.loads(await ws.recv())
                    if msg.get("type") != "auth_required":
                        logger.warning(f"HA unexpected initial message: {msg.get('type')}")
                        continue

                    # Step 2: Send auth
                    await ws.send(
                        json.dumps(
                            {
                                "type": "auth",
                                "access_token": self._token,
                            }
                        )
                    )

                    msg = json.loads(await ws.recv())
                    if msg.get("type") != "auth_ok":
                        logger.error(f"HA auth failed: {msg.get('message', 'unknown error')}")
                        await asyncio.sleep(self._RECONNECT_DELAY)
                        continue

                    self._ws = ws
                    self._connected = True
                    logger.info(
                        f"HA connected: {self._source_id} (version {msg.get('ha_version', '?')})"
                    )

                    # Step 3: Fetch all current states
                    fetch_id = self._next_id()
                    await ws.send(
                        json.dumps(
                            {
                                "id": fetch_id,
                                "type": "get_states",
                            }
                        )
                    )

                    # Step 4: Subscribe to state_changed events
                    sub_id = self._next_id()
                    await ws.send(
                        json.dumps(
                            {
                                "id": sub_id,
                                "type": "subscribe_events",
                                "event_type": "state_changed",
                            }
                        )
                    )

                    # Step 5: Message loop
                    async for raw in ws:
                        msg = json.loads(raw)
                        msg_type = msg.get("type")

                        if msg_type == "result" and msg.get("id") == fetch_id:
                            # Initial state dump
                            self._handle_state_dump(msg.get("result", []))

                        elif msg_type == "event":
                            event = msg.get("event", {})
                            if event.get("event_type") == "state_changed":
                                self._handle_state_changed(event.get("data", {}))

            except asyncio.CancelledError:
                self._ws = None
                break
            except Exception as e:
                self._ws = None
                self._connected = False
                logger.warning(
                    f"HA connection lost ({self._source_id}): {e}. Reconnecting in {self._RECONNECT_DELAY}s..."
                )
                await asyncio.sleep(self._RECONNECT_DELAY)

    def _handle_state_dump(self, states: list) -> None:
        """Process initial state dump from get_states."""
        now = time.monotonic()
        with self._lock:
            for s in states:
                eid = s.get("entity_id", "")
                if not self._matches_filter(eid):
                    continue
                self._states[eid] = HAEntityState(
                    entity_id=eid,
                    state=str(s.get("state", "")),
                    attributes=s.get("attributes", {}),
                    last_changed=s.get("last_changed", ""),
                    fetched_at=now,
                )
        logger.info(f"HA {self._source_id}: loaded {len(self._states)} entity states")

    def _handle_state_changed(self, data: dict) -> None:
        """Process a state_changed event."""
        new_state = data.get("new_state")
        if new_state is None:
            return

        eid = new_state.get("entity_id", "")
        if not self._matches_filter(eid):
            return

        now = time.monotonic()
        entity_state = HAEntityState(
            entity_id=eid,
            state=str(new_state.get("state", "")),
            attributes=new_state.get("attributes", {}),
            last_changed=new_state.get("last_changed", ""),
            fetched_at=now,
        )

        with self._lock:
            self._states[eid] = entity_state

        # Dispatch callbacks
        callbacks = self._callbacks.get(eid, set())
        for cb in callbacks:
            try:
                cb(entity_state)
            except Exception as e:
                logger.error(f"HA callback error ({eid}): {e}")

    async def fetch_entities(self) -> List[Dict[str, Any]]:
        """Fetch entity list via one-shot WebSocket call (for API /entities endpoint)."""
        try:
            import websockets
        except ImportError:
            return []

        try:
            async with websockets.connect(self._ws_url) as ws:
                # Auth
                msg = json.loads(await ws.recv())
                if msg.get("type") != "auth_required":
                    return []
                await ws.send(
                    json.dumps(
                        {
                            "type": "auth",
                            "access_token": self._token,
                        }
                    )
                )
                msg = json.loads(await ws.recv())
                if msg.get("type") != "auth_ok":
                    return []

                # Get states
                req_id = 1
                await ws.send(json.dumps({"id": req_id, "type": "get_states"}))
                msg = json.loads(await ws.recv())
                if msg.get("type") == "result" and msg.get("success"):
                    entities = []
                    for s in msg.get("result", []):
                        eid = s.get("entity_id", "")
                        if self._matches_filter(eid):
                            entities.append(
                                {
                                    "entity_id": eid,
                                    "state": s.get("state", ""),
                                    "friendly_name": s.get("attributes", {}).get(
                                        "friendly_name", eid
                                    ),
                                    "domain": eid.split(".")[0] if "." in eid else "",
                                }
                            )
                    return entities
        except Exception as e:
            logger.warning(f"HA fetch_entities failed ({self._source_id}): {e}")
        return []
