"""Output target routes: CRUD endpoints and batch state/metrics queries."""

import asyncio
from typing import Annotated, Optional

from fastapi import APIRouter, Body, HTTPException, Depends

from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import (
    fire_entity_event,
    get_device_store,
    get_mqtt_store,
    get_output_target_store,
    get_processor_manager,
    get_value_source_store,
)
from ledgrab.api.schemas.output_targets import (
    HALightMappingSchema,
    HALightOutputTargetCreate,
    HALightOutputTargetResponse,
    HALightOutputTargetUpdate,
    LedOutputTargetCreate,
    LedOutputTargetResponse,
    LedOutputTargetUpdate,
    OutputTargetCreate,
    OutputTargetListResponse,
    OutputTargetResponse,
    OutputTargetUpdate,
    Z2MLightMappingSchema,
    Z2MLightOutputTargetCreate,
    Z2MLightOutputTargetResponse,
    Z2MLightOutputTargetUpdate,
)
from ledgrab.core.processing.processor_manager import ProcessorManager
from ledgrab.storage import DeviceStore
from ledgrab.storage.bindable import BindableFloat
from ledgrab.storage.wled_output_target import WledOutputTarget
from ledgrab.storage.ha_light_output_target import (
    HALightMapping,
    HALightOutputTarget,
)
from ledgrab.storage.z2m_light_output_target import (
    Z2MLightMapping,
    Z2MLightOutputTarget,
)
from ledgrab.storage.mqtt_source_store import MQTTSourceStore
from ledgrab.storage.output_target_store import OutputTargetStore
from ledgrab.storage.value_source_store import ValueSourceStore
from ledgrab.utils import get_logger
from ledgrab.storage.base_store import EntityNotFoundError

logger = get_logger(__name__)

router = APIRouter()


def _led_target_to_response(target: WledOutputTarget) -> LedOutputTargetResponse:
    """Convert a WledOutputTarget to LedOutputTargetResponse."""
    return LedOutputTargetResponse(
        id=target.id,
        name=target.name,
        device_id=target.device_id,
        color_strip_source_id=target.color_strip_source_id,
        brightness=target.brightness.to_dict(),
        fps=target.fps.to_dict(),
        keepalive_interval=target.keepalive_interval,
        state_check_interval=target.state_check_interval,
        min_brightness_threshold=target.min_brightness_threshold.to_dict(),
        adaptive_fps=target.adaptive_fps,
        protocol=target.protocol,
        description=target.description,
        tags=target.tags,
        icon=getattr(target, "icon", "") or "",
        icon_color=getattr(target, "icon_color", "") or "",
        created_at=target.created_at,
        updated_at=target.updated_at,
    )


def _ha_light_target_to_response(
    target: HALightOutputTarget,
) -> HALightOutputTargetResponse:
    """Convert an HALightOutputTarget to HALightOutputTargetResponse."""
    return HALightOutputTargetResponse(
        id=target.id,
        name=target.name,
        ha_source_id=target.ha_source_id or "",
        source_kind=target.source_kind if target.source_kind in ("css", "color_vs") else "css",
        # Defensive coalesce — older records stored via resolve_ref may hold None.
        color_strip_source_id=target.color_strip_source_id or "",
        color_value_source_id=target.color_value_source_id or "",
        brightness=target.brightness.to_dict(),
        ha_light_mappings=[
            HALightMappingSchema(
                entity_id=m.entity_id,
                led_start=m.led_start,
                led_end=m.led_end,
                brightness_scale=m.brightness_scale.to_dict(),
            )
            for m in target.light_mappings
        ],
        update_rate=target.update_rate.to_dict(),
        transition=target.transition.to_dict(),
        color_tolerance=target.color_tolerance.to_dict(),
        min_brightness_threshold=target.min_brightness_threshold.to_dict(),
        stop_action=target.stop_action,
        description=target.description,
        tags=target.tags,
        icon=getattr(target, "icon", "") or "",
        icon_color=getattr(target, "icon_color", "") or "",
        created_at=target.created_at,
        updated_at=target.updated_at,
    )


