"""Value source routes: CRUD for value sources."""

import asyncio
from typing import Annotated, Optional

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

from wled_controller.api.auth import AuthRequired
from wled_controller.api.dependencies import (
    fire_entity_event,
    get_output_target_store,
    get_processor_manager,
    get_value_source_store,
)
from wled_controller.api.schemas.value_sources import (
    AdaptiveSceneValueSourceResponse,
    AdaptiveTimeColorValueSourceResponse,
    AdaptiveTimeValueSourceResponse,
    AnimatedColorValueSourceResponse,
    AnimatedValueSourceResponse,
    AudioValueSourceResponse,
    CSSExtractValueSourceResponse,
    DaylightValueSourceResponse,
    GradientMapValueSourceResponse,
    HAEntityValueSourceResponse,
    StaticColorValueSourceResponse,
    StaticValueSourceResponse,
    SystemMetricsValueSourceResponse,
    ValueSourceCreate,
    ValueSourceListResponse,
    ValueSourceResponse,
    ValueSourceUpdate,
)
from wled_controller.storage.value_source import (
    AdaptiveTimeColorValueSource,
    AdaptiveValueSource,
    AnimatedColorValueSource,
    AnimatedValueSource,
    AudioValueSource,
    CSSExtractValueSource,
    DaylightValueSource,
    GradientMapValueSource,
    HAEntityValueSource,
    StaticColorValueSource,
    StaticValueSource,
    SystemMetricsValueSource,
    ValueSource,
)
from wled_controller.storage.value_source_store import ValueSourceStore
from wled_controller.storage.output_target_store import OutputTargetStore
from wled_controller.core.processing.processor_manager import ProcessorManager
from wled_controller.core.processing.value_stream import ValueStream
from wled_controller.utils import get_logger
from wled_controller.storage.base_store import EntityNotFoundError

logger = get_logger(__name__)

router = APIRouter()

# Maps storage class to the response builder for that type.
_RESPONSE_MAP = {
    StaticValueSource: lambda s: StaticValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        value=s.value,
    ),
    AnimatedValueSource: lambda s: AnimatedValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        waveform=s.waveform,
        speed=s.speed,
        min_value=s.min_value,
        max_value=s.max_value,
    ),
    AudioValueSource: lambda s: AudioValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        audio_source_id=s.audio_source_id,
        mode=s.mode,
        sensitivity=s.sensitivity,
        smoothing=s.smoothing,
        min_value=s.min_value,
        max_value=s.max_value,
        auto_gain=s.auto_gain,
    ),
    DaylightValueSource: lambda s: DaylightValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        speed=s.speed,
        use_real_time=s.use_real_time,
        latitude=s.latitude,
        min_value=s.min_value,
        max_value=s.max_value,
    ),
    StaticColorValueSource: lambda s: StaticColorValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        color=list(s.color),
    ),
    AnimatedColorValueSource: lambda s: AnimatedColorValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        colors=[list(c) for c in s.colors],
        speed=s.speed,
        easing=s.easing,
    ),
    AdaptiveTimeColorValueSource: lambda s: AdaptiveTimeColorValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        schedule=s.schedule,
    ),
    HAEntityValueSource: lambda s: HAEntityValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        ha_source_id=s.ha_source_id,
        entity_id=s.entity_id,
        attribute=s.attribute,
        min_ha_value=s.min_ha_value,
        max_ha_value=s.max_ha_value,
        smoothing=s.smoothing,
    ),
    GradientMapValueSource: lambda s: GradientMapValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        value_source_id=s.value_source_id,
        gradient_id=s.gradient_id,
        easing=s.easing,
    ),
    CSSExtractValueSource: lambda s: CSSExtractValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        color_strip_source_id=s.color_strip_source_id,
        led_start=s.led_start,
        led_end=s.led_end,
    ),
    SystemMetricsValueSource: lambda s: SystemMetricsValueSourceResponse(
        id=s.id,
        name=s.name,
        description=s.description,
        tags=s.tags,
        created_at=s.created_at,
        updated_at=s.updated_at,
        metric=s.metric,
        min_value=s.min_value,
        max_value=s.max_value,
        max_rate=s.max_rate,
        disk_path=s.disk_path,
        sensor_label=s.sensor_label,
        poll_interval=s.poll_interval,
        smoothing=s.smoothing,
    ),
}


