"""Identity confirmation endpoints for panel/API admin MFA.

Flow:
  POST /auth/login              → access_token | {mfa_required, challenge_id, methods}
  POST /auth/mfa/sms            → send SMS OTP for challenge
  POST /auth/mfa/verify         → access_token after TOTP or SMS code
  POST /auth/logout             → revoke Bearer token
  GET  /auth/mfa                → admin MFA status (authenticated)
  POST /auth/mfa/totp/enroll    → start TOTP enrollment
  POST /auth/mfa/totp/confirm   → confirm TOTP with first code
  DELETE /auth/mfa/totp         → disable TOTP
  PATCH /auth/mfa/sms           → enable/configure SMS OTP phone
  GET|PUT /auth/mfa/providers   → SMS provider credentials (Crisaleo / SMS2SMS)
"""

from __future__ import annotations

import secrets
from datetime import UTC, datetime
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session

from .. import models, schemas
from ..db import get_db
from ..mfa import (
    admin_mfa_status,
    challenge_ttl,
    consume_challenge,
    create_challenge,
    ensure_admin_mfa_or_issue_challenge,
    generate_totp_secret,
    get_or_create_admin_mfa,
    issue_access_token,
    issue_sms_otp,
    load_access_token,
    load_challenge,
    load_totp_secret,
    normalize_phone,
    revoke_access_token,
    store_totp_secret,
    totp_issuer,
    totp_provisioning_uri,
    verify_admin_mfa_code,
    verify_totp_code,
)
from ..security import (
    clear_auth_failures,
    client_address,
    is_rate_limited,
    register_auth_failure,
    require_admin,
    verify_admin_secret,
)
from ..settings import get_settings
from ..sms_providers import PROVIDER_DEFAULTS, encrypt_secret, resolve_provider_config

router = APIRouter(prefix="/auth", tags=["auth"])
settings = get_settings()
bearer_optional = HTTPBearer(auto_error=False)


def _utcnow() -> datetime:
    return datetime.now(UTC).replace(tzinfo=None)


@router.post("/login")
def auth_login(payload: schemas.AuthLoginRequest, request: Request, db: Session = Depends(get_db)):
    address = client_address(request)
    if is_rate_limited(address):
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Too many authentication attempts",
            headers={"Retry-After": str(settings.api_auth_block_seconds)},
        )

    correct_user = secrets.compare_digest(payload.username, settings.api_admin_user)
    try:
        correct_pass = verify_admin_secret(payload.password)
    except HTTPException:
        register_auth_failure(address)
        raise
    if not (correct_user and correct_pass):
        register_auth_failure(address)
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")

    clear_auth_failures(address)
    challenge = ensure_admin_mfa_or_issue_challenge(db, settings.api_admin_user)
    if challenge:
        return challenge
    return issue_access_token(settings.api_admin_user, purpose="admin")


@router.post("/mfa/sms")
def auth_mfa_send_sms(payload: schemas.AuthMfaSmsSendRequest, request: Request, db: Session = Depends(get_db)):
    address = client_address(request)
    if is_rate_limited(address):
        raise HTTPException(status_code=429, detail="Too many authentication attempts")

    data = load_challenge(payload.challenge_id)
    if "sms" not in (data.get("methods") or []):
        raise HTTPException(status_code=400, detail="SMS is not available for this challenge")

    row = get_or_create_admin_mfa(db)
    if not row.sms_enabled or not row.sms_phone:
        raise HTTPException(status_code=400, detail="SMS MFA is not configured")

    return issue_sms_otp(
        db,
        phone=row.sms_phone,
        provider_code=row.sms_provider or "crisaleo",
        challenge_id=payload.challenge_id,
    )


@router.post("/mfa/verify")
def auth_mfa_verify(payload: schemas.AuthMfaVerifyRequest, request: Request, db: Session = Depends(get_db)):
    address = client_address(request)
    if is_rate_limited(address):
        raise HTTPException(status_code=429, detail="Too many authentication attempts")

    try:
        data = verify_admin_mfa_code(db, payload.challenge_id, payload.method, payload.code)
    except HTTPException:
        register_auth_failure(address)
        raise

    clear_auth_failures(address)
    username = str(data.get("subject") or settings.api_admin_user)
    return issue_access_token(username, purpose="admin")


@router.post("/logout")
def auth_logout(
    payload: schemas.AuthLogoutRequest | None = None,
    bearer: HTTPAuthorizationCredentials | None = Depends(bearer_optional),
):
    token = None
    if payload and payload.access_token:
        token = payload.access_token
    elif bearer and bearer.credentials:
        token = bearer.credentials
    if token:
        revoke_access_token(token)
    return {"ok": True}