def _z2m_light_target_to_response(
    target: Z2MLightOutputTarget,
) -> Z2MLightOutputTargetResponse:
    """Convert a Z2MLightOutputTarget to Z2MLightOutputTargetResponse."""
    return Z2MLightOutputTargetResponse(
        id=target.id,
        name=target.name,
        mqtt_source_id=target.mqtt_source_id or "",
        source_kind=target.source_kind if target.source_kind in ("css", "color_vs") else "css",
        color_strip_source_id=target.color_strip_source_id or "",
        color_value_source_id=target.color_value_source_id or "",
        brightness=target.brightness.to_dict(),
        z2m_light_mappings=[
            Z2MLightMappingSchema(
                friendly_name=m.friendly_name,
                led_start=m.led_start,
                led_end=m.led_end,
                brightness_scale=m.brightness_scale.to_dict(),
            )
            for m in target.light_mappings
        ],
        base_topic=target.base_topic,
        update_rate=target.update_rate.to_dict(),
        transition=target.transition.to_dict(),
        color_tolerance=target.color_tolerance.to_dict(),
        min_brightness_threshold=target.min_brightness_threshold.to_dict(),
        stop_action=target.stop_action if target.stop_action in ("none", "turn_off") else "none",
        description=target.description,
        tags=target.tags,
        icon=getattr(target, "icon", "") or "",
        icon_color=getattr(target, "icon_color", "") or "",
        created_at=target.created_at,
        updated_at=target.updated_at,
    )


def _validate_color_value_source(
    value_source_store: ValueSourceStore, color_value_source_id: str
) -> None:
    """Ensure the referenced ValueSource exists and returns colour."""
    if not color_value_source_id:
        raise HTTPException(
            status_code=400,
            detail="color_value_source_id is required when source_kind='color_vs'",
        )
    try:
        source = value_source_store.get_source(color_value_source_id)
    except (ValueError, EntityNotFoundError):
        raise HTTPException(
            status_code=422,
            detail=f"Color value source {color_value_source_id} not found",
        )
    if source.to_dict().get("return_type") != "color":
        raise HTTPException(
            status_code=400,
            detail=(
                f"Value source {color_value_source_id} does not return colour "
                "(return_type must be 'color')"
            ),
        )


def _target_to_response(target) -> OutputTargetResponse:
    """Convert any OutputTarget to the appropriate typed response."""
    if isinstance(target, WledOutputTarget):
        return _led_target_to_response(target)
    elif isinstance(target, HALightOutputTarget):
        return _ha_light_target_to_response(target)
    elif isinstance(target, Z2MLightOutputTarget):
        return _z2m_light_target_to_response(target)
    else:
        # Fallback for unknown types — use LED response with defaults
        return LedOutputTargetResponse(
            id=target.id,
            name=target.name,
            description=target.description,
            tags=target.tags,
            created_at=target.created_at,
            updated_at=target.updated_at,
        )


# ===== CRUD ENDPOINTS =====


def _build_ha_mappings(
    payload: list[HALightMappingSchema] | None,
) -> list[HALightMapping] | None:
    if not payload:
        return None
    return [
        HALightMapping(
            entity_id=m.entity_id,
            led_start=m.led_start,
            led_end=m.led_end,
            brightness_scale=BindableFloat.from_raw(m.brightness_scale, default=1.0),
        )
        for m in payload
    ]


def _build_z2m_mappings(
    payload: list[Z2MLightMappingSchema] | None,
) -> list[Z2MLightMapping] | None:
    if not payload:
        return None
    return [
        Z2MLightMapping(
            friendly_name=m.friendly_name,
            led_start=m.led_start,
            led_end=m.led_end,
            brightness_scale=BindableFloat.from_raw(m.brightness_scale, default=1.0),
        )
        for m in payload
    ]


def _validate_device_exists(device_store: DeviceStore, device_id: str) -> None:
    if not device_id:
        return
    try:
        device_store.get_device(device_id)
    except ValueError:
        raise HTTPException(status_code=422, detail=f"Device {device_id} not found")


