"""SMS-based multi-factor authentication endpoints.

Endpoints split into three groups:

* ``/api/v1/auth/mfa/status``       — current user inspects their MFA state.
* ``/api/v1/auth/mfa/setup``        — enroll / verify / disable for the
  logged-in user.
* ``/api/v1/auth/mfa/challenge/...``— login-challenge step, called by the UI
  with the short-lived ``mfa_token`` returned by ``/auth/local/login``.
* ``/api/v1/auth/mfa/admin/...``    — admin-only roster + global enforcement
  toggle.

The heavy lifting lives in ``services/mfa.py``. These routes only translate
between HTTP, Pydantic schemas, and the service layer.
"""

from __future__ import annotations

import logging
from typing import List

from core.database import get_db
from dependencies.auth import get_admin_user, get_current_user
from fastapi import APIRouter, Depends, HTTPException, Request, status
from models.auth import User
from models.mfa import UserMFA
from schemas.auth import UserResponse
from schemas.local_auth import LocalAuthResponse
from schemas.mfa import (
    MfaAdminEnforceResponse,
    MfaAdminEnforceUpdate,
    MfaAdminRosterEntry,
    MfaChallengeSendRequest,
    MfaChallengeVerifyRequest,
    MfaDisableRequest,
    MfaGenericResponse,
    MfaSetupRequest,
    MfaSetupResponse,
    MfaStatusResponse,
    MfaVerifySetupRequest,
)
from services.audit_log import ACTION_LOGIN, record_admin_event
from services.auth import AuthService
from services.local_auth import LocalAuthService
from services.mfa import (
    CODE_TTL_MINUTES,
    decode_mfa_challenge_token,
    disable_mfa,
    get_mfa,
    get_require_admin_mfa,
    send_login_challenge,
    set_require_admin_mfa,
    start_enrollment,
    verify_enrollment,
    verify_login_challenge,
)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/auth/mfa", tags=["mfa"])


# ---------------- helpers ----------------
def _mask(phone):
    if not phone:
        return None
    if len(phone) <= 4:
        return "•" * len(phone)
    return f"{phone[:3]}•••{phone[-2:]}"


def _status_from_row(row) -> MfaStatusResponse:
    if row is None:
        return MfaStatusResponse(enabled=False, method="sms", phone_masked=None, last_verified_at=None)
    return MfaStatusResponse(
        enabled=bool(row.enabled),
        method=row.method or "sms",
        phone_masked=_mask(row.phone_e164),
        last_verified_at=row.last_verified_at.isoformat() if row.last_verified_at else None,
    )


async def _load_user(db: AsyncSession, user_id: str) -> User:
    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if user is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
    return user


# ---------------- user endpoints ----------------
@router.get("/status", response_model=MfaStatusResponse)
async def mfa_status(
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> MfaStatusResponse:
    row = await get_mfa(db, current_user.id)
    return _status_from_row(row)


@router.post("/setup", response_model=MfaSetupResponse)
async def mfa_setup(
    payload: MfaSetupRequest,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> MfaSetupResponse:
    """Start SMS enrollment: persist pending phone + send verification code."""
    user = await _load_user(db, current_user.id)
    try:
        row, _code = await start_enrollment(db, user, payload.phone_e164)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
    except RuntimeError as exc:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
    except Exception:  # noqa: BLE001
        logger.exception("[mfa] Unexpected error during setup for user=%s", current_user.id)
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Failed to start MFA setup",
        )
    return MfaSetupResponse(
        ok=True,
        message="Verification code sent.",
        phone_masked=_mask(row.pending_phone),
        expires_in_seconds=CODE_TTL_MINUTES * 60,
    )


@router.post("/setup/verify", response_model=MfaStatusResponse)
async def mfa_setup_verify(
    payload: MfaVerifySetupRequest,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> MfaStatusResponse:
    """Confirm enrollment by submitting the SMS code."""
    user = await _load_user(db, current_user.id)
    try:
        row = await verify_enrollment(db, user, payload.code)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))
    return _status_from_row(row)


