from __future__ import annotations

import logging

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

from app.api.routes.api.deps import require_csrf, require_session
from app.config import Settings
from app.dependencies import get_app_settings, get_db
from app.schemas.session import (
    LoginRequest,
    PasswordChangeRequest,
    SessionResponse,
    SessionUser,
)
from app.schemas.totp import (
    TotpDisableRequest,
    TotpEnableRequest,
    TotpSetupResponse,
    TotpStatusResponse,
)
from app.services.auth import (
    AuthPasswordChangeError,
    AuthenticatedSession,
    authenticate_user,
    change_password,
    create_session,
    revoke_session,
)
from app.services import login_throttle
from app.services import totp as totp_service

logger = logging.getLogger(__name__)

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


def _build_session_response(auth: AuthenticatedSession) -> SessionResponse:
    return SessionResponse(
        user=SessionUser(
            username=auth.user.username,
            force_password_change=auth.user.force_password_change,
        ),
        csrf_token=auth.session.csrf_token,
    )


@router.get("/session", response_model=SessionResponse)
def get_session(
    auth: AuthenticatedSession = Depends(require_session),
) -> SessionResponse:
    """Return the current session user and CSRF token. Returns 401 if not authenticated."""
    return _build_session_response(auth)


def _get_client_ip(request: Request, *, trust_forwarded_for: bool) -> str:
    """Extract the client IP address from the request.

    When ``trust_forwarded_for`` is True (reverse-proxy deployments) the
    left-most value from the ``X-Forwarded-For`` header is used.  Otherwise the
    direct socket IP (``request.client.host``) is used.
    """
    if trust_forwarded_for:
        xff = request.headers.get("X-Forwarded-For", "")
        if xff:
            return xff.split(",")[0].strip()
    if request.client is not None:
        return request.client.host
    return "unknown"


@router.post("/auth/login", response_model=SessionResponse)
def post_login(
    body: LoginRequest,
    request: Request,
    response: Response,
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_app_settings),
) -> SessionResponse:
    """
    Authenticate with username and password.

    On success, sets an HttpOnly session cookie and returns the session user + CSRF token.
    On failure, returns 401 with no cookie set.
    Repeated failures trigger exponential back-off (429 + Retry-After).
    No X-CSRF-Token required (unauthenticated endpoint).
    """
    client_ip = _get_client_ip(request, trust_forwarded_for=settings.auth_trust_forwarded_for)

    # --- Throttle check (before any password verification) ---
    if settings.auth_login_throttle_enabled:
        wait_seconds = login_throttle.check_and_get_wait(
            db, ip=client_ip, username=body.username
        )
        if wait_seconds > 0:
            raise HTTPException(
                status_code=status.HTTP_429_TOO_MANY_REQUESTS,
                detail="too many failed login attempts; please try again later",
                headers={"Retry-After": str(wait_seconds)},
            )

    # --- Password verification ---
    user = authenticate_user(db, username=body.username, password=body.password)
    if user is None:
        if settings.auth_login_throttle_enabled:
            login_throttle.register_failure(db, ip=client_ip, username=body.username)
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="invalid username or password",
        )

    # --- TOTP second-factor check (only when TOTP is enabled for the user) ---
    if user.totp_enabled:
        if not body.totp_code:
            # Password correct but no TOTP code supplied: signal the front-end to
            # prompt for the second factor.  Do NOT issue a session.
            # Deliberate: this is a normal two-step protocol step from a legitimate
            # user — we do NOT register a throttle failure here.  The attacker must
            # already have the correct password to reach this branch, and counting
            # each normal first-step as a failure would risk locking out the real
            # admin on every login attempt.
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail={"totp_required": True},
            )

        # TOTP code supplied — verify against time-based code or a recovery code.
        totp_ok = totp_service.verify_totp_code(user, body.totp_code)
        if not totp_ok:
            totp_ok = totp_service.verify_recovery_code(db, user=user, code=body.totp_code)

        if not totp_ok:
            # Second-factor failure is an active attack signal — register failure
            # so that repeated wrong TOTP/recovery-code guesses trigger back-off.
            if settings.auth_login_throttle_enabled:
                login_throttle.register_failure(db, ip=client_ip, username=body.username)
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="invalid username or password",
            )

    # --- Success: clear back-off state and issue session ---
    if settings.auth_login_throttle_enabled:
        login_throttle.clear(db, ip=client_ip, username=body.username)

    auth_session, raw_token = create_session(db, user=user, settings=settings)
    logger.info("Created API authenticated session for user '%s'", user.username)

    response.set_cookie(
        key=settings.auth_session_cookie_name,
        value=raw_token,
        max_age=settings.auth_session_ttl_hours * 3600,
        httponly=True,
        secure=settings.auth_cookie_secure,
        samesite="lax",
        path="/",
    )

    auth = AuthenticatedSession(user=user, session=auth_session)
    return _build_session_response(auth)