def _validate_mqtt_source_exists(mqtt_store: MQTTSourceStore, mqtt_source_id: str) -> None:
    """Ensure the referenced MQTT source exists. Empty id is allowed (unconfigured)."""
    if not mqtt_source_id:
        return
    try:
        mqtt_store.get(mqtt_source_id)
    except (ValueError, EntityNotFoundError):
        raise HTTPException(status_code=422, detail=f"MQTT source {mqtt_source_id} not found")


@router.post(
    "/api/v1/output-targets", response_model=OutputTargetResponse, tags=["Targets"], status_code=201
)
async def create_target(
    data: Annotated[OutputTargetCreate, Body(discriminator="target_type")],
    _auth: AuthRequired,
    target_store: OutputTargetStore = Depends(get_output_target_store),
    device_store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
    value_source_store: ValueSourceStore = Depends(get_value_source_store),
    mqtt_store: MQTTSourceStore = Depends(get_mqtt_store),
):
    """Create a new output target."""
    try:
        match data:
            case LedOutputTargetCreate():
                _validate_device_exists(device_store, data.device_id)
                target = target_store.create_wled_target(
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    device_id=data.device_id,
                    color_strip_source_id=data.color_strip_source_id,
                    brightness=data.brightness,
                    fps=data.fps,
                    keepalive_interval=data.keepalive_interval,
                    state_check_interval=data.state_check_interval,
                    min_brightness_threshold=data.min_brightness_threshold,
                    adaptive_fps=data.adaptive_fps,
                    protocol=data.protocol,
                )
            case HALightOutputTargetCreate():
                if data.source_kind == "color_vs":
                    _validate_color_value_source(value_source_store, data.color_value_source_id)
                target = target_store.create_ha_light_target(
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    ha_source_id=data.ha_source_id,
                    source_kind=data.source_kind,
                    color_strip_source_id=data.color_strip_source_id,
                    color_value_source_id=data.color_value_source_id,
                    brightness=data.brightness,
                    ha_light_mappings=_build_ha_mappings(data.ha_light_mappings),
                    update_rate=data.update_rate,
                    transition=data.transition,
                    min_brightness_threshold=data.min_brightness_threshold,
                    color_tolerance=data.color_tolerance,
                    stop_action=data.stop_action,
                )
            case Z2MLightOutputTargetCreate():
                if data.source_kind == "color_vs":
                    _validate_color_value_source(value_source_store, data.color_value_source_id)
                _validate_mqtt_source_exists(mqtt_store, data.mqtt_source_id)
                target = target_store.create_z2m_light_target(
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    mqtt_source_id=data.mqtt_source_id,
                    source_kind=data.source_kind,
                    color_strip_source_id=data.color_strip_source_id,
                    color_value_source_id=data.color_value_source_id,
                    brightness=data.brightness,
                    z2m_light_mappings=_build_z2m_mappings(data.z2m_light_mappings),
                    base_topic=data.base_topic,
                    update_rate=data.update_rate,
                    transition=data.transition,
                    min_brightness_threshold=data.min_brightness_threshold,
                    color_tolerance=data.color_tolerance,
                    stop_action=data.stop_action,
                )
            case _:  # pragma: no cover — Pydantic discriminator already ensures one of the three
                raise HTTPException(status_code=400, detail="Unknown target_type")

        # Register in processor manager
        try:
            target.register_with_manager(manager)
        except ValueError as e:
            logger.warning(f"Could not register target {target.id} in processor manager: {e}")

        fire_entity_event("output_target", "created", target.id)
        return _target_to_response(target)

    except HTTPException:
        raise
    except EntityNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))

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


@router.get("/api/v1/output-targets", response_model=OutputTargetListResponse, tags=["Targets"])
async def list_targets(
    _auth: AuthRequired,
    target_store: OutputTargetStore = Depends(get_output_target_store),
):
    """List all output targets."""
    targets = target_store.get_all_targets()
    responses = [_target_to_response(t) for t in targets]
    return OutputTargetListResponse(targets=responses, count=len(responses))


