"""User preferences routes — dashboard layout + notification settings + daylight tz.

The dashboard layout schema is owned by the frontend (open registry of
section/cell keys); the backend treats the value as an opaque JSON blob,
validates it's a dict with a `version` field, and persists it under the
`dashboard_layout` settings key.

Notification preferences are validated server-side via Pydantic so the
backend can read them when deciding whether to start the background
discovery watcher.

Daylight timezone is a single global IANA tz name shared by every
daylight value-source / color-strip-source. Stored as
``{"value": "Europe/Berlin"}`` under the ``daylight_timezone`` key, with
empty/missing meaning "use system local time".
"""

from typing import Any

from fastapi import APIRouter, Body, Depends, HTTPException
from pydantic import BaseModel, Field

from ledgrab.api.auth import AuthRequired
from ledgrab.api.dependencies import get_database
from ledgrab.api.schemas.preferences import NotificationPreferences
from ledgrab.core.processing.daylight_settings import (
    DAYLIGHT_TIMEZONE_KEY,
    get_daylight_timezone,
    set_daylight_timezone,
)
from ledgrab.storage.database import Database
from ledgrab.utils import get_logger

logger = get_logger(__name__)

router = APIRouter()

_DASHBOARD_LAYOUT_KEY = "dashboard_layout"
_NOTIFICATION_PREFS_KEY = "notification_preferences"
_CARD_MODES_KEY = "card_modes"


class DaylightTimezonePreference(BaseModel):
    """Global IANA timezone applied to every daylight cycle source."""

    timezone: str = Field("", description="IANA timezone name; empty = system local")


def load_notification_preferences(db: Database | None = None) -> NotificationPreferences:
    """Read notification prefs, returning defaults when unset or corrupt.

    Used by both the route handler and `main.lifespan` (so the discovery
    watcher can decide whether to start without going through HTTP).
    """
    if db is None:
        from ledgrab.api.dependencies import get_database as _get_db

        db = _get_db()
    raw = db.get_setting(_NOTIFICATION_PREFS_KEY)
    if not raw:
        return NotificationPreferences()
    try:
        return NotificationPreferences.model_validate(raw)
    except Exception as e:
        logger.warning("Stored notification preferences invalid (%s); using defaults", e)
        return NotificationPreferences()


@router.get(
    "/api/v1/preferences/dashboard-layout",
    tags=["Preferences"],
)
async def get_dashboard_layout(
    _: AuthRequired,
    db: Database = Depends(get_database),
) -> dict[str, Any]:
    """Read the saved dashboard layout. Returns an empty object when no
    layout has been saved yet — the frontend falls back to its built-in
    default in that case."""
    value = db.get_setting(_DASHBOARD_LAYOUT_KEY)
    return value if value is not None else {}


@router.put(
    "/api/v1/preferences/dashboard-layout",
    tags=["Preferences"],
)
async def put_dashboard_layout(
    _: AuthRequired,
    body: dict[str, Any] = Body(...),
    db: Database = Depends(get_database),
) -> dict[str, bool]:
    """Save the dashboard layout. The body must be a JSON object with a
    numeric `version` field; everything else is treated as opaque payload
    that the frontend will validate on read."""
    if not isinstance(body, dict):
        raise HTTPException(status_code=422, detail="Body must be a JSON object")
    if not isinstance(body.get("version"), int):
        raise HTTPException(
            status_code=422,
            detail="Layout must include a numeric 'version' field",
        )
    db.set_setting(_DASHBOARD_LAYOUT_KEY, body)
    return {"ok": True}


@router.delete(
    "/api/v1/preferences/dashboard-layout",
    tags=["Preferences"],
)
async def delete_dashboard_layout(
    _: AuthRequired,
    db: Database = Depends(get_database),
) -> dict[str, bool]:
    """Delete the saved layout — frontend will revert to the default
    on next load. Used by the 'Reset' button when the user wants
    to clear the server-side override entirely."""
    db.set_setting(_DASHBOARD_LAYOUT_KEY, {})
    return {"ok": True}


# ---------------------------------------------------------------------------
# Notification preferences
# ---------------------------------------------------------------------------


@router.get(
    "/api/v1/preferences/notifications",
    response_model=NotificationPreferences,
    tags=["Preferences"],
)
async def get_notification_preferences(
    _: AuthRequired,
    db: Database = Depends(get_database),
) -> NotificationPreferences:
    """Read notification prefs, returning defaults when unset.

    Defaults: device_offline=both, device_online/discovered=snack,
    device_lost=none, background discovery on, 10 s startup grace,
    5 s flap debounce.
    """
    return load_notification_preferences(db)


