from __future__ import annotations

import logging

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

from app.api.routes.api.deps import require_csrf, require_session
from app.config import Settings, get_settings
from app.dependencies import get_app_settings, get_db
from app.integrations.mqtt import MQTT_SETTINGS_KEYS, mqtt_manager
from app.schemas.config import (
    ConfigField,
    ConfigResponse,
    ConfigSection,
    ConfigUpdateRequest,
    ConfigUpdateResponse,
    MqttTestResponse,
    SmtpTestResponse,
)
from app.services.auth import AuthenticatedSession
from app.services.config_page import ConfigSaveError, build_config_sections, save_config_updates
from app.services.email import EmailConfigurationError, EmailDeliveryError, send_smtp_test_email

logger = logging.getLogger(__name__)

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


def _sections_from_raw(sections_raw: list[dict]) -> list[ConfigSection]:
    result = []
    for section in sections_raw:
        fields = [ConfigField(**f) for f in section["fields"]]
        result.append(ConfigSection(name=section["name"], fields=fields))
    return result


@router.get("/config", response_model=ConfigResponse)
def get_config(
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_app_settings),
    _auth: AuthenticatedSession = Depends(require_session),
) -> ConfigResponse:
    """Return all configuration sections. Secret field values are masked (empty string)."""
    sections_raw = build_config_sections(db, settings)
    return ConfigResponse(sections=_sections_from_raw(sections_raw))


@router.put("/config", response_model=ConfigUpdateResponse)
def put_config(
    body: ConfigUpdateRequest,
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_app_settings),
    _auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> ConfigUpdateResponse:
    """
    Save configuration updates.

    - Blank secret value keeps the existing stored value (no change).
    - Invalid values return 422 and nothing is written to the database.
    - If MQTT-related settings changed, the MQTT client reconnects automatically.
    """
    # Detect whether any MQTT-related key is being submitted (non-secret change
    # or non-blank secret change) so we know to reconnect after saving.
    mqtt_keys_submitted = any(k.lower() in MQTT_SETTINGS_KEYS for k in body.updates)

    try:
        save_config_updates(db, body.updates, settings)
    except ConfigSaveError as exc:
        logger.warning("Rejected config update via API: %s", exc)
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="invalid config submission",
        ) from exc

    # Re-read settings after save (save_config_updates clears the settings cache).
    # Use build_runtime_settings so the reconnect picks up DB-stored values, not
    # just the bootstrap env (otherwise a broker configured via the UI is ignored).
    from app.services.config_page import build_runtime_settings
    refreshed_settings = build_runtime_settings(db, get_settings())

    # Reconnect MQTT client if any MQTT setting was updated.
    if mqtt_keys_submitted:
        logger.info("MQTT settings changed — triggering reconnect.")
        mqtt_manager.reconnect(refreshed_settings)

    # Re-apply the DSMR subscription so enabling/disabling DSMR ingest (or changing
    # its topic / sample interval) takes effect immediately, without an app restart.
    # Done after any MQTT reconnect so it operates on the current client.
    from app.services.dsmr_ingest import apply_dsmr_subscription
    apply_dsmr_subscription(refreshed_settings)

    sections_raw = build_config_sections(db, refreshed_settings)
    return ConfigUpdateResponse(sections=_sections_from_raw(sections_raw))