@router.get("/api/v1/output-targets/batch/states", tags=["Processing"])
async def batch_target_states(
    _auth: AuthRequired,
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get processing state for all targets in a single request."""
    return {"states": manager.get_all_target_states()}


@router.get("/api/v1/output-targets/batch/metrics", tags=["Metrics"])
async def batch_target_metrics(
    _auth: AuthRequired,
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Get metrics for all targets in a single request."""
    return {"metrics": manager.get_all_target_metrics()}


@router.get(
    "/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
)
async def get_target(
    target_id: str,
    _auth: AuthRequired,
    target_store: OutputTargetStore = Depends(get_output_target_store),
):
    """Get a output target by ID."""
    try:
        target = target_store.get_target(target_id)
        return _target_to_response(target)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))


def _resolve_effective_color_vs_id(
    target_store: OutputTargetStore, target_id: str, payload_id: Optional[str]
) -> str:
    if payload_id is not None:
        return payload_id
    try:
        existing = target_store.get_target(target_id)
    except ValueError:
        return ""
    return getattr(existing, "color_value_source_id", "") or ""


@router.put(
    "/api/v1/output-targets/{target_id}", response_model=OutputTargetResponse, tags=["Targets"]
)
async def update_target(
    target_id: str,
    data: Annotated[OutputTargetUpdate, Body(discriminator="target_type")],
    _auth: AuthRequired,
    target_store: OutputTargetStore = Depends(get_output_target_store),
    device_store: DeviceStore = Depends(get_device_store),
    manager: ProcessorManager = Depends(get_processor_manager),
    value_source_store: ValueSourceStore = Depends(get_value_source_store),
    mqtt_store: MQTTSourceStore = Depends(get_mqtt_store),
):
    """Update a output target."""
    try:
        css_changed = False
        brightness_changed = False
        settings_changed = False
        device_changed = False

        match data:
            case LedOutputTargetUpdate():
                if data.device_id:
                    _validate_device_exists(device_store, data.device_id)
                target = target_store.update_wled_target(
                    target_id,
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    icon=data.icon,
                    icon_color=data.icon_color,
                    device_id=data.device_id,
                    color_strip_source_id=data.color_strip_source_id,
                    brightness=data.brightness,
                    fps=data.fps,
                    keepalive_interval=data.keepalive_interval,
                    state_check_interval=data.state_check_interval,
                    min_brightness_threshold=data.min_brightness_threshold,
                    adaptive_fps=data.adaptive_fps,
                    protocol=data.protocol,
                )
                css_changed = data.color_strip_source_id is not None
                brightness_changed = data.brightness is not None
                settings_changed = any(
                    v is not None
                    for v in (
                        data.fps,
                        data.keepalive_interval,
                        data.state_check_interval,
                        data.min_brightness_threshold,
                        data.adaptive_fps,
                        data.brightness,
                    )
                )
                device_changed = data.device_id is not None
            case HALightOutputTargetUpdate():
                # Validate color VS when switching into / staying in color_vs mode
                if data.source_kind == "color_vs" or (
                    data.source_kind is None and data.color_value_source_id
                ):
                    effective_id = _resolve_effective_color_vs_id(
                        target_store, target_id, data.color_value_source_id
                    )
                    _validate_color_value_source(value_source_store, effective_id)
                target = target_store.update_ha_light_target(
                    target_id,
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    icon=data.icon,
                    icon_color=data.icon_color,
                    ha_source_id=data.ha_source_id,
                    source_kind=data.source_kind,
                    color_strip_source_id=data.color_strip_source_id,
                    color_value_source_id=data.color_value_source_id,
                    brightness=data.brightness,
                    ha_light_mappings=_build_ha_mappings(data.ha_light_mappings),
                    update_rate=data.update_rate,
                    transition=data.transition,
                    min_brightness_threshold=data.min_brightness_threshold,
                    color_tolerance=data.color_tolerance,
                    stop_action=data.stop_action,
                )
                css_changed = data.color_strip_source_id is not None
                brightness_changed = data.brightness is not None
                settings_changed = any(
                    v is not None
                    for v in (
                        data.source_kind,
                        data.color_value_source_id,
                        data.brightness,
                        data.update_rate,
                        data.transition,
                        data.min_brightness_threshold,
                        data.color_tolerance,
                        data.ha_light_mappings,
                        data.stop_action,
                    )
                )
            case Z2MLightOutputTargetUpdate():
                if data.source_kind == "color_vs" or (
                    data.source_kind is None and data.color_value_source_id
                ):
                    effective_id = _resolve_effective_color_vs_id(
                        target_store, target_id, data.color_value_source_id
                    )
                    _validate_color_value_source(value_source_store, effective_id)
                if data.mqtt_source_id:
                    _validate_mqtt_source_exists(mqtt_store, data.mqtt_source_id)
                target = target_store.update_z2m_light_target(
                    target_id,
                    name=data.name,
                    description=data.description,
                    tags=data.tags,
                    icon=data.icon,
                    icon_color=data.icon_color,
                    mqtt_source_id=data.mqtt_source_id,
                    source_kind=data.source_kind,
                    color_strip_source_id=data.color_strip_source_id,
                    color_value_source_id=data.color_value_source_id,
                    brightness=data.brightness,
                    z2m_light_mappings=_build_z2m_mappings(data.z2m_light_mappings),
                    base_topic=data.base_topic,
                    update_rate=data.update_rate,
                    transition=data.transition,
                    min_brightness_threshold=data.min_brightness_threshold,
                    color_tolerance=data.color_tolerance,
                    stop_action=data.stop_action,
                )
                css_changed = data.color_strip_source_id is not None
                brightness_changed = data.brightness is not None
                settings_changed = any(
                    v is not None
                    for v in (
                        data.source_kind,
                        data.color_value_source_id,
                        data.mqtt_source_id,
                        data.brightness,
                        data.base_topic,
                        data.update_rate,
                        data.transition,
                        data.min_brightness_threshold,
                        data.color_tolerance,
                        data.z2m_light_mappings,
                        data.stop_action,
                    )
                )
            case _:  # pragma: no cover — Pydantic discriminator already ensures one of the three
                raise HTTPException(status_code=400, detail="Unknown target_type")

        # Sync processor manager (run in thread — css release/acquire can block)
        try:
            await asyncio.to_thread(
                target.sync_with_manager,
                manager,
                settings_changed=settings_changed,
                css_changed=css_changed,
                brightness_changed=brightness_changed,
            )
        except ValueError as e:
            logger.debug("Processor config update skipped for target %s: %s", target_id, e)
            pass

        # LED-only: device change requires async stop -> swap -> start cycle
        if device_changed and isinstance(target, WledOutputTarget):
            try:
                await manager.update_target_device(target_id, target.device_id)
            except ValueError as e:
                logger.debug("Device update skipped for target %s: %s", target_id, e)
                pass

        fire_entity_event("output_target", "updated", target_id)
        return _target_to_response(target)

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


@router.delete("/api/v1/output-targets/{target_id}", status_code=204, tags=["Targets"])
async def delete_target(
    target_id: str,
    _auth: AuthRequired,
    target_store: OutputTargetStore = Depends(get_output_target_store),
    manager: ProcessorManager = Depends(get_processor_manager),
):
    """Delete a output target. Stops processing first if active."""
    try:
        # Stop processing if running
        try:
            await manager.stop_processing(target_id)
        except ValueError as e:
            logger.debug("Stop processing skipped for target %s (not running): %s", target_id, e)
            pass

        # Remove from manager
        try:
            manager.remove_target(target_id)
        except (ValueError, RuntimeError) as e:
            logger.debug("Remove target from manager skipped for %s: %s", target_id, e)
            pass

        # Delete from store
        target_store.delete_target(target_id)

        fire_entity_event("output_target", "deleted", target_id)
        logger.info(f"Deleted target {target_id}")

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