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

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

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

logger = get_logger(__name__)

router = APIRouter()


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,
        espnow_peer_mac=device.espnow_peer_mac,
        espnow_channel=device.espnow_channel,
        hue_username=device.hue_username,
        hue_client_key=device.hue_client_key,
        hue_entertainment_group_id=device.hue_entertainment_group_id,
        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,
        default_css_processing_template_id=device.default_css_processing_template_id,
        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}")

        device_url = device_data.url.rstrip("/")

        # 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:
            raise HTTPException(
                status_code=422,
                detail=f"Failed to connect to {device_type} device at {device_url}: {e}"
            )

        # 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_data.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,
            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 "",
            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",
        )

        # 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.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
        providers = get_all_providers()
        discover_tasks = [p.discover(timeout=capped_timeout) for p in providers.values()]
        all_results = await asyncio.gather(*discover_tasks)
        discovered = [d for batch in all_results for d in batch]
    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,
                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 wled_controller.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="WLED Controller (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:
        device = store.update_device(
            device_id=device_id,
            name=update_data.name,
            url=update_data.url,
            enabled=update_data.enabled,
            led_count=update_data.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,
            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,
            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,
        )

        # Sync connection info in processor manager
        try:
            manager.update_device_info(
                device_id,
                device_url=update_data.url,
                led_count=update_data.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)
            pass

        # 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."
            )

        # 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,
    token: str = Query(""),
):
    """WebSocket stream of LED pixel data for WS device type.

    Wire format: [brightness_byte][R G B R G B ...]
    Auth via ?token=<api_key>.
    """
    from wled_controller.api.auth import verify_ws_token
    if not verify_ws_token(token):
        await websocket.close(code=4001, reason="Unauthorized")
        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

    await websocket.accept()

    from wled_controller.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)