def _to_response(source: ValueSource) -> ValueSourceResponse:
    """Convert a ValueSource dataclass to the matching response schema."""
    # AdaptiveValueSource covers both adaptive_time and adaptive_scene
    if isinstance(source, AdaptiveValueSource):
        if source.source_type == "adaptive_scene":
            return AdaptiveSceneValueSourceResponse(
                id=source.id,
                name=source.name,
                description=source.description,
                tags=source.tags,
                created_at=source.created_at,
                updated_at=source.updated_at,
                picture_source_id=source.picture_source_id,
                scene_behavior=source.scene_behavior,
                sensitivity=source.sensitivity,
                smoothing=source.smoothing,
                min_value=source.min_value,
                max_value=source.max_value,
            )
        return AdaptiveTimeValueSourceResponse(
            id=source.id,
            name=source.name,
            description=source.description,
            tags=source.tags,
            created_at=source.created_at,
            updated_at=source.updated_at,
            schedule=source.schedule,
            min_value=source.min_value,
            max_value=source.max_value,
        )

    builder = _RESPONSE_MAP.get(type(source))
    if builder is None:
        # Fallback for unknown types — return as static
        return StaticValueSourceResponse(
            id=source.id,
            name=source.name,
            description=source.description,
            tags=source.tags,
            created_at=source.created_at,
            updated_at=source.updated_at,
            value=getattr(source, "value", 1.0),
        )
    return builder(source)


@router.get("/api/v1/value-sources", response_model=ValueSourceListResponse, tags=["Value Sources"])
async def list_value_sources(
    _auth: AuthRequired,
    source_type: Optional[str] = Query(
        None,
        description="Filter by source_type: static, animated, audio, adaptive_time, or adaptive_scene",
    ),
    store: ValueSourceStore = Depends(get_value_source_store),
):
    """List all value sources, optionally filtered by type."""
    sources = store.get_all_sources()
    if source_type:
        sources = [s for s in sources if s.source_type == source_type]
    return ValueSourceListResponse(
        sources=[_to_response(s) for s in sources],
        count=len(sources),
    )


@router.post(
    "/api/v1/value-sources",
    response_model=ValueSourceResponse,
    status_code=201,
    tags=["Value Sources"],
)
async def create_value_source(
    data: Annotated[ValueSourceCreate, Body(discriminator="source_type")],
    _auth: AuthRequired,
    store: ValueSourceStore = Depends(get_value_source_store),
):
    """Create a new value source."""
    try:
        # Extract all fields from the discriminated union body
        fields = data.model_dump(exclude={"source_type", "name", "description", "tags"})
        source = store.create_source(
            name=data.name,
            source_type=data.source_type,
            description=data.description,
            tags=data.tags,
            **fields,
        )
        fire_entity_event("value_source", "created", source.id)
        return _to_response(source)
    except EntityNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))

    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.get(
    "/api/v1/value-sources/{source_id}", response_model=ValueSourceResponse, tags=["Value Sources"]
)
async def get_value_source(
    source_id: str,
    _auth: AuthRequired,
    store: ValueSourceStore = Depends(get_value_source_store),
):
    """Get a value source by ID."""
    try:
        source = store.get_source(source_id)
        return _to_response(source)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))


