"""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, Tuple

import numpy as np

from ledgrab.core.processing.light_target_helpers import swap_color_source
from ledgrab.core.processing.target_processor import TargetContext, TargetProcessor
from ledgrab.storage.ha_light_output_target import HALightMapping
from ledgrab.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,
        source_kind: str = "css",
        color_strip_source_id: str = "",
        color_value_source_id: str = "",
        brightness=None,
        # legacy compat
        brightness_value_source_id: str = "",
        light_mappings: List[HALightMapping] | None = None,
        update_rate: float = 2.0,
        transition=None,
        min_brightness_threshold: int = 0,
        color_tolerance: int = 5,
        stop_action: str = "none",
        ctx: TargetContext | None = None,
    ):
        from ledgrab.storage.bindable import BindableFloat, bfloat

        super().__init__(target_id, ctx)
        self._ha_source_id = ha_source_id
        self._source_kind = source_kind if source_kind in ("css", "color_vs") else "css"
        self._css_id = color_strip_source_id
        self._color_vs_id = color_value_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))
        self._stop_action = (
            stop_action if stop_action in ("none", "turn_off", "restore") else "none"
        )

        # Runtime state
        self._css_stream = None
        self._color_stream = None  # color-returning ValueStream (source_kind="color_vs")
        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]] = {}
        # Snapshot of entity states captured at start() — used by "restore" stop action
        self._captured_states: Dict[str, Any] = {}
        self._ws_clients: List[Any] = []
        self._start_time: float | None = None

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

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

        # Acquire colour source — CSS stream OR colour value stream depending on mode.
        if self._source_kind == "color_vs":
            if self._color_vs_id and self._ctx.value_stream_manager:
                try:
                    self._color_stream = self._ctx.value_stream_manager.acquire(self._color_vs_id)
                except Exception as e:
                    logger.warning(
                        f"HA light {self._target_id}: failed to acquire color VS stream: {e}"
                    )
        else:
            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 ledgrab.core.home_assistant.ha_manager import HomeAssistantManager

            ha_manager: HomeAssistantManager | None = 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

        # Capture initial entity states for "restore" stop action.
        # We always capture (cheap) so changing stop_action while running still works.
        self._captured_states = self._snapshot_mapped_entity_states()

        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

        # Run finalization (turn_off / restore) before releasing the HA runtime.
        try:
            await self._apply_stop_action()
        except Exception as e:
            logger.warning(
                f"HA light {self._target_id}: stop_action '{self._stop_action}' failed: {e}"
            )

        # 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 colour value stream (color_vs mode)
        if self._color_stream is not None and self._ctx.value_stream_manager:
            try:
                self._ctx.value_stream_manager.release(self._color_vs_id)
            except Exception:
                pass
            self._color_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._captured_states.clear()
        self._ws_clients.clear()
        logger.info(f"HA light target stopped: {self._target_id}")

    def update_settings(self, settings) -> None:
        from ledgrab.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"]
            if "stop_action" in settings:
                sa = settings["stop_action"]
                if sa in ("none", "turn_off", "restore"):
                    self._stop_action = sa
            # source_kind / color_value_source_id swap is handled here so that
            # toggling modes (or repointing the colour VS) takes effect without
            # restarting the target.  CSS swaps continue to flow through
            # update_css_source().
            new_kind = settings.get("source_kind")
            new_color_vs = settings.get("color_value_source_id")
            kind_changed = new_kind in ("css", "color_vs") and new_kind != self._source_kind
            color_vs_changed = new_color_vs is not None and new_color_vs != self._color_vs_id
            if kind_changed or color_vs_changed:
                self._swap_color_source(
                    new_kind if kind_changed else self._source_kind,
                    new_color_vs if new_color_vs is not None else self._color_vs_id,
                )

    def update_css_source(self, color_strip_source_id: str) -> None:
        """Hot-swap the CSS stream (only meaningful when source_kind='css')."""
        old_id = self._css_id
        self._css_id = color_strip_source_id

        if self._source_kind == "css" and 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}")

    def _swap_color_source(self, new_kind: str, new_color_vs_id: str) -> None:
        """Release the previous colour stream and acquire the new one."""
        swap_color_source(self, new_kind, new_color_vs_id, log_label="HA light")
        # Reset per-entity history so the new source isn't gated by stale values.
        self._previous_colors.clear()
        self._previous_on.clear()

    # ── 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,
            "source_kind": self._source_kind,
            "css_id": self._css_id,
            "color_value_source_id": self._color_vs_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,
            "fps_capture": self._update_rate if self._is_running else None,
            "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 source colour(s) and send to HA lights."""
        interval = 1.0 / self._update_rate

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

                ha_ready = self._ha_runtime and self._ha_runtime.is_connected
                if ha_ready:
                    if self._source_kind == "color_vs" and self._color_stream is not None:
                        try:
                            color = self._color_stream.get_color()
                        except Exception:
                            color = None
                        if isinstance(color, (list, tuple)) and len(color) >= 3:
                            await self._update_lights_single_color(
                                int(color[0]), int(color[1]), int(color[2])
                            )
                    elif self._css_stream is not None:
                        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)

    def _read_brightness_multiplier(self) -> float:
        if self._value_stream is None:
            return 1.0
        try:
            return float(self._value_stream.get_value())
        except Exception:
            return 1.0

    async def _send_entity_color(
        self, mapping: HALightMapping, r: int, g: int, b: int, vs_multiplier: float
    ) -> None:
        """Apply tolerance/threshold gates and push one entity update."""
        entity_id = mapping.entity_id
        # Cache for WS preview (always, even if HA call is skipped)
        self._latest_entity_colors[entity_id] = (r, g, b)

        # Brightness (0-255) is derived from the max channel and scaled once by
        # the per-mapping brightness_scale × value-source multiplier. eff_scale
        # may exceed 1.0 (a boosting value source), so we clamp at 255.
        bs = (
            mapping.brightness_scale.value
            if hasattr(mapping.brightness_scale, "value")
            else mapping.brightness_scale
        )
        eff_scale = bs * vs_multiplier
        brightness = max(0, min(255, int(max(r, g, b) * eff_scale)))

        should_be_on = (
            brightness >= self._min_brightness_threshold or self._min_brightness_threshold == 0
        )

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

        if should_be_on:
            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:
                    return  # skip — colour hasn't changed enough

            service_data = {
                "rgb_color": [r, g, b],
                "brightness": brightness,
            }
            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:
            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)

    async def _update_lights(self, colors: np.ndarray) -> None:
        """CSS mode: average each mapping's LED segment and dispatch."""
        led_count = len(colors)
        vs_multiplier = self._read_brightness_multiplier()

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

            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

            segment = colors[start:end]
            avg = segment.mean(axis=0).astype(int)
            await self._send_entity_color(
                mapping, int(avg[0]), int(avg[1]), int(avg[2]), vs_multiplier
            )

        if self._ws_clients and self._latest_entity_colors:
            await self._broadcast_entity_colors()

    async def _update_lights_single_color(self, r: int, g: int, b: int) -> None:
        """color_vs mode: push the same RGB triple to every mapping."""
        vs_multiplier = self._read_brightness_multiplier()

        for mapping in self._light_mappings:
            if not mapping.entity_id:
                continue
            await self._send_entity_color(mapping, r, g, b, vs_multiplier)

        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)

    # ── Stop-action finalization ──

    def _snapshot_mapped_entity_states(self) -> Dict[str, Any]:
        """Capture current state of every mapped entity from the HA cache."""
        if not self._ha_runtime:
            return {}
        snap: Dict[str, Any] = {}
        for mapping in self._light_mappings:
            eid = mapping.entity_id
            if not eid:
                continue
            state = self._ha_runtime.get_state(eid)
            if state is not None:
                snap[eid] = state
        return snap

    def _unique_mapped_entity_ids(self) -> List[str]:
        """Return unique non-empty entity ids from configured mappings."""
        entity_ids: List[str] = []
        seen = set()
        for mapping in self._light_mappings:
            eid = mapping.entity_id
            if eid and eid not in seen:
                seen.add(eid)
                entity_ids.append(eid)
        return entity_ids

    async def turn_off_lights(self) -> int:
        """Turn off every mapped HA light entity.

        Works regardless of whether the processor is currently running.
        If the HA runtime isn't already acquired, temporarily acquires it
        from the manager so the call can succeed without starting the
        target. Returns the number of entities the turn_off was issued for.
        """
        entity_ids = self._unique_mapped_entity_ids()
        if not entity_ids:
            return 0

        # Use existing runtime when running; otherwise borrow one from the
        # manager via acquire/release so we don't keep a connection open.
        ha_manager = getattr(self._ctx, "ha_manager", None)
        runtime = self._ha_runtime
        borrowed = False
        if runtime is None:
            if ha_manager is None or not self._ha_source_id:
                raise RuntimeError("HA runtime not available")
            runtime = await ha_manager.acquire(self._ha_source_id)
            borrowed = True

        try:
            if not runtime.is_connected:
                raise RuntimeError("HA not connected")
            for eid in entity_ids:
                await runtime.call_service(
                    domain="light",
                    service="turn_off",
                    service_data={},
                    target={"entity_id": eid},
                )
        finally:
            if borrowed and ha_manager is not None:
                try:
                    await ha_manager.release(self._ha_source_id)
                except Exception:
                    pass

        return len(entity_ids)

    async def _apply_stop_action(self) -> None:
        """Run the configured finalization on stop."""
        if self._stop_action == "none":
            return
        if not self._ha_runtime or not self._ha_runtime.is_connected:
            logger.info(
                f"HA light {self._target_id}: skipping stop_action "
                f"'{self._stop_action}' — HA not connected"
            )
            return

        entity_ids = self._unique_mapped_entity_ids()
        if not entity_ids:
            return

        if self._stop_action == "turn_off":
            for eid in entity_ids:
                await self._ha_runtime.call_service(
                    domain="light",
                    service="turn_off",
                    service_data={},
                    target={"entity_id": eid},
                )
            return

        if self._stop_action == "restore":
            for eid in entity_ids:
                state = self._captured_states.get(eid)
                if state is None:
                    continue
                await self._restore_entity(eid, state)

    async def _restore_entity(self, entity_id: str, state: Any) -> None:
        """Restore one light entity to a captured HAEntityState."""
        if state.state == "off":
            await self._ha_runtime.call_service(
                domain="light",
                service="turn_off",
                service_data={},
                target={"entity_id": entity_id},
            )
            return

        if state.state != "on":
            # unknown / unavailable — best effort: do nothing
            return

        attrs = state.attributes or {}
        service_data: Dict[str, Any] = {}

        # Color: prefer rgb_color, then hs_color, then color_temp, then nothing
        rgb = attrs.get("rgb_color")
        if isinstance(rgb, (list, tuple)) and len(rgb) >= 3:
            service_data["rgb_color"] = [int(rgb[0]), int(rgb[1]), int(rgb[2])]
        else:
            hs = attrs.get("hs_color")
            color_temp = attrs.get("color_temp")
            color_temp_kelvin = attrs.get("color_temp_kelvin")
            if isinstance(hs, (list, tuple)) and len(hs) >= 2:
                service_data["hs_color"] = [float(hs[0]), float(hs[1])]
            elif color_temp_kelvin is not None:
                service_data["color_temp_kelvin"] = int(color_temp_kelvin)
            elif color_temp is not None:
                service_data["color_temp"] = int(color_temp)

        brightness = attrs.get("brightness")
        if brightness is not None:
            service_data["brightness"] = int(brightness)

        await self._ha_runtime.call_service(
            domain="light",
            service="turn_on",
            service_data=service_data,
            target={"entity_id": entity_id},
        )