@router.post("/auth/logout")
def post_logout(
    response: Response,
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_app_settings),
    auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> Response:
    """
    Revoke the current session and clear the session cookie.
    Requires authentication and X-CSRF-Token header.
    Returns 204 No Content.
    """
    revoke_session(db, auth_session=auth.session)
    logger.info("Revoked API authenticated session for user '%s'", auth.user.username)
    no_content = Response(status_code=status.HTTP_204_NO_CONTENT)
    no_content.delete_cookie(settings.auth_session_cookie_name, path="/")
    return no_content


@router.post("/auth/password")
def post_change_password(
    body: PasswordChangeRequest,
    db: Session = Depends(get_db),
    auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> Response:
    """
    Change the current user's password.
    Requires authentication and X-CSRF-Token header.
    On AuthPasswordChangeError returns 400 with a generic message.
    On success, force_password_change becomes False (handled by the service).
    Returns 204 No Content.
    """
    try:
        change_password(
            db,
            user=auth.user,
            current_password=body.current_password,
            new_password=body.new_password,
            confirm_password=body.confirm_password,
        )
    except AuthPasswordChangeError as exc:
        logger.info(
            "Rejected password change for user '%s': %s",
            auth.user.username,
            exc,
        )
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="password change failed",
        ) from exc

    logger.info("Password updated for user '%s'", auth.user.username)
    return Response(status_code=status.HTTP_204_NO_CONTENT)


# ---------------------------------------------------------------------------
# TOTP endpoints (M4-T05)
# ---------------------------------------------------------------------------


@router.post("/auth/totp/setup", response_model=TotpSetupResponse)
def post_totp_setup(
    db: Session = Depends(get_db),
    settings: Settings = Depends(get_app_settings),
    auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> TotpSetupResponse:
    """
    Generate a new pending TOTP secret, otpauth URI, and one-time recovery codes.

    The secret is stored in the DB but TOTP is NOT yet enabled (totp_enabled stays
    False until the user confirms with POST /api/auth/totp/enable).

    Recovery codes are returned here as plaintext exactly once; their Argon2 hashes
    are persisted immediately so enable only needs to flip the enabled flag.

    Repeating this call replaces any prior pending secret and regenerates codes.

    Requires: session cookie + X-CSRF-Token.
    """
    secret, otpauth_uri, recovery_codes = totp_service.setup(
        db,
        user=auth.user,
        issuer=settings.effective_totp_issuer,
    )
    return TotpSetupResponse(
        secret=secret,
        otpauth_uri=otpauth_uri,
        recovery_codes=recovery_codes,
    )


@router.post("/auth/totp/enable", status_code=status.HTTP_204_NO_CONTENT)
def post_totp_enable(
    body: TotpEnableRequest,
    db: Session = Depends(get_db),
    auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> Response:
    """
    Enable TOTP by confirming with the current 6-digit code from the authenticator app.

    Requires a prior call to POST /api/auth/totp/setup (so that a pending secret
    exists).  On success, totp_enabled becomes True.

    Returns 400 if the code is wrong or there is no pending secret.
    Requires: session cookie + X-CSRF-Token.
    """
    ok = totp_service.enable(db, user=auth.user, code=body.code)
    if not ok:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="invalid TOTP code or no pending setup",
        )
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post("/auth/totp/disable", status_code=status.HTTP_204_NO_CONTENT)
def post_totp_disable(
    body: TotpDisableRequest,
    db: Session = Depends(get_db),
    auth: AuthenticatedSession = Depends(require_session),
    _csrf: None = Depends(require_csrf),
) -> Response:
    """
    Disable TOTP.  The caller must provide exactly one of:
    - ``password``: the user's current login password, OR
    - ``code``: the current 6-digit TOTP code.

    On success: totp_enabled=False, totp_secret cleared, all recovery codes deleted.
    Returns 400 if neither credential matches or neither is provided.
    Requires: session cookie + X-CSRF-Token.
    """
    ok = totp_service.disable(
        db,
        user=auth.user,
        password=body.password,
        code=body.code,
    )
    if not ok:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="invalid credential; provide a valid password or TOTP code",
        )
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.get("/auth/totp", response_model=TotpStatusResponse)
def get_totp_status(
    auth: AuthenticatedSession = Depends(require_session),
) -> TotpStatusResponse:
    """
    Return the current TOTP status for the authenticated user.

    Response contains only ``{"enabled": bool}``.
    Secret and recovery codes are NEVER returned here.
    Requires: session cookie only (no CSRF — read-only).
    """
    return TotpStatusResponse(enabled=auth.user.totp_enabled)