@router.post(
    "/config/smtp/test",
    responses={
        200: {"model": SmtpTestResponse},
        400: {"model": SmtpTestResponse},
        502: {"model": SmtpTestResponse},
    },
)
def post_smtp_test(
    settings: Settings = Depends(get_app_settings),
    _auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> JSONResponse:
    """
    Send a test SMTP email using the current runtime settings.

    Returns a structured result indicating success or the category of failure.
    Three possible outcomes:
    - 200 { "result": "success", "message": ... }
    - 400 { "result": "config-error", "message": ... }   (EmailConfigurationError)
    - 502 { "result": "failed", "message": ... }          (EmailDeliveryError)

    SMTP credentials are never echoed in the response.
    """
    try:
        send_smtp_test_email(settings)
    except EmailConfigurationError as exc:
        logger.warning("SMTP test rejected due to configuration: %s", exc)
        return JSONResponse(
            status_code=status.HTTP_400_BAD_REQUEST,
            content={"result": "config-error", "message": str(exc)},
        )
    except EmailDeliveryError as exc:
        logger.warning("SMTP test delivery failed: %s", exc)
        return JSONResponse(
            status_code=status.HTTP_502_BAD_GATEWAY,
            content={"result": "failed", "message": str(exc)},
        )

    return JSONResponse(
        status_code=status.HTTP_200_OK,
        content={"result": "success", "message": "Test email sent successfully."},
    )


# ---------------------------------------------------------------------------
# POST /api/config/mqtt/test — M5-T10
# ---------------------------------------------------------------------------

class _MqttConfigurationError(ValueError):
    """Raised when MQTT settings are incomplete or disabled."""


class _MqttConnectionError(RuntimeError):
    """Raised when MQTT broker connection or publish fails."""


@router.post(
    "/config/mqtt/test",
    responses={
        200: {"model": MqttTestResponse},
        400: {"model": MqttTestResponse},
        502: {"model": MqttTestResponse},
    },
)
def post_mqtt_test(
    settings: Settings = Depends(get_app_settings),
    _auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> JSONResponse:
    """
    Test MQTT broker connectivity by attempting to connect and publishing a
    test message to ``<ha_discovery_prefix>/home-automation/test``.

    The message is visible in MQTT Explorer (or any subscriber) so users can
    confirm the full broker publish path is working.

    Three possible outcomes:
    - 200 { "result": "success", "message": ... }
    - 400 { "result": "config-error", "message": ... }   (not configured)
    - 502 { "result": "failed", "message": ... }          (connection/publish error)

    MQTT credentials are never echoed in the response.
    """
    try:
        _run_mqtt_test(settings)
    except _MqttConfigurationError as exc:
        logger.warning("MQTT test rejected due to configuration: %s", exc)
        return JSONResponse(
            status_code=status.HTTP_400_BAD_REQUEST,
            content={"result": "config-error", "message": str(exc)},
        )
    except _MqttConnectionError as exc:
        logger.warning("MQTT test connection/publish failed: %s", exc)
        return JSONResponse(
            status_code=status.HTTP_502_BAD_GATEWAY,
            content={"result": "failed", "message": str(exc)},
        )

    return JSONResponse(
        status_code=status.HTTP_200_OK,
        content={
            "result": "success",
            "message": (
                f"Test message published to "
                f"{settings.ha_discovery_prefix}/home-automation/test."
            ),
        },
    )


def _run_mqtt_test(settings: Settings) -> None:
    """Attempt a transient MQTT connection and publish a test message.

    Raises
    ------
    _MqttConfigurationError
        When MQTT is not enabled or broker host is not configured.
    _MqttConnectionError
        When the broker is unreachable or the publish fails.
    """
    import socket

    import paho.mqtt.client as mqtt

    if not settings.mqtt_broker_host:
        raise _MqttConfigurationError("MQTT broker host is not configured.")

    test_topic = f"{settings.ha_discovery_prefix}/home-automation/test"
    test_payload = '{"source": "home-automation", "event": "mqtt_test"}'

    connected_event = __import__("threading").Event()
    published_event = __import__("threading").Event()
    connect_error: list[str] = []

    client = mqtt.Client(
        callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
        client_id="home-automation-test",
    )

    def _on_connect(
        _client: mqtt.Client,
        _userdata: object,
        _flags: mqtt.ConnectFlags,
        reason_code: mqtt.ReasonCode,
        _properties: mqtt.Properties | None,
    ) -> None:
        if reason_code.is_failure:
            connect_error.append(f"Broker refused connection: {reason_code}")
        connected_event.set()

    def _on_publish(
        _client: mqtt.Client,
        _userdata: object,
        _mid: int,
        _reason_code: mqtt.ReasonCode,
        _properties: mqtt.Properties | None,
    ) -> None:
        published_event.set()

    client.on_connect = _on_connect
    client.on_publish = _on_publish

    if settings.mqtt_tls_enabled:
        try:
            client.tls_set()
        except Exception as exc:
            raise _MqttConnectionError(f"TLS setup failed: {exc}") from exc

    if settings.mqtt_username:
        client.username_pw_set(
            username=settings.mqtt_username,
            password=settings.mqtt_password or None,
        )

    client.loop_start()
    try:
        try:
            client.connect(
                host=settings.mqtt_broker_host,
                port=settings.mqtt_broker_port,
                keepalive=10,
            )
        except (OSError, socket.error) as exc:
            # Sanitise: ensure password never leaks into the error message.
            msg = _sanitize_mqtt_error(str(exc), settings.mqtt_password)
            raise _MqttConnectionError(f"Cannot reach broker: {msg}") from exc

        # Wait up to 5 s for connection acknowledgement.
        if not connected_event.wait(timeout=5):
            raise _MqttConnectionError("Broker connection timed out (5 s).")

        if connect_error:
            raise _MqttConnectionError(connect_error[0])

        # Publish test message.
        client.publish(test_topic, payload=test_payload, qos=1, retain=False)
        # Wait up to 5 s for the publish ACK (QoS 1).
        if not published_event.wait(timeout=5):
            raise _MqttConnectionError("Publish timed out (5 s) — broker reachable but no ACK.")

    finally:
        try:
            client.disconnect()
        except Exception:
            pass
        client.loop_stop()


def _sanitize_mqtt_error(message: str, password: str | None) -> str:
    """Replace *password* in *message* with ``[redacted]``."""
    if password:
        return message.replace(password, "[redacted]")
    return message