@router.get("/mfa", response_model=schemas.MfaAdminStatusOut)
def auth_mfa_status(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    return admin_mfa_status(db)


@router.post("/mfa/totp/enroll", response_model=schemas.MfaTotpEnrollOut)
def auth_totp_enroll(db: Session = Depends(get_db), username: str = Depends(require_admin)):
    row = get_or_create_admin_mfa(db)
    secret = generate_totp_secret()
    store_totp_secret(row, secret)
    row.totp_enabled = False
    row.totp_confirmed_at = None
    db.commit()
    return {
        "secret": secret,
        "provisioning_uri": totp_provisioning_uri(secret, username),
        "issuer": totp_issuer(),
    }


@router.post("/mfa/totp/confirm", response_model=schemas.MfaAdminStatusOut)
def auth_totp_confirm(
    payload: schemas.MfaTotpConfirmRequest,
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    row = get_or_create_admin_mfa(db)
    secret = load_totp_secret(row)
    if not secret:
        raise HTTPException(status_code=400, detail="Start TOTP enrollment first")
    if not verify_totp_code(secret, payload.code):
        raise HTTPException(status_code=400, detail="Invalid TOTP code")
    row.totp_enabled = True
    row.totp_confirmed_at = _utcnow()
    db.commit()
    return admin_mfa_status(db)


@router.delete("/mfa/totp", response_model=schemas.MfaAdminStatusOut)
def auth_totp_disable(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    row = get_or_create_admin_mfa(db)
    row.totp_enabled = False
    row.totp_secret_enc = None
    row.totp_confirmed_at = None
    db.commit()
    return admin_mfa_status(db)


@router.patch("/mfa/sms", response_model=schemas.MfaAdminStatusOut)
def auth_sms_config(
    payload: schemas.MfaSmsConfigUpdate,
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    row = get_or_create_admin_mfa(db)
    if payload.phone is not None:
        if payload.phone == "":
            row.sms_phone = None
            row.sms_enabled = False
        else:
            row.sms_phone = normalize_phone(payload.phone)
    if payload.provider is not None:
        row.sms_provider = payload.provider
    if payload.enabled is not None:
        if payload.enabled and not row.sms_phone:
            raise HTTPException(status_code=400, detail="Configure a phone number before enabling SMS OTP")
        # Ensure provider is at least partially configured.
        if payload.enabled:
            cfg = resolve_provider_config(db, row.sms_provider or "crisaleo")
            if not cfg.is_configured():
                raise HTTPException(
                    status_code=400,
                    detail=f"SMS provider {row.sms_provider} is not configured (credentials required)",
                )
        row.sms_enabled = bool(payload.enabled)
    db.commit()
    return admin_mfa_status(db)


@router.get("/mfa/providers", response_model=List[schemas.MfaSmsProviderOut])
def list_sms_providers(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    rows = {r.code: r for r in db.query(models.MfaSmsProvider).all()}
    result = []
    for code, defaults in PROVIDER_DEFAULTS.items():
        row = rows.get(code)
        if row:
            result.append(
                schemas.MfaSmsProviderOut(
                    code=row.code,
                    enabled=bool(row.enabled),
                    base_url=row.base_url,
                    client_id=row.client_id,
                    has_client_secret=bool(row.client_secret_enc),
                    has_api_key=bool(row.api_key_enc),
                    send_path=row.send_path,
                    from_sender=row.from_sender,
                    route=row.route or "internal",
                    label=defaults.get("label", code),
                )
            )
        else:
            from ..sms_providers import default_provider_config

            cfg = default_provider_config(code)
            result.append(
                schemas.MfaSmsProviderOut(
                    code=code,
                    enabled=cfg.enabled,
                    base_url=cfg.base_url,
                    client_id=cfg.client_id or None,
                    has_client_secret=bool(cfg.client_secret),
                    has_api_key=bool(cfg.api_key),
                    send_path=cfg.send_path,
                    from_sender=cfg.from_sender or None,
                    route=cfg.route,
                    label=defaults.get("label", code),
                )
            )
    return result


@router.put("/mfa/providers", response_model=schemas.MfaSmsProviderOut)
def update_sms_provider(
    payload: schemas.MfaSmsProviderUpdate,
    db: Session = Depends(get_db),
    _: str = Depends(require_admin),
):
    defaults = PROVIDER_DEFAULTS[payload.code]
    row = db.query(models.MfaSmsProvider).filter(models.MfaSmsProvider.code == payload.code).first()
    if not row:
        row = models.MfaSmsProvider(
            code=payload.code,
            enabled=False,
            base_url=defaults["base_url"],
            send_path=defaults["send_path"],
            route="internal",
        )
        db.add(row)

    if payload.enabled is not None:
        row.enabled = payload.enabled
    if payload.base_url is not None:
        row.base_url = payload.base_url.rstrip("/")
    if payload.client_id is not None:
        row.client_id = payload.client_id
    if payload.send_path is not None:
        row.send_path = payload.send_path
    if payload.from_sender is not None:
        row.from_sender = payload.from_sender
    if payload.route is not None:
        row.route = payload.route
    if payload.clear_secrets:
        row.client_secret_enc = None
        row.api_key_enc = None
    if payload.client_secret:
        row.client_secret_enc = encrypt_secret(payload.client_secret)
    if payload.api_key:
        row.api_key_enc = encrypt_secret(payload.api_key)

    db.commit()
    db.refresh(row)
    return schemas.MfaSmsProviderOut(
        code=row.code,
        enabled=bool(row.enabled),
        base_url=row.base_url,
        client_id=row.client_id,
        has_client_secret=bool(row.client_secret_enc),
        has_api_key=bool(row.api_key_enc),
        send_path=row.send_path,
        from_sender=row.from_sender,
        route=row.route or "internal",
        label=defaults.get("label", row.code),
    )