@router.put(
    "/api/v1/preferences/notifications",
    response_model=NotificationPreferences,
    tags=["Preferences"],
)
async def put_notification_preferences(
    _: AuthRequired,
    body: NotificationPreferences,
    db: Database = Depends(get_database),
) -> NotificationPreferences:
    """Persist the notification prefs. Pydantic enforces channel
    enum + grace/debounce ranges so a bad client cannot poison
    the stored value."""
    db.set_setting(_NOTIFICATION_PREFS_KEY, body.model_dump())
    logger.info(
        "Notification preferences updated (background_discovery=%s, " "channels=%s)",
        body.background_discovery_enabled,
        body.channels.model_dump(),
    )
    return body


# ---------------------------------------------------------------------------
# Card presentation modes (per-surface comfortable/compact/dense)
# ---------------------------------------------------------------------------


_VALID_CARD_MODES = {"comfortable", "compact", "dense", "row"}


@router.get(
    "/api/v1/preferences/card-modes",
    tags=["Preferences"],
)
async def get_card_modes(
    _: AuthRequired,
    db: Database = Depends(get_database),
) -> dict[str, Any]:
    """Read the saved card-mode preferences. Returns an empty object when
    nothing has been saved yet — the frontend falls back to the default
    mode ("compact") for every surface in that case."""
    value = db.get_setting(_CARD_MODES_KEY)
    return value if value is not None else {}


@router.put(
    "/api/v1/preferences/card-modes",
    tags=["Preferences"],
)
async def put_card_modes(
    _: AuthRequired,
    body: dict[str, Any] = Body(...),
    db: Database = Depends(get_database),
) -> dict[str, bool]:
    """Save card-mode preferences. The body must be a JSON object shaped
    like ``{"version": 1, "surfaces": {"<surface>": "<mode>", …}}``.

    The surface registry is intentionally open (any string accepted) so
    new card surfaces can adopt the toggle without a server migration.
    Invalid mode values are rejected to prevent a bad client from
    poisoning the stored value."""
    if not isinstance(body, dict):
        raise HTTPException(status_code=422, detail="Body must be a JSON object")
    if not isinstance(body.get("version"), int):
        raise HTTPException(
            status_code=422,
            detail="Body must include a numeric 'version' field",
        )
    surfaces = body.get("surfaces", {})
    if not isinstance(surfaces, dict):
        raise HTTPException(
            status_code=422,
            detail="'surfaces' must be an object mapping surface keys to modes",
        )
    for key, mode in surfaces.items():
        if not isinstance(key, str) or not key:
            raise HTTPException(
                status_code=422,
                detail=f"Surface keys must be non-empty strings (got {key!r})",
            )
        if mode not in _VALID_CARD_MODES:
            raise HTTPException(
                status_code=422,
                detail=(
                    f"Surface {key!r} has invalid mode {mode!r}; "
                    f"expected one of {sorted(_VALID_CARD_MODES)}"
                ),
            )
    db.set_setting(_CARD_MODES_KEY, body)
    return {"ok": True}


@router.delete(
    "/api/v1/preferences/card-modes",
    tags=["Preferences"],
)
async def delete_card_modes(
    _: AuthRequired,
    db: Database = Depends(get_database),
) -> dict[str, bool]:
    """Delete saved card-mode preferences — every surface reverts to the
    frontend default on next load."""
    db.set_setting(_CARD_MODES_KEY, {})
    return {"ok": True}


# ---------------------------------------------------------------------------
# Daylight timezone (global)
# ---------------------------------------------------------------------------


@router.get(
    "/api/v1/preferences/daylight-timezone",
    response_model=DaylightTimezonePreference,
    tags=["Preferences"],
)
async def get_daylight_timezone_preference(
    _: AuthRequired,
) -> DaylightTimezonePreference:
    """Return the global daylight cycle timezone (empty = system local)."""
    return DaylightTimezonePreference(timezone=get_daylight_timezone())


@router.put(
    "/api/v1/preferences/daylight-timezone",
    response_model=DaylightTimezonePreference,
    tags=["Preferences"],
)
async def put_daylight_timezone_preference(
    _: AuthRequired,
    body: DaylightTimezonePreference,
) -> DaylightTimezonePreference:
    """Persist the global daylight cycle timezone.

    The string is stored verbatim — clients should send a valid IANA name
    (e.g. ``Europe/Berlin``) or an empty string for "use server local".
    Daylight streams pick up the new value within ~1 second.
    """
    saved = set_daylight_timezone(body.timezone)
    logger.info("Daylight timezone updated: %r", saved or "<system local>")
    return DaylightTimezonePreference(timezone=saved)


__all__ = ["router", "DAYLIGHT_TIMEZONE_KEY"]