@router.put(
    "/api/v1/value-sources/{source_id}", response_model=ValueSourceResponse, tags=["Value Sources"]
)
async def update_value_source(
    source_id: str,
    data: Annotated[ValueSourceUpdate, Body(discriminator="source_type")],
    _auth: AuthRequired,
    store: ValueSourceStore = Depends(get_value_source_store),
    pm: ProcessorManager = Depends(get_processor_manager),
):
    """Update an existing value source."""
    try:
        # Extract all fields, excluding None values and the discriminator
        fields = data.model_dump(exclude={"source_type"}, exclude_none=True)
        source = store.update_source(source_id=source_id, **fields)
        # Hot-reload running value streams
        pm.update_value_source(source_id)
        fire_entity_event("value_source", "updated", source_id)
        return _to_response(source)
    except EntityNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))

    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))


@router.delete("/api/v1/value-sources/{source_id}", status_code=204, tags=["Value Sources"])
async def delete_value_source(
    source_id: str,
    _auth: AuthRequired,
    store: ValueSourceStore = Depends(get_value_source_store),
    target_store: OutputTargetStore = Depends(get_output_target_store),
):
    """Delete a value source."""
    try:
        # Check if any targets reference this value source
        from wled_controller.storage.wled_output_target import WledOutputTarget

        for target in target_store.get_all_targets():
            if isinstance(target, WledOutputTarget):
                if getattr(target, "brightness_value_source_id", "") == source_id:
                    raise ValueError(f"Cannot delete: referenced by target '{target.name}'")

        store.delete_source(source_id)
        fire_entity_event("value_source", "deleted", source_id)
    except EntityNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))

    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))


# ===== REAL-TIME VALUE SOURCE TEST WEBSOCKET =====


@router.websocket("/api/v1/value-sources/{source_id}/test/ws")
async def test_value_source_ws(
    websocket: WebSocket,
    source_id: str,
    token: str = Query(""),
):
    """WebSocket for real-time value source output. Auth via ?token=<api_key>.

    Acquires a ValueStream for the given source, polls get_value() at ~20 Hz,
    and streams {value: float} JSON to the client.
    """
    from wled_controller.api.auth import verify_ws_token

    if not verify_ws_token(token):
        await websocket.close(code=4001, reason="Unauthorized")
        return

    # Validate source exists
    store = get_value_source_store()
    try:
        store.get_source(source_id)
    except ValueError as e:
        await websocket.close(code=4004, reason=str(e))
        return

    # Acquire a value stream
    manager = get_processor_manager()
    vsm = manager.value_stream_manager
    if vsm is None:
        await websocket.close(code=4003, reason="Value stream manager not available")
        return

    try:
        stream = vsm.acquire(source_id)
    except Exception as e:
        await websocket.close(code=4003, reason=str(e))
        return

    await websocket.accept()
    logger.info(f"Value source test WebSocket connected for {source_id}")

    # Detect if this stream produces colors
    _is_color_stream = (
        hasattr(stream, "get_color") and type(stream).get_color is not ValueStream.get_color
    )

    try:
        while True:
            value = stream.get_value()
            msg: dict = {"value": round(value, 4)}
            if _is_color_stream:
                try:
                    r, g, b = stream.get_color()
                    msg["color"] = [int(r), int(g), int(b)]
                except NotImplementedError:
                    pass
            if hasattr(stream, "get_input_value"):
                msg["input_value"] = round(stream.get_input_value(), 4)
            if hasattr(stream, "get_raw_value"):
                raw = stream.get_raw_value()
                if raw is not None:
                    msg["raw_value"] = round(raw, 4)
                    if hasattr(stream, "_min_ha"):
                        msg["raw_range"] = [stream._min_ha, stream._max_ha]
            await websocket.send_json(msg)
            await asyncio.sleep(0.05)
    except WebSocketDisconnect:
        logger.debug("Value source test WebSocket disconnected for %s", source_id)
        pass
    except Exception as e:
        logger.error(f"Value source test WebSocket error for {source_id}: {e}")
    finally:
        vsm.release(source_id)
        logger.info(f"Value source test WebSocket disconnected for {source_id}")
