"""Device routes: CRUD, health state, brightness, power, calibration, WS stream."""

import httpx
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect

from ledgrab.api.auth import AuthRequired
from ledgrab.core.devices.led_client import (
    PairingNotReady,
    get_all_providers,
    get_device_capabilities,
    get_provider,
)
from ledgrab.api.dependencies import (
    fire_entity_event,
    get_device_store,
    get_output_target_store,
    get_processor_manager,
)
from ledgrab.api.schemas.devices import (
    BrightnessRequest,
    DeviceCreate,
    DeviceListResponse,
    DeviceResponse,
    DeviceStateResponse,
    DeviceUpdate,
    DiscoveredDeviceResponse,
    DiscoverDevicesResponse,
    OpenRGBZoneResponse,
    OpenRGBZonesResponse,
    PairDeviceRequest,
    PairDeviceResponse,
    PowerRequest,
)
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage import DeviceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.utils import get_logger
from ledgrab.utils.url_scheme import infer_http_scheme

logger = get_logger(__name__)

router = APIRouter()


def _sanitize_url_for_log(url: str) -> str:
    """Strip userinfo + fragment from a URL so secrets don't reach logs.

    The pair endpoint receives a user-supplied URL on every call; if a
    future driver ever accepts ``scheme://user:pass@host`` form the
    credentials would land in logs without this guard.
    """
    if not url:
        return ""
    try:
        from urllib.parse import urlparse, urlunparse

        parsed = urlparse(url)
        # urlparse stores userinfo in `netloc`; rebuild without it.
        if parsed.hostname:
            netloc = parsed.hostname
            if parsed.port:
                netloc = f"{netloc}:{parsed.port}"
            return urlunparse((parsed.scheme, netloc, parsed.path, parsed.params, parsed.query, ""))
    except ValueError:
        pass
    return url


def _device_to_response(device) -> DeviceResponse:
    """Convert a Device to DeviceResponse."""
    return DeviceResponse(
        id=device.id,
        name=device.name,
        url=device.url,
        device_type=device.device_type,
        led_count=device.led_count,
        enabled=device.enabled,
        baud_rate=device.baud_rate,
        auto_shutdown=device.auto_shutdown,
        send_latency_ms=device.send_latency_ms,
        rgbw=device.rgbw,
        zone_mode=device.zone_mode,
        capabilities=sorted(get_device_capabilities(device.device_type)),
        tags=device.tags,
        dmx_protocol=device.dmx_protocol,
        dmx_start_universe=device.dmx_start_universe,
        dmx_start_channel=device.dmx_start_channel,
        ddp_port=device.ddp_port,
        ddp_destination_id=device.ddp_destination_id,
        ddp_color_order=device.ddp_color_order,
        espnow_peer_mac=device.espnow_peer_mac,
        espnow_channel=device.espnow_channel,
        hue_paired=bool(device.hue_username and device.hue_client_key),
        hue_entertainment_group_id=device.hue_entertainment_group_id,
        yeelight_min_interval_ms=device.yeelight_min_interval_ms,
        wiz_min_interval_ms=device.wiz_min_interval_ms,
        lifx_min_interval_ms=device.lifx_min_interval_ms,
        govee_min_interval_ms=device.govee_min_interval_ms,
        opc_channel=device.opc_channel,
        nanoleaf_paired=bool(device.nanoleaf_token),
        nanoleaf_min_interval_ms=device.nanoleaf_min_interval_ms,
        spi_speed_hz=device.spi_speed_hz,
        spi_led_type=device.spi_led_type,
        chroma_device_type=device.chroma_device_type,
        gamesense_device_type=device.gamesense_device_type,
        ble_family=device.ble_family,
        ble_govee_key=device.ble_govee_key,
        default_css_processing_template_id=device.default_css_processing_template_id,
        group_device_ids=device.group_device_ids,
        group_mode=device.group_mode,
        icon=getattr(device, "icon", "") or "",
        icon_color=getattr(device, "icon_color", "") or "",
        created_at=device.created_at,
        updated_at=device.updated_at,
    )


# ===== DEVICE MANAGEMENT ENDPOINTS =====


