"""Device test mode and idle client management mixin for ProcessorManager.

Extracted from processor_manager.py to keep files under 800 lines.
"""

from typing import Dict, List, Optional

from wled_controller.core.capture.calibration import CalibrationConfig
from wled_controller.core.devices.led_client import create_led_client
from wled_controller.utils import get_logger

logger = get_logger(__name__)


class DeviceTestModeMixin:
    """Mixin providing calibration test mode and idle LED client management.

    Requires the host class to have:
        _devices: Dict[str, DeviceState]
        _processors: Dict[str, TargetProcessor]
        _idle_clients: Dict[str, object]
    """

    # ===== CALIBRATION TEST MODE (on device, driven by CSS calibration) =====

    async def set_test_mode(
        self,
        device_id: str,
        edges: Dict[str, List[int]],
        calibration: Optional[CalibrationConfig] = None,
    ) -> None:
        """Set or clear calibration test mode for a device.

        When setting test mode, pass the calibration from the CSS being tested.
        When clearing (edges={}), calibration is not needed.
        """
        if device_id not in self._devices:
            raise ValueError(f"Device {device_id} not found")

        ds = self._devices[device_id]

        if edges:
            ds.test_mode_active = True
            ds.test_mode_edges = {
                edge: tuple(color) for edge, color in edges.items()
            }
            if calibration is not None:
                ds.test_calibration = calibration
            await self._send_test_pixels(device_id)
        else:
            ds.test_mode_active = False
            ds.test_mode_edges = {}
            ds.test_calibration = None
            await self._send_clear_pixels(device_id)

    async def _get_idle_client(self, device_id: str):
        """Get or create a cached idle LED client for a device.

        Reuses an existing connected client to avoid repeated serial
        reconnection (which triggers Arduino bootloader reset on Adalight).
        """
        # Prefer a running processor's client (already connected)
        active = self._find_active_led_client(device_id)
        if active:
            return active

        # Reuse cached idle client if still connected
        cached = self._idle_clients.get(device_id)
        if cached and cached.is_connected:
            return cached

        # Create and cache a new client
        ds = self._devices[device_id]
        client = create_led_client(
            ds.device_type, ds.device_url,
            use_ddp=True, led_count=ds.led_count, baud_rate=ds.baud_rate,
        )
        await client.connect()
        self._idle_clients[device_id] = client
        return client

    async def _close_idle_client(self, device_id: str) -> None:
        """Close and remove the cached idle client for a device."""
        client = self._idle_clients.pop(device_id, None)
        if client:
            try:
                await client.close()
            except Exception as e:
                logger.warning(f"Error closing idle client for {device_id}: {e}")

    async def _send_test_pixels(self, device_id: str) -> None:
        """Build and send test pixel array for active test edges."""
        ds = self._devices[device_id]

        # Require calibration to know which LEDs map to which edges
        if ds.test_calibration is None:
            logger.debug(f"No calibration for test mode on {device_id}, skipping LED test")
            return

        pixels = [(0, 0, 0)] * ds.led_count

        for edge_name, color in ds.test_mode_edges.items():
            for seg in ds.test_calibration.segments:
                if seg.edge == edge_name:
                    for i in range(seg.led_start, seg.led_start + seg.led_count):
                        if i < ds.led_count:
                            pixels[i] = color
                    break

        # Apply offset rotation (same as Phase 2 in PixelMapper.map_border_to_leds)
        total_leds = ds.test_calibration.get_total_leds()
        offset = ds.test_calibration.offset % total_leds if total_leds > 0 else 0
        if offset > 0:
            pixels = pixels[-offset:] + pixels[:-offset]

        await self._send_pixels_to_device(device_id, pixels)

    async def _send_clear_pixels(self, device_id: str) -> None:
        """Send all-black pixels to clear LED output."""
        ds = self._devices[device_id]
        pixels = [(0, 0, 0)] * ds.led_count
        await self._send_pixels_to_device(device_id, pixels)

    async def _send_pixels_to_device(self, device_id: str, pixels) -> None:
        """Send pixels to a device via cached idle client.

        Reuses a cached connection to avoid repeated serial reconnections
        (which trigger Arduino bootloader reset on Adalight devices).
        """
        try:
            client = await self._get_idle_client(device_id)
            await client.send_pixels(pixels)
        except Exception as e:
            logger.error(f"Failed to send pixels to {device_id}: {e}")

    def _find_active_led_client(self, device_id: str):
        """Find an active LED client for a device (from a running processor)."""
        for proc in self._processors.values():
            if proc.device_id == device_id and proc.is_running and proc.led_client:
                return proc.led_client
        return None

    # ===== DISPLAY LOCK INFO =====

    def is_display_locked(self, display_index: int) -> bool:
        """Check if a display is currently being captured by any target."""
        for proc in self._processors.values():
            if proc.is_running and proc.get_display_index() == display_index:
                return True
        return False

    def get_display_lock_info(self, display_index: int) -> Optional[str]:
        """Get the device ID that is currently capturing from a display."""
        for proc in self._processors.values():
            if proc.is_running and proc.get_display_index() == display_index:
                return proc.device_id
        return None

    async def clear_device(self, device_id: str) -> None:
        """Clear LED output on a device (send black / power off)."""
        ds = self._devices.get(device_id)
        if not ds:
            raise ValueError(f"Device {device_id} not found")
        try:
            await self._send_clear_pixels(device_id)
        except Exception as e:
            logger.error(f"Failed to clear device {device_id}: {e}")

    async def _restore_device_idle_state(self, device_id: str) -> None:
        """Restore a device to its idle state when all targets stop.

        - For WLED: do nothing — stop() already restored the snapshot.
        - For serial: do nothing — AdalightClient.close() already sent black frame.
        """
        ds = self._devices.get(device_id)
        if not ds or not ds.auto_shutdown:
            return

        if self.is_device_processing(device_id):
            return

        if ds.device_type == "wled":
            logger.info(f"Auto-restore: WLED device {device_id} restored by snapshot")
        else:
            logger.info(f"Auto-restore: {ds.device_type} device {device_id} dark (closed by processor)")

    async def send_clear_pixels(self, device_id: str) -> None:
        """Send all-black pixels to a device (public wrapper)."""
        await self._send_clear_pixels(device_id)