@router.post("/disable", response_model=MfaStatusResponse)
async def mfa_disable(
    payload: MfaDisableRequest,
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
) -> MfaStatusResponse:
    """Disable MFA after re-confirming the account password.

    Platform/OIDC accounts (no local password profile) are also supported —
    in that case the password field is ignored but still required to keep
    the UX consistent, and the user must be the one holding the session
    token, so the security trade-off is acceptable.
    """
    user = await _load_user(db, current_user.id)
    local_service = LocalAuthService(db)
    profile = await local_service.get_profile_for_user(user)
    if profile is not None:
        authed = await local_service.authenticate(email=user.email, password=payload.password)
        if authed is None or authed.id != user.id:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Password verification failed.",
            )
    row = await disable_mfa(db, user)
    return _status_from_row(row)


# ---------------- challenge endpoints ----------------
@router.post("/challenge/send", response_model=MfaGenericResponse)
async def mfa_challenge_send(
    payload: MfaChallengeSendRequest,
    db: AsyncSession = Depends(get_db),
) -> MfaGenericResponse:
    """Resend the login OTP for a pending MFA challenge."""
    try:
        user_id = decode_mfa_challenge_token(payload.mfa_token)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
    user = await _load_user(db, user_id)
    try:
        await send_login_challenge(db, user)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail=str(exc))
    except RuntimeError as exc:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
    return MfaGenericResponse(ok=True, message="Verification code resent.")


@router.post("/challenge/verify", response_model=LocalAuthResponse)
async def mfa_challenge_verify(
    payload: MfaChallengeVerifyRequest,
    request: Request,
    db: AsyncSession = Depends(get_db),
) -> LocalAuthResponse:
    """Exchange mfa_token + OTP for the final session JWT."""
    try:
        user_id = decode_mfa_challenge_token(payload.mfa_token)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc))
    user = await _load_user(db, user_id)
    try:
        await verify_login_challenge(db, user_id, payload.code)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc))

    auth_service = AuthService(db)
    token, _expires_at, _claims = await auth_service.issue_app_token(user=user)

    await record_admin_event(
        db,
        action=ACTION_LOGIN,
        admin_user_id=user.id,
        admin_email=user.email,
        target_user_id=user.id,
        target_email=user.email,
        details={"role": user.role, "auth_provider": "local", "mfa": "sms"},
        request=request,
    )

    return LocalAuthResponse(
        token=token,
        user_id=user.id,
        email=user.email,
        role=user.role,
    )


# ---------------- admin endpoints ----------------
@router.get("/admin/enforcement", response_model=MfaAdminEnforceResponse)
async def mfa_admin_get_enforcement(
    _admin: UserResponse = Depends(get_admin_user),
) -> MfaAdminEnforceResponse:
    return MfaAdminEnforceResponse(require_admin_mfa=await get_require_admin_mfa())


@router.put("/admin/enforcement", response_model=MfaAdminEnforceResponse)
async def mfa_admin_update_enforcement(
    payload: MfaAdminEnforceUpdate,
    admin: UserResponse = Depends(get_admin_user),
) -> MfaAdminEnforceResponse:
    await set_require_admin_mfa(payload.require_admin_mfa, updated_by=admin.email or admin.id)
    return MfaAdminEnforceResponse(require_admin_mfa=await get_require_admin_mfa())


@router.get("/admin/roster", response_model=List[MfaAdminRosterEntry])
async def mfa_admin_roster(
    _admin: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> List[MfaAdminRosterEntry]:
    """Return one row per admin user with their MFA status."""
    result = await db.execute(select(User).where(User.role == "admin").order_by(User.email))
    admins = list(result.scalars().all())
    if not admins:
        return []
    rows_result = await db.execute(select(UserMFA).where(UserMFA.user_id.in_([u.id for u in admins])))
    rows_by_user = {row.user_id: row for row in rows_result.scalars().all()}
    out: List[MfaAdminRosterEntry] = []
    for u in admins:
        row = rows_by_user.get(u.id)
        out.append(
            MfaAdminRosterEntry(
                user_id=u.id,
                email=u.email or "",
                name=u.name,
                enabled=bool(row.enabled) if row else False,
                phone_masked=_mask(row.phone_e164) if row else None,
                last_verified_at=row.last_verified_at.isoformat() if (row and row.last_verified_at) else None,
            )
        )
    return out