@router.post("/api/v1/devices", response_model=DeviceResponse, tags=["Devices"], status_code=201)
async def create_device(
    device_data: DeviceCreate,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Create and attach a new LED device."""
    try:
        device_type = device_data.device_type
        logger.info(f"Creating {device_type} device: {device_data.name}")

        # ── Group device: validate children + compute LED count ──
        if device_type == "group":
            group_device_ids = device_data.group_device_ids or []
            group_mode = device_data.group_mode or "sequence"
            if not group_device_ids:
                raise HTTPException(
                    status_code=422,
                    detail="Group device requires at least one child device.",
                )
            if group_mode not in ("sequence", "independent"):
                raise HTTPException(
                    status_code=422,
                    detail=f"Invalid group mode: {group_mode}. Must be 'sequence' or 'independent'.",
                )
            # Validate all children exist and no cycles
            try:
                store.validate_group_no_cycles(None, group_device_ids)
            except ValueError as e:
                raise HTTPException(status_code=422, detail=str(e))
            # Compute LED count
            if group_mode == "sequence":
                led_count = store.resolve_group_led_count(group_device_ids)
            else:
                led_count = device_data.led_count or store.resolve_group_max_led_count(
                    group_device_ids
                )
            if not led_count or led_count < 1:
                raise HTTPException(
                    status_code=422,
                    detail="Could not determine LED count for group device.",
                )
            device_url = "group://virtual"
        else:
            group_device_ids = []
            group_mode = "sequence"
            if not device_data.url:
                raise HTTPException(
                    status_code=422,
                    detail="URL is required for non-group device types.",
                )
            device_url = device_data.url.rstrip("/")
            if device_type == "wled":
                device_url = infer_http_scheme(device_url)

        # ── Non-group: validate via provider ──
        if device_type != "group":
            # Validate via provider
            try:
                provider = get_provider(device_type)
            except ValueError:
                raise HTTPException(
                    status_code=400, detail=f"Unsupported device type: {device_type}"
                )

            try:
                result = await provider.validate_device(device_url)
                led_count = result.get("led_count") or device_data.led_count
                if not led_count or led_count < 1:
                    raise HTTPException(
                        status_code=422,
                        detail="LED count is required for this device type.",
                    )
            except httpx.ConnectError:
                raise HTTPException(
                    status_code=422,
                    detail=f"Cannot reach {device_type} device at {device_url}. Check the URL and ensure the device is powered on.",
                )
            except httpx.TimeoutException:
                raise HTTPException(
                    status_code=422,
                    detail=f"Connection to {device_url} timed out. Check network connectivity.",
                )
            except ValueError as e:
                raise HTTPException(status_code=422, detail=str(e))
            except HTTPException:
                raise
            except Exception as e:
                # Don't leak the raw exception text — it can carry stack
                # frames, host headers, or other internals that aren't safe
                # to echo. Log with full context, return a generic message.
                logger.warning(
                    "Failed to validate %s device at %s: %s",
                    device_type,
                    device_url,
                    e,
                    exc_info=True,
                )
                raise HTTPException(
                    status_code=422,
                    detail=f"Failed to connect to {device_type} device at {device_url}.",
                )

        # Resolve auto_shutdown default: False for all types
        auto_shutdown = device_data.auto_shutdown
        if auto_shutdown is None:
            auto_shutdown = False

        # Create device in storage
        device = store.create_device(
            name=device_data.name,
            url=device_url,
            led_count=led_count,
            device_type=device_type,
            baud_rate=device_data.baud_rate,
            auto_shutdown=auto_shutdown,
            send_latency_ms=device_data.send_latency_ms or 0,
            rgbw=device_data.rgbw or False,
            zone_mode=device_data.zone_mode or "combined",
            tags=device_data.tags,
            dmx_protocol=device_data.dmx_protocol or "artnet",
            dmx_start_universe=device_data.dmx_start_universe or 0,
            dmx_start_channel=device_data.dmx_start_channel or 1,
            ddp_port=device_data.ddp_port or 0,
            ddp_destination_id=(
                device_data.ddp_destination_id if device_data.ddp_destination_id is not None else 1
            ),
            ddp_color_order=(
                device_data.ddp_color_order if device_data.ddp_color_order is not None else 1
            ),
            espnow_peer_mac=device_data.espnow_peer_mac or "",
            espnow_channel=device_data.espnow_channel or 1,
            hue_username=device_data.hue_username or "",
            hue_client_key=device_data.hue_client_key or "",
            hue_entertainment_group_id=device_data.hue_entertainment_group_id or "",
            yeelight_min_interval_ms=(
                device_data.yeelight_min_interval_ms
                if device_data.yeelight_min_interval_ms is not None
                else 500
            ),
            wiz_min_interval_ms=(
                device_data.wiz_min_interval_ms
                if device_data.wiz_min_interval_ms is not None
                else 50
            ),
            lifx_min_interval_ms=(
                device_data.lifx_min_interval_ms
                if device_data.lifx_min_interval_ms is not None
                else 50
            ),
            govee_min_interval_ms=(
                device_data.govee_min_interval_ms
                if device_data.govee_min_interval_ms is not None
                else 50
            ),
            opc_channel=(device_data.opc_channel if device_data.opc_channel is not None else 0),
            nanoleaf_token=device_data.nanoleaf_token or "",
            nanoleaf_min_interval_ms=(
                device_data.nanoleaf_min_interval_ms
                if device_data.nanoleaf_min_interval_ms is not None
                else 100
            ),
            spi_speed_hz=device_data.spi_speed_hz or 800000,
            spi_led_type=device_data.spi_led_type or "WS2812B",
            chroma_device_type=device_data.chroma_device_type or "chromalink",
            gamesense_device_type=device_data.gamesense_device_type or "keyboard",
            ble_family=device_data.ble_family or "",
            ble_govee_key=device_data.ble_govee_key or "",
            group_device_ids=group_device_ids,
            group_mode=group_mode,
        )

        # WS devices: auto-set URL to ws://{device_id}
        if device_type == "ws":
            device = store.update_device(device.id, url=f"ws://{device.id}")

        # Register in processor manager for health monitoring
        manager.add_device(
            device_id=device.id,
            device_url=device.url,
            led_count=device.led_count,
            device_type=device.device_type,
            baud_rate=device.baud_rate,
            auto_shutdown=device.auto_shutdown,
            zone_mode=device.zone_mode,
        )

        fire_entity_event("device", "created", device.id)
        return _device_to_response(device)

    except HTTPException:
        raise
    except Exception as e:
        logger.error("Failed to create device: %s", e, exc_info=True)
        raise HTTPException(status_code=500, detail="Internal server error")


@router.post(
    "/api/v1/devices/pair",
    response_model=PairDeviceResponse,
    tags=["Devices"],
)
async def pair_device(
    body: PairDeviceRequest,
    _auth: AuthRequired,
):
    """Run a pairing handshake against a device before creating it.

    The frontend opens this endpoint after the user has performed the
    device's physical pairing action (e.g. held the power button for 5s).
    The response carries provider-specific fields the caller must include
    in the subsequent ``POST /api/v1/devices`` body.

    Status codes:
      200  paired — fields returned
      400  unknown device type, or device type does not support pairing
      409  device not ready — user must perform the physical action
            (or retry, e.g. the pairing window timed out)
      422  invalid URL or device configuration
    """
    try:
        provider = get_provider(body.device_type)
    except ValueError:
        raise HTTPException(status_code=400, detail=f"Unknown device type: {body.device_type}")

    try:
        fields = await provider.pair_device(body.url)
    except NotImplementedError:
        raise HTTPException(
            status_code=400,
            detail=f"Device type {body.device_type!r} does not support pairing",
        )
    except PairingNotReady as exc:
        raise HTTPException(status_code=409, detail=str(exc))
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc))
    except Exception as exc:
        # Strip userinfo before logging so a `scheme://user:pass@host` URL
        # never lands in the logs (no shipped driver uses userinfo today,
        # but the pattern is a foot-gun for the next driver author --
        # caught by review MEDIUM #9). Also keep exc_info=False so a
        # provider stack trace that may include response bytes from a
        # hostile receiver doesn't end up in the file either.
        safe_url = _sanitize_url_for_log(body.url)
        logger.warning(
            "Pairing failed for %s at %s: %s: %s",
            body.device_type,
            safe_url,
            type(exc).__name__,
            exc,
        )
        raise HTTPException(
            status_code=502,
            detail=f"Pairing failed for {body.device_type} at {safe_url}.",
        )

    if not isinstance(fields, dict):
        logger.warning(
            "Provider %s.pair_device returned %r (expected dict)",
            body.device_type,
            type(fields).__name__,
        )
        raise HTTPException(
            status_code=500,
            detail=f"Provider {body.device_type!r} returned malformed pairing result",
        )

    return PairDeviceResponse(fields=fields)


@router.get("/api/v1/devices", response_model=DeviceListResponse, tags=["Devices"])
async def list_devices(
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
):
    """List all attached WLED devices."""
    devices = store.get_all_devices()
    responses = [_device_to_response(d) for d in devices]
    return DeviceListResponse(devices=responses, count=len(responses))


@router.get("/api/v1/devices/discover", response_model=DiscoverDevicesResponse, tags=["Devices"])
async def discover_devices(
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    timeout: float = 3.0,
    device_type: str | None = None,
):
    """Scan for LED devices. Optionally filter by device_type (e.g. wled, adalight)."""
    import asyncio
    import time

    start = time.time()
    capped_timeout = min(timeout, 10.0)

    if device_type:
        # Discover from a single provider
        try:
            provider = get_provider(device_type)
        except ValueError:
            raise HTTPException(status_code=400, detail=f"Unknown device type: {device_type}")
        discovered = await provider.discover(timeout=capped_timeout)
    else:
        # Discover from all providers in parallel. Discovery is best-effort:
        # one provider failing (firewall, missing dep, mDNS race) must not
        # take the entire scan down, so collect exceptions instead of
        # raising and log them individually.
        providers = get_all_providers()
        provider_items = list(providers.items())
        discover_tasks = [p.discover(timeout=capped_timeout) for _, p in provider_items]
        all_results = await asyncio.gather(*discover_tasks, return_exceptions=True)
        discovered = []
        for (name, _), result in zip(provider_items, all_results):
            if isinstance(result, BaseException):
                logger.warning("Discovery failed for provider %s: %s", name, result)
                continue
            discovered.extend(result)
    elapsed_ms = (time.time() - start) * 1000

    existing_urls = {d.url.rstrip("/").lower() for d in store.get_all_devices()}

    results = []
    for d in discovered:
        already_added = d.url.rstrip("/").lower() in existing_urls
        results.append(
            DiscoveredDeviceResponse(
                name=d.name,
                url=d.url,
                device_type=d.device_type,
                ip=d.ip,
                mac=d.mac,
                led_count=d.led_count,
                version=d.version,
                ble_family=getattr(d, "ble_family", None),
                already_added=already_added,
            )
        )

    return DiscoverDevicesResponse(
        devices=results,
        count=len(results),
        scan_duration_ms=round(elapsed_ms, 1),
    )


@router.get("/api/v1/devices/openrgb-zones", response_model=OpenRGBZonesResponse, tags=["Devices"])
async def get_openrgb_zones(
    _auth: AuthRequired,
    url: str = Query(..., description="Base OpenRGB URL (e.g. openrgb://localhost:6742/0)"),
):
    """List available zones on an OpenRGB device."""
    import asyncio

    from ledgrab.core.devices.openrgb_client import parse_openrgb_url

    host, port, device_index, _zones = parse_openrgb_url(url)

    def _fetch_zones():
        from openrgb import OpenRGBClient

        client = OpenRGBClient(host, port, name="LedGrab (zones)")
        try:
            devices = client.devices
            if device_index >= len(devices):
                raise ValueError(
                    f"Device index {device_index} out of range "
                    f"(server has {len(devices)} device(s))"
                )
            device = devices[device_index]
            zone_type_map = {0: "single", 1: "linear", 2: "matrix"}
            zones = []
            for z in device.zones:
                zt = zone_type_map.get(getattr(z, "type", -1), "unknown")
                zones.append(
                    OpenRGBZoneResponse(
                        name=z.name,
                        led_count=len(z.leds),
                        zone_type=zt,
                    )
                )
            return device.name, zones
        finally:
            client.disconnect()

    try:
        device_name, zones = await asyncio.to_thread(_fetch_zones)
        return OpenRGBZonesResponse(device_name=device_name, zones=zones)
    except ValueError as e:
        raise HTTPException(status_code=422, detail=str(e))
    except Exception as e:
        logger.error(f"Failed to list OpenRGB zones: {e}")
        raise HTTPException(status_code=502, detail=f"Cannot reach OpenRGB server: {e}")


@router.get("/api/v1/devices/batch/states", tags=["Devices"])
async def batch_device_states(
    _auth: AuthRequired,
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get health/connection state for all devices in a single request."""
    return {"states": manager.get_all_device_health_dicts()}


@router.get("/api/v1/devices/{device_id}", response_model=DeviceResponse, tags=["Devices"])
async def get_device(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
):
    """Get device details by ID."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    return _device_to_response(device)


@router.put("/api/v1/devices/{device_id}", response_model=DeviceResponse, tags=["Devices"])
async def update_device(
    device_id: str,
    update_data: DeviceUpdate,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Update device information."""
    try:
        # Group-specific validation before applying update
        existing = store.get_device(device_id)
        is_group = existing.device_type == "group"

        # Normalize URL the same way we do on create:
        #   * always rstrip trailing slashes (so PUT-with-trailing-/ matches
        #     POST-with-trailing-/ in the stored value -- caught by review HIGH #6)
        #   * only WLED gets http/https scheme inference; other schemes
        #     (yeelight://, lifx://, opc://, ddp://, …) pass through.
        # Done via a local rather than mutating the request DTO so the
        # input is preserved for any future caller that inspects it.
        normalized_url = update_data.url
        if update_data.url:
            normalized_url = update_data.url.rstrip("/")
            if existing.device_type == "wled":
                inferred = infer_http_scheme(normalized_url)
                if inferred != normalized_url:
                    logger.debug("Inferred WLED URL scheme: %r -> %r", normalized_url, inferred)
                normalized_url = inferred

        # Group-only field overrides (led_count auto-recompute) are accumulated
        # here too so the update_data Pydantic model is not mutated in place.
        normalized_led_count = update_data.led_count

        if is_group:
            new_children = update_data.group_device_ids
            new_mode = update_data.group_mode or existing.group_mode
            if new_children is not None:
                if not new_children:
                    raise HTTPException(
                        status_code=422,
                        detail="Group device requires at least one child device.",
                    )
                try:
                    store.validate_group_no_cycles(device_id, new_children)
                except ValueError as e:
                    raise HTTPException(status_code=422, detail=str(e))
            effective_children = (
                new_children if new_children is not None else existing.group_device_ids
            )
            effective_mode = new_mode

            # Auto-recompute led_count for sequence mode
            if effective_mode == "sequence":
                normalized_led_count = store.resolve_group_led_count(effective_children)
            elif (
                normalized_led_count is None
                and new_mode == "independent"
                and new_children is not None
            ):
                normalized_led_count = store.resolve_group_max_led_count(effective_children)

        device = store.update_device(
            device_id=device_id,
            name=update_data.name,
            url=normalized_url,
            enabled=update_data.enabled,
            led_count=normalized_led_count,
            baud_rate=update_data.baud_rate,
            auto_shutdown=update_data.auto_shutdown,
            send_latency_ms=update_data.send_latency_ms,
            rgbw=update_data.rgbw,
            zone_mode=update_data.zone_mode,
            tags=update_data.tags,
            dmx_protocol=update_data.dmx_protocol,
            dmx_start_universe=update_data.dmx_start_universe,
            dmx_start_channel=update_data.dmx_start_channel,
            ddp_port=update_data.ddp_port,
            ddp_destination_id=update_data.ddp_destination_id,
            ddp_color_order=update_data.ddp_color_order,
            espnow_peer_mac=update_data.espnow_peer_mac,
            espnow_channel=update_data.espnow_channel,
            hue_username=update_data.hue_username,
            hue_client_key=update_data.hue_client_key,
            hue_entertainment_group_id=update_data.hue_entertainment_group_id,
            yeelight_min_interval_ms=update_data.yeelight_min_interval_ms,
            wiz_min_interval_ms=update_data.wiz_min_interval_ms,
            lifx_min_interval_ms=update_data.lifx_min_interval_ms,
            govee_min_interval_ms=update_data.govee_min_interval_ms,
            opc_channel=update_data.opc_channel,
            nanoleaf_token=update_data.nanoleaf_token,
            nanoleaf_min_interval_ms=update_data.nanoleaf_min_interval_ms,
            spi_speed_hz=update_data.spi_speed_hz,
            spi_led_type=update_data.spi_led_type,
            chroma_device_type=update_data.chroma_device_type,
            gamesense_device_type=update_data.gamesense_device_type,
            ble_family=update_data.ble_family,
            ble_govee_key=update_data.ble_govee_key,
            group_device_ids=update_data.group_device_ids,
            group_mode=update_data.group_mode,
            icon=update_data.icon,
            icon_color=update_data.icon_color,
        )

        # Sync connection info in processor manager.
        #
        # When a PATCH omits `url` (rename / icon-only edit) `normalized_url`
        # is None — fall back to the existing record's URL so the processor
        # is always told the current address, otherwise it silently keeps
        # whatever it had cached (or worse, treats None as "unconfigured"
        # and refuses to re-sync).
        effective_url = normalized_url if normalized_url is not None else existing.url
        try:
            manager.update_device_info(
                device_id,
                device_url=effective_url,
                led_count=normalized_led_count,
                baud_rate=update_data.baud_rate,
            )
        except ValueError as e:
            logger.debug("Processor manager device update skipped for %s: %s", device_id, e)

        # Sync auto_shutdown and zone_mode in runtime state
        ds = manager.find_device_state(device_id)
        if ds:
            if update_data.auto_shutdown is not None:
                ds.auto_shutdown = update_data.auto_shutdown
            if update_data.zone_mode is not None:
                ds.zone_mode = update_data.zone_mode

        fire_entity_event("device", "updated", device_id)
        return _device_to_response(device)

    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    except Exception as e:
        logger.error("Failed to update device: %s", e, exc_info=True)
        raise HTTPException(status_code=500, detail="Internal server error")


@router.delete("/api/v1/devices/{device_id}", status_code=204, tags=["Devices"])
async def delete_device(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    target_store: OutputTargetStore = Depends(get_output_target_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Delete/detach a device. Returns 409 if referenced by a target."""
    try:
        # Check if any target references this device
        refs = target_store.get_targets_for_device(device_id)
        if refs:
            names = ", ".join(t.name for t in refs)
            raise HTTPException(
                status_code=409,
                detail=f"Device is referenced by target(s): {names}. Delete the target(s) first.",
            )

        # Check if any group references this device
        group_refs = store.get_groups_referencing(device_id)
        if group_refs:
            names = ", ".join(g.name for g in group_refs)
            raise HTTPException(
                status_code=409,
                detail=f"Device is referenced by group(s): {names}. Remove from group(s) first.",
            )

        # Remove from manager
        try:
            await manager.remove_device(device_id)
        except (ValueError, RuntimeError):
            pass

        # Delete from storage
        store.delete_device(device_id)

        fire_entity_event("device", "deleted", device_id)
        logger.info(f"Deleted device {device_id}")

    except HTTPException:
        raise
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    except Exception as e:
        logger.error("Failed to delete device: %s", e, exc_info=True)
        raise HTTPException(status_code=500, detail="Internal server error")


# ===== DEVICE STATE (health only) =====


@router.get(
    "/api/v1/devices/{device_id}/state", response_model=DeviceStateResponse, tags=["Devices"]
)
async def get_device_state(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get device health/connection state."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))

    try:
        state = manager.get_device_health_dict(device_id)
        state["device_type"] = device.device_type
        return DeviceStateResponse(**state)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))


@router.post(
    "/api/v1/devices/{device_id}/ping", response_model=DeviceStateResponse, tags=["Devices"]
)
async def ping_device(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Force an immediate health check on a device."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))

    try:
        state = await manager.force_device_health_check(device_id)
        state["device_type"] = device.device_type
        return DeviceStateResponse(**state)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))


# ===== WLED BRIGHTNESS ENDPOINTS =====


@router.get("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
async def get_device_brightness(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get current brightness from the device.

    Uses a server-side cache to avoid polling the physical device on every
    frontend request — hitting the ESP32 over WiFi in the async event loop
    causes ~150 ms jitter in the processing loop.
    """
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    if "brightness_control" not in get_device_capabilities(device.device_type):
        raise HTTPException(
            status_code=400,
            detail=f"Brightness control is not supported for {device.device_type} devices",
        )

    # Return cached hardware brightness if available (updated by SET endpoint)
    ds = manager.find_device_state(device_id)
    if ds and ds.hardware_brightness is not None:
        return {"brightness": ds.hardware_brightness}

    try:
        provider = get_provider(device.device_type)
        bri = await provider.get_brightness(device.url)
        # Cache the result so subsequent polls don't hit the device
        if ds:
            ds.hardware_brightness = bri
        return {"brightness": bri}
    except NotImplementedError:
        # Provider has no hardware brightness; use software brightness
        return {"brightness": device.software_brightness}
    except Exception as e:
        logger.error(f"Failed to get brightness for {device_id}: {e}")
        raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")


@router.put("/api/v1/devices/{device_id}/brightness", tags=["Settings"])
async def set_device_brightness(
    device_id: str,
    body: BrightnessRequest,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Set brightness on the device."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    if "brightness_control" not in get_device_capabilities(device.device_type):
        raise HTTPException(
            status_code=400,
            detail=f"Brightness control is not supported for {device.device_type} devices",
        )

    bri = body.brightness

    try:
        try:
            provider = get_provider(device.device_type)
            await provider.set_brightness(device.url, bri)
        except NotImplementedError:
            # Provider has no hardware brightness; use software brightness
            store.update_device(device_id=device_id, software_brightness=bri)
            ds = manager.find_device_state(device_id)
            if ds:
                ds.software_brightness = bri

        # Update cached hardware brightness
        ds = manager.find_device_state(device_id)
        if ds:
            ds.hardware_brightness = bri

        return {"brightness": bri}
    except Exception as e:
        logger.error(f"Failed to set brightness for {device_id}: {e}")
        raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")


# ===== POWER ENDPOINTS =====


@router.get("/api/v1/devices/{device_id}/power", tags=["Settings"])
async def get_device_power(
    device_id: str,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get current power state from the device."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    if "power_control" not in get_device_capabilities(device.device_type):
        raise HTTPException(
            status_code=400,
            detail=f"Power control is not supported for {device.device_type} devices",
        )

    try:
        # Serial devices: use tracked state (no hardware query available)
        ds = manager.find_device_state(device_id)
        if device.device_type in ("adalight", "ambiled") and ds:
            return {"on": ds.power_on}

        provider = get_provider(device.device_type)
        on = await provider.get_power(device.url)
        return {"on": on}
    except Exception as e:
        logger.error(f"Failed to get power for {device_id}: {e}")
        raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")


@router.put("/api/v1/devices/{device_id}/power", tags=["Settings"])
async def set_device_power(
    device_id: str,
    body: PowerRequest,
    _auth: AuthRequired,
    store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Turn device on or off."""
    try:
        device = store.get_device(device_id)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    if "power_control" not in get_device_capabilities(device.device_type):
        raise HTTPException(
            status_code=400,
            detail=f"Power control is not supported for {device.device_type} devices",
        )

    on = body.power

    try:
        # For serial devices, use the cached idle client to avoid port conflicts
        ds = manager.find_device_state(device_id)
        if device.device_type in ("adalight", "ambiled") and ds:
            if not on:
                await manager.send_clear_pixels(device_id)
            ds.power_on = on
        else:
            provider = get_provider(device.device_type)
            await provider.set_power(
                device.url,
                on,
                led_count=device.led_count,
                baud_rate=device.baud_rate,
            )
        return {"on": on}
    except Exception as e:
        logger.error(f"Failed to set power for {device_id}: {e}")
        raise HTTPException(status_code=502, detail=f"Failed to reach device: {e}")


# ===== WEBSOCKET DEVICE STREAM =====


@router.websocket("/api/v1/devices/{device_id}/ws")
async def device_ws_stream(
    websocket: WebSocket,
    device_id: str,
):
    """WebSocket stream of LED pixel data for WS device type.

    Wire format: [brightness_byte][R G B R G B ...]
    Auth via first-message handshake: ``{"type":"auth","token":"..."}``
    within 3 s of connect.
    """
    from ledgrab.api.auth import accept_and_authenticate_ws

    if await accept_and_authenticate_ws(websocket) is None:
        return

    store = get_device_store()
    try:
        device = store.get_device(device_id)
    except ValueError:
        await websocket.close(code=4004, reason="Device not found")
        return
    if device.device_type != "ws":
        await websocket.close(code=4003, reason="Device is not a WebSocket device")
        return

    from ledgrab.core.devices.ws_client import get_ws_broadcaster

    broadcaster = get_ws_broadcaster()
    broadcaster.add_client(device_id, websocket)

    try:
        while True:
            await websocket.receive_text()
    except WebSocketDisconnect:
        logger.debug("Device event WebSocket disconnected for %s", device_id)
        pass
    finally:
        broadcaster.remove_client(device_id, websocket)
