"""
MQTT authentication and ACL endpoints for mosquitto-go-auth HTTP backend.

Mosquitto calls these on every CONNECT, SUBSCRIBE, and PUBLISH.
- POST /mqtt/auth/user  → validate device credentials
- POST /mqtt/auth/acl   → enforce per-device topic isolation

Password strategy: HMAC-SHA256(MQTT_SECRET, username)[:32]
- Deterministic: no storage needed, re-derive on every auth check
- Rotating MQTT_SECRET invalidates all passwords at once if needed

Transition support: during rollout, the legacy password "vesper" is also
accepted so that devices still on old firmware stay connected.

User types handled:
- Device users (e.g. "PV25L22BP01R01", "PV-26A18-BC02R-X7KQA"):
    Authenticated via HMAC. ACL restricted to their own vesper/{sn}/... topics.
- Kiosk users (e.g. "PV25L22BP01R01-kiosk"):
    Same HMAC auth derived from the full kiosk username.
    ACL: allowed to access topics of their base device (suffix stripped).
- admin, bonamin, NodeRED, and other non-device users:
    These connect via the passwd file backend (go-auth file backend).
    They never reach this HTTP backend — go-auth resolves them first.
    The ACL endpoint below handles them defensively anyway (superuser list).
"""

import hmac
import hashlib
from fastapi import APIRouter, Form, Response
from config import settings

router = APIRouter(prefix="/mqtt/auth", tags=["mqtt-auth"])

LEGACY_PASSWORD = "vesper"

# Users authenticated via passwd file (go-auth file backend).
# If they somehow reach the HTTP ACL endpoint, grant full access.
SUPERUSERS = {"admin", "bonamin", "NodeRED"}


def _derive_password(username: str) -> str:
    """Derive the expected MQTT password for a given username."""
    return hmac.new(
        settings.mqtt_secret.encode(),
        username.encode(),
        hashlib.sha256,
    ).hexdigest()[:32]


def _is_valid_password(username: str, password: str) -> bool:
    """
    Accept the password if it matches either:
    - The HMAC-derived password (new firmware)
    - The legacy hardcoded "vesper" password (old firmware, transition period)

    Remove the legacy check in Stage 7 once all devices are on new firmware.
    """
    expected = _derive_password(username)
    hmac_ok = hmac.compare_digest(expected, password)
    legacy_ok = hmac.compare_digest(LEGACY_PASSWORD, password)
    return hmac_ok or legacy_ok


def _base_sn(username: str) -> str:
    """
    Strip the -kiosk suffix if present, returning the base serial number.
    e.g. "PV25L22BP01R01-kiosk" -> "PV25L22BP01R01"
         "PV25L22BP01R01"       -> "PV25L22BP01R01"
    """
    if username.endswith("-kiosk"):
        return username[: -len("-kiosk")]
    return username


@router.post("/user")
async def mqtt_auth_user(
    username: str = Form(...),
    password: str = Form(...),
    clientid: str = Form(default=""),
):
    """
    Called by Mosquitto on every CONNECT.
    Returns 200 to allow, 403 to deny.

    Username = device SN (new format: "PV-26A18-BC02R-X7KQA", old format: "PV25L22BP01R01")
               or kiosk variant: "PV25L22BP01R01-kiosk"
    Password = HMAC-derived (new firmware) or "vesper" (legacy firmware)

    Note: admin, bonamin and NodeRED authenticate via the go-auth passwd file backend
    and never reach this endpoint.
    """
    if _is_valid_password(username, password):
        return Response(status_code=200)

    return Response(status_code=403)


@router.post("/acl")
async def mqtt_auth_acl(
    username: str = Form(...),
    topic: str = Form(...),
    clientid: str = Form(default=""),
    acc: int = Form(...),  # 1 = subscribe, 2 = publish, 3 = subscribe+publish
):
    """
    Called by Mosquitto on every SUBSCRIBE and PUBLISH.
    Returns 200 to allow, 403 to deny.

    Topic pattern: vesper/{sn}/...
    - Device users: may only access their own SN segment
    - Kiosk users: stripped of -kiosk suffix, then same rule applies
    - Superusers (bonamin, NodeRED): full access
    """
    # Superusers get full access (shouldn't reach here but handled defensively)
    if username in SUPERUSERS:
        return Response(status_code=200)

    # Derive the base SN (handles -kiosk suffix)
    base = _base_sn(username)

    # Topic must be vesper/{base_sn}/...
    parts = topic.split("/")
    if len(parts) >= 2 and parts[0] == "vesper" and parts[1] == base:
        return Response(status_code=200)

    return Response(status_code=403)
