"""Home Assistant light target processor — casts LED colors to HA lights.

Reads from a ColorStripStream, averages LED segments to single RGB values,
and calls light.turn_on / light.turn_off via the HA WebSocket connection.
Rate-limited to update_rate Hz (typically 1-5 Hz).
"""

import asyncio
import json
import time
from typing import Any, Dict, List, Optional, Tuple

import numpy as np

from wled_controller.core.processing.target_processor import TargetContext, TargetProcessor
from wled_controller.storage.ha_light_output_target import HALightMapping
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class HALightTargetProcessor(TargetProcessor):
    """Streams averaged LED colors to Home Assistant light entities."""

    def __init__(
        self,
        target_id: str,
        ha_source_id: str,
        color_strip_source_id: str = "",
        brightness=None,
        # legacy compat
        brightness_value_source_id: str = "",
        light_mappings: Optional[List[HALightMapping]] = None,
        update_rate: float = 2.0,
        transition=None,
        min_brightness_threshold: int = 0,
        color_tolerance: int = 5,
        ctx: Optional[TargetContext] = None,
    ):
        from wled_controller.storage.bindable import BindableFloat, bfloat

        super().__init__(target_id, ctx)
        self._ha_source_id = ha_source_id
        self._css_id = color_strip_source_id
        # Accept BindableFloat or legacy string
        if brightness is not None and isinstance(brightness, BindableFloat):
            self._brightness = brightness
        else:
            self._brightness = BindableFloat(1.0, source_id=brightness_value_source_id or "")
        # Transition as BindableFloat
        if transition is not None and isinstance(transition, BindableFloat):
            self._transition = transition
        else:
            self._transition = BindableFloat(float(transition) if transition is not None else 0.5)
        self._light_mappings = light_mappings or []
        self._update_rate = max(0.5, min(5.0, bfloat(update_rate, 2.0)))
        self._min_brightness_threshold = int(bfloat(min_brightness_threshold, 0.0))
        self._color_tolerance = int(bfloat(color_tolerance, 5.0))

        # Runtime state
        self._css_stream = None
        self._ha_runtime = None
        self._value_stream = None  # brightness value source stream
        self._previous_colors: Dict[str, Tuple[int, int, int]] = {}
        self._previous_on: Dict[str, bool] = {}  # track on/off state per entity
        self._latest_entity_colors: Dict[str, Tuple[int, int, int]] = {}
        self._ws_clients: List[Any] = []
        self._start_time: Optional[float] = None

    @property
    def device_id(self) -> Optional[str]:
        return None  # HA light targets don't use device providers

    async def start(self) -> None:
        if self._is_running:
            return

        # Acquire CSS stream
        if self._css_id and self._ctx.color_strip_stream_manager:
            try:
                self._css_stream = self._ctx.color_strip_stream_manager.acquire(
                    self._css_id, self._target_id
                )
            except Exception as e:
                logger.warning(f"HA light {self._target_id}: failed to acquire CSS stream: {e}")

        # Acquire HA runtime
        try:
            from wled_controller.core.home_assistant.ha_manager import HomeAssistantManager

            ha_manager: Optional[HomeAssistantManager] = getattr(self._ctx, "ha_manager", None)
            if ha_manager:
                self._ha_runtime = await ha_manager.acquire(self._ha_source_id)
        except Exception as e:
            logger.warning(f"HA light {self._target_id}: failed to acquire HA runtime: {e}")

        # Acquire brightness value stream (if configured)
        if self._brightness.source_id and self._ctx.value_stream_manager:
            try:
                self._value_stream = self._ctx.value_stream_manager.acquire(
                    self._brightness.source_id
                )
            except Exception as e:
                logger.warning(f"HA light {self._target_id}: failed to acquire brightness VS: {e}")
                self._value_stream = None

        self._is_running = True
        self._start_time = time.monotonic()
        self._task = asyncio.create_task(self._processing_loop())
        logger.info(f"HA light target started: {self._target_id}")

    async def stop(self) -> None:
        self._is_running = False
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None

        # Release CSS stream
        if self._css_stream and self._ctx.color_strip_stream_manager:
            try:
                self._ctx.color_strip_stream_manager.release(self._css_id, self._target_id)
            except Exception:
                pass
            self._css_stream = None

        # Release brightness value stream
        if self._value_stream is not None and self._ctx.value_stream_manager:
            try:
                self._ctx.value_stream_manager.release(self._brightness.source_id)
            except Exception:
                pass
            self._value_stream = None

        # Release HA runtime
        if self._ha_runtime:
            try:
                ha_manager = getattr(self._ctx, "ha_manager", None)
                if ha_manager:
                    await ha_manager.release(self._ha_source_id)
            except Exception:
                pass
            self._ha_runtime = None

        self._previous_colors.clear()
        self._previous_on.clear()
        self._latest_entity_colors.clear()
        self._ws_clients.clear()
        logger.info(f"HA light target stopped: {self._target_id}")

    def update_settings(self, settings) -> None:
        from wled_controller.storage.bindable import BindableFloat, bfloat

        if isinstance(settings, dict):
            if "update_rate" in settings:
                self._update_rate = max(0.5, min(5.0, bfloat(settings["update_rate"], 2.0)))
            if "transition" in settings:
                t = settings["transition"]
                if isinstance(t, BindableFloat):
                    self._transition = t
                else:
                    self._transition = self._transition.apply_update(t)
            if "brightness" in settings:
                b = settings["brightness"]
                if isinstance(b, BindableFloat):
                    self._brightness = b
                else:
                    self._brightness = self._brightness.apply_update(b)
            if "min_brightness_threshold" in settings:
                self._min_brightness_threshold = int(
                    bfloat(settings["min_brightness_threshold"], 0.0)
                )
            if "color_tolerance" in settings:
                self._color_tolerance = int(bfloat(settings["color_tolerance"], 5.0))
            if "light_mappings" in settings:
                self._light_mappings = settings["light_mappings"]

    def update_css_source(self, color_strip_source_id: str) -> None:
        """Hot-swap the CSS stream."""
        old_id = self._css_id
        self._css_id = color_strip_source_id

        if self._is_running and self._ctx.color_strip_stream_manager:
            try:
                new_stream = self._ctx.color_strip_stream_manager.acquire(
                    color_strip_source_id, self._target_id
                )
                old_stream = self._css_stream
                self._css_stream = new_stream
                if old_stream:
                    self._ctx.color_strip_stream_manager.release(old_id, self._target_id)
            except Exception as e:
                logger.warning(f"HA light {self._target_id}: CSS swap failed: {e}")

    # ── WebSocket clients ──

    def add_ws_client(self, ws: Any) -> None:
        self._ws_clients.append(ws)

    def remove_ws_client(self, ws: Any) -> None:
        if ws in self._ws_clients:
            self._ws_clients.remove(ws)

    def supports_websocket(self) -> bool:
        return True

    def get_state(self) -> dict:
        uptime = time.monotonic() - self._start_time if self._start_time and self._is_running else 0
        entity_colors = {
            eid: {"r": r, "g": g, "b": b, "hex": f"#{r:02x}{g:02x}{b:02x}"}
            for eid, (r, g, b) in self._latest_entity_colors.items()
        }
        return {
            "target_id": self._target_id,
            "processing": self._is_running,
            "ha_source_id": self._ha_source_id,
            "css_id": self._css_id,
            "is_running": self._is_running,
            "ha_connected": self._ha_runtime.is_connected if self._ha_runtime else False,
            "light_count": len(self._light_mappings),
            "update_rate": self._update_rate,
            "fps_actual": self._update_rate if self._is_running else None,
            "fps_target": self._update_rate,
            "uptime_seconds": uptime,
            "entity_colors": entity_colors,
        }

    def get_metrics(self) -> dict:
        uptime = time.monotonic() - self._start_time if self._start_time and self._is_running else 0
        return {
            "target_id": self._target_id,
            "processing": self._is_running,
            "fps_actual": self._update_rate if self._is_running else None,
            "fps_target": self._update_rate,
            "uptime_seconds": uptime,
            "frames_processed": 0,
            "errors_count": 0,
            "last_error": None,
            "last_update": None,
        }

    async def _processing_loop(self) -> None:
        """Main loop: read CSS colors, average per mapping, send to HA lights."""
        interval = 1.0 / self._update_rate

        while self._is_running:
            try:
                loop_start = time.monotonic()

                if self._css_stream and self._ha_runtime and self._ha_runtime.is_connected:
                    colors = self._css_stream.get_latest_colors()
                    if colors is not None and len(colors) > 0:
                        await self._update_lights(colors)

                # Sleep for remaining frame time
                elapsed = time.monotonic() - loop_start
                sleep_time = max(0.05, interval - elapsed)
                await asyncio.sleep(sleep_time)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"HA light {self._target_id} loop error: {e}")
                await asyncio.sleep(1.0)

    async def _update_lights(self, colors: np.ndarray) -> None:
        """Average LED segments and call HA services for changed lights."""
        led_count = len(colors)

        # Get brightness multiplier from value source (1.0 if not configured)
        vs_multiplier = 1.0
        if self._value_stream is not None:
            try:
                vs_multiplier = self._value_stream.get_value()
            except Exception:
                vs_multiplier = 1.0

        for mapping in self._light_mappings:
            if not mapping.entity_id:
                continue

            # Resolve LED range
            start = max(0, mapping.led_start)
            end = mapping.led_end if mapping.led_end >= 0 else led_count
            end = min(end, led_count)
            if start >= end:
                continue

            # Average the LED segment
            segment = colors[start:end]
            avg = segment.mean(axis=0).astype(int)
            r, g, b = int(avg[0]), int(avg[1]), int(avg[2])

            # Cache for WS preview (always, even if HA call is skipped)
            self._latest_entity_colors[mapping.entity_id] = (r, g, b)

            # Calculate brightness (0-255) from max channel
            brightness = max(r, g, b)

            # Apply brightness scale and value source multiplier
            bs = (
                mapping.brightness_scale.value
                if hasattr(mapping.brightness_scale, "value")
                else mapping.brightness_scale
            )
            eff_scale = bs * vs_multiplier
            if eff_scale < 1.0:
                brightness = int(brightness * eff_scale)

            # Check brightness threshold
            should_be_on = (
                brightness >= self._min_brightness_threshold or self._min_brightness_threshold == 0
            )

            entity_id = mapping.entity_id
            prev_color = self._previous_colors.get(entity_id)
            was_on = self._previous_on.get(entity_id, True)

            if should_be_on:
                # Check if color changed beyond tolerance
                new_color = (r, g, b)
                if prev_color is not None and was_on:
                    dr = abs(r - prev_color[0])
                    dg = abs(g - prev_color[1])
                    db = abs(b - prev_color[2])
                    if max(dr, dg, db) < self._color_tolerance:
                        continue  # skip — color hasn't changed enough

                # Call light.turn_on
                service_data = {
                    "rgb_color": [r, g, b],
                    "brightness": min(255, int(brightness * bs)),
                }
                transition_val = self._transition.value
                if transition_val > 0:
                    service_data["transition"] = transition_val

                await self._ha_runtime.call_service(
                    domain="light",
                    service="turn_on",
                    service_data=service_data,
                    target={"entity_id": entity_id},
                )
                self._previous_colors[entity_id] = new_color
                self._previous_on[entity_id] = True

            elif was_on:
                # Brightness dropped below threshold — turn off
                await self._ha_runtime.call_service(
                    domain="light",
                    service="turn_off",
                    service_data={},
                    target={"entity_id": entity_id},
                )
                self._previous_on[entity_id] = False
                self._previous_colors.pop(entity_id, None)

        # Broadcast colors to WS clients
        if self._ws_clients and self._latest_entity_colors:
            await self._broadcast_entity_colors()

    async def _broadcast_entity_colors(self) -> None:
        """Send current entity colors to all connected WS clients."""
        colors_payload = {
            eid: {"r": r, "g": g, "b": b, "hex": f"#{r:02x}{g:02x}{b:02x}"}
            for eid, (r, g, b) in self._latest_entity_colors.items()
        }
        message = json.dumps({"type": "colors_update", "colors": colors_payload})
        dead: List[Any] = []
        for ws in self._ws_clients:
            try:
                await ws.send_text(message)
            except Exception:
                dead.append(ws)
        for ws in dead:
            self._ws_clients.remove(ws)
