"""Expose API routes (M5-T12).

Three endpoints:
  GET  /api/expose           — Return catalog of exposable entities, per-key
                               toggle state, and MQTT/Discovery connection status.
  PUT  /api/expose           — Set per-key toggle state (map key → bool);
                               triggers a HA Discovery re-publish on success.
  POST /api/expose/republish — Manually trigger a full HA Discovery re-publish.

Auth model:
  - GET: session required (no CSRF — read-only).
  - PUT, POST: session + CSRF required.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.api.routes.api.deps import require_csrf, require_session
from app.config import get_settings
from app.dependencies import get_db
from app.services.config_page import build_runtime_settings
from app.integrations.expose import CatalogEntry, build_catalog
from app.integrations.mqtt import mqtt_manager
from app.models.expose import ExposedEntityToggle
from app.schemas.expose import (
    CatalogEntrySchema,
    DeviceInfoSchema,
    ExposeResponse,
    ExposeUpdateRequest,
    ExposeUpdateResponse,
    ExposableEntitySchema,
    MqttStatusSchema,
    RepublishResponse,
)
from app.services.auth import AuthenticatedSession

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api", tags=["api-expose"])


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _get_mqtt_status(db: Session) -> MqttStatusSchema:
    """Build MQTT/Discovery status from live runtime state (DB-merged settings)."""
    settings = build_runtime_settings(db, get_settings())
    return MqttStatusSchema(
        mqtt_configured=mqtt_manager.is_configured(settings),
        mqtt_connected=mqtt_manager.is_connected,
        discovery_enabled=bool(settings.ha_discovery_enabled),
    )


def _entry_to_schema(entry: CatalogEntry) -> CatalogEntrySchema:
    """Convert a CatalogEntry to its Pydantic schema form.

    Excludes ``value_getter`` because it is a non-serialisable callable.
    """
    entity = entry.entity
    return CatalogEntrySchema(
        entity=ExposableEntitySchema(
            key=entity.key,
            component=entity.component,
            device=DeviceInfoSchema(
                identifiers=list(entity.device.identifiers),
                name=entity.device.name,
            ),
            device_class=entity.device_class,
            unit=entity.unit,
            name=entity.name,
            state_class=entity.state_class,
        ),
        enabled=entry.enabled,
    )


def _build_response_data(
    session: Session,
) -> tuple[list[CatalogEntrySchema], MqttStatusSchema]:
    """Return (catalog_entries, mqtt_status) for building GET / PUT responses."""
    catalog = build_catalog(session)
    catalog_schema = [_entry_to_schema(e) for e in catalog]
    mqtt_status = _get_mqtt_status(session)
    return catalog_schema, mqtt_status


# ---------------------------------------------------------------------------
# GET /api/expose
# ---------------------------------------------------------------------------


@router.get("/expose", response_model=ExposeResponse)
def get_expose(
    db: Session = Depends(get_db),
    _auth: AuthenticatedSession = Depends(require_session),
) -> ExposeResponse:
    """Return the full exposable-entity catalog with toggle states and MQTT status.

    The catalog is computed dynamically from registered providers (e.g. the
    Modbus provider enumerates all enabled devices and their metric entities).
    Toggle states come from the ``exposed_entity_toggle`` table; entities with
    no row default to ``enabled=False``.
    """
    catalog_schema, mqtt_status = _build_response_data(db)
    return ExposeResponse(catalog=catalog_schema, mqtt_status=mqtt_status)


# ---------------------------------------------------------------------------
# PUT /api/expose
# ---------------------------------------------------------------------------


@router.put("/expose", response_model=ExposeUpdateResponse)
def put_expose(
    body: ExposeUpdateRequest,
    db: Session = Depends(get_db),
    _auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> ExposeUpdateResponse:
    """Set per-entity toggle state.

    Accepts a map of ``{key: bool}`` and upserts rows in the
    ``exposed_entity_toggle`` table.  Only keys present in ``body.toggles``
    are touched; other entities' toggles are left unchanged.

    After writing the toggles, triggers a HA Discovery re-publish so any
    changes (enabled ↔ disabled) are reflected in Home Assistant immediately.
    """
    now = datetime.now(UTC)

    for key, enabled in body.toggles.items():
        existing = (
            db.query(ExposedEntityToggle)
            .filter(ExposedEntityToggle.key == key)
            .first()
        )
        if existing is not None:
            existing.enabled = enabled
            existing.updated_at = now
        else:
            db.add(
                ExposedEntityToggle(
                    key=key,
                    enabled=enabled,
                    updated_at=now,
                )
            )

    db.commit()

    # Trigger discovery re-publish after toggle change.
    _trigger_republish(db)

    catalog_schema, mqtt_status = _build_response_data(db)
    return ExposeUpdateResponse(catalog=catalog_schema, mqtt_status=mqtt_status)


# ---------------------------------------------------------------------------
# POST /api/expose/republish
# ---------------------------------------------------------------------------


@router.post("/expose/republish", response_model=RepublishResponse)
def post_expose_republish(
    db: Session = Depends(get_db),
    _auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> RepublishResponse:
    """Manually trigger a full HA Discovery re-publish.

    Calls ``publish_discovery(session)`` from the HA discovery service (M5-T11).
    Returns a status indicating whether the publish was attempted (or skipped
    because MQTT / discovery is not enabled / connected).
    """
    settings = build_runtime_settings(db, get_settings())
    if not (settings.mqtt_enabled and settings.ha_discovery_enabled and mqtt_manager.is_connected):
        return RepublishResponse(
            ok=False,
            message="MQTT / HA Discovery not enabled or broker not connected.",
        )

    try:
        _trigger_republish(db)
        return RepublishResponse(
            ok=True,
            message="HA Discovery re-published successfully.",
        )
    except Exception as exc:
        logger.exception("post_expose_republish: unexpected error during publish")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Discovery re-publish failed.",
        ) from exc


# ---------------------------------------------------------------------------
# Internal: trigger discovery re-publish
# ---------------------------------------------------------------------------


def _trigger_republish(session: Session) -> None:
    """Call publish_discovery from the HA discovery service.

    No-op if MQTT / discovery is not enabled or broker is not connected
    (publish_discovery guards internally).  All errors are swallowed to avoid
    breaking the API response.
    """
    try:
        from app.services.ha_discovery import publish_discovery

        publish_discovery(session)
    except Exception:
        logger.exception("_trigger_republish: publish_discovery raised an error")
