"""Authentication routes — cookie-based JWT sessions.

⚠️ AUTO-GENERATED BY PRISM (GENERATE_ONCE)
Signup, login, email verification, password reset, MFA/TOTP, OAuth.
"""

from __future__ import annotations

import logging
import os
{% if oauth_providers %}
import secrets
{% endif %}
from datetime import UTC, datetime, timedelta
from typing import Annotated
{% if oauth_providers %}
from urllib.parse import urlencode
{% endif %}

{% if oauth_providers %}
import httpx
{% endif %}
from fastapi import APIRouter, Cookie, Depends, HTTPException, {% if oauth_providers %}Query, {% endif %}Response, status
{% if oauth_providers %}
from fastapi.responses import RedirectResponse
{% endif %}
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from {{ project_name }}.auth.password_service import (
    generate_token,
    hash_password,
    validate_password_strength,
    verify_password,
)
from {{ project_name }}.auth.token_service import create_session_jwt
from {{ project_name }}.auth.config import auth_settings
from {{ project_name }}.database import get_db
from {{ project_name }}.middleware.auth import CurrentActiveUser
from {{ project_name }}.models.{{ user_model_snake }} import {{ user_model }}
from {{ project_name }}.schemas.auth import (
    LoginRequest,
    LoginResponse,
    UserResponse,
{% if mfa_enabled %}
    LoginMFARequest,
    MFADisableRequest,
    MFASetupResponse,
    MFAVerifySetupRequest,
{% endif %}
{% if email_verification %}
    ResendVerificationRequest,
    VerifyEmailRequest,
{% endif %}
{% if password_reset %}
    ForgotPasswordRequest,
    ResetPasswordRequest,
{% endif %}
)
{% if has_email_features %}
from {{ project_name }}.services.email_service import (
{% if email_verification %}
    send_verification_email,
{% endif %}
{% if password_reset %}
    send_password_changed_notification,
    send_password_reset_email,
{% endif %}
)
{% endif %}
{% if mfa_enabled %}
from {{ project_name }}.auth.totp_service import (
    generate_totp_secret,
    get_totp_uri,
    verify_totp,
)
{% endif %}

logger = logging.getLogger(__name__)

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


# ── Helpers ─────────────────────────────────────────────────────


def _is_secure_context() -> bool:
    debug = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
    return not debug


def _set_session_cookie(response: Response, token: str) -> None:
    response.set_cookie(
        key=auth_settings.session_cookie_name,
        value=token,
        max_age=auth_settings.session_max_age,
        httponly=True,
        secure=_is_secure_context(),
        samesite="lax",
    )


def _clear_session_cookie(response: Response) -> None:
    response.delete_cookie(
        key=auth_settings.session_cookie_name,
        httponly=True,
        secure=_is_secure_context(),
        samesite="lax",
    )

{% if account_lockout %}

def _check_account_locked(user: {{ user_model }}) -> None:
    """Raise 423 if the account is currently locked."""
    if user.locked_until and user.locked_until > datetime.now(UTC):
        raise HTTPException(
            status_code=status.HTTP_423_LOCKED,
            detail="Account temporarily locked due to too many failed login attempts. Try again later.",
        )


def _record_failed_login(user: {{ user_model }}) -> None:
    """Increment failed attempts and lock if threshold reached."""
    user.failed_login_attempts = (user.failed_login_attempts or 0) + 1
    if user.failed_login_attempts >= {{ max_failed_attempts }}:
        user.locked_until = datetime.now(UTC) + timedelta(minutes={{ lockout_duration_minutes }})


def _reset_failed_logins(user: {{ user_model }}) -> None:
    user.failed_login_attempts = 0
    user.locked_until = None
{% endif %}


# ── Signup ──────────────────────────────────────────────────────


@router.post("/signup")
async def signup(
    body: SignupRequest,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict[str, str]:
    """Create a new user account{% if email_verification %} and send verification email{% endif %}."""
    from {{ project_name }}.schemas.auth import SignupRequest

    pw_error = validate_password_strength(body.password)
    if pw_error:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=pw_error)

    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.{{ username_field }} == body.{{ username_field }})
    )
    if result.scalar_one_or_none():
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="An account with this {{ username_field }} already exists.",
        )

{% if email_verification %}
    token = generate_token()
    user = {{ user_model }}(
        {{ username_field }}=body.{{ username_field }},
        username=body.username,
        password_hash=hash_password(body.password),
        email_verified=False,
        email_verification_token=token,
        email_verification_token_expires_at=datetime.now(UTC)
        + timedelta(hours={{ email_verification_token_hours }}),
        roles=["{{ default_role }}"],
        is_active=True,
    )
    db.add(user)
    await db.commit()

    send_verification_email(body.{{ username_field }}, token)
    return {"message": "Account created. Please check your email to verify your address."}
{% else %}
    user = {{ user_model }}(
        {{ username_field }}=body.{{ username_field }},
        username=body.username,
        password_hash=hash_password(body.password),
        roles=["{{ default_role }}"],
        is_active=True,
    )
    db.add(user)
    await db.commit()
    return {"message": "Account created."}
{% endif %}


{% if email_verification %}
# ── Email verification ──────────────────────────────────────────


@router.post("/verify-email")
async def verify_email(
    body: VerifyEmailRequest,
    response: Response,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict:
    """Verify email address and auto-login."""
    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.email_verification_token == body.token)
    )
    user = result.scalar_one_or_none()

    if not user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired verification token.",
        )

    if (
        user.email_verification_token_expires_at
        and user.email_verification_token_expires_at < datetime.now(UTC)
    ):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Verification token has expired. Please request a new one.",
        )

    user.email_verified = True
    user.email_verification_token = None
    user.email_verification_token_expires_at = None
    await db.commit()
    await db.refresh(user)

    token = create_session_jwt(user)
    _set_session_cookie(response, token)

    return {"message": "Email verified", "user": UserResponse.model_validate(user).model_dump()}


@router.post("/resend-verification")
async def resend_verification(
    body: ResendVerificationRequest,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict[str, str]:
    """Resend verification email. Always returns 200 to prevent email enumeration."""
    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.{{ username_field }} == body.{{ username_field }})
    )
    user = result.scalar_one_or_none()

    if user and not user.email_verified:
        token = generate_token()
        user.email_verification_token = token
        user.email_verification_token_expires_at = datetime.now(UTC) + timedelta(
            hours={{ email_verification_token_hours }}
        )
        await db.commit()
        send_verification_email(body.{{ username_field }}, token)

    return {"message": "If an unverified account exists, a verification email has been sent."}
{% endif %}


# ── Login ───────────────────────────────────────────────────────


@router.post("/login", response_model=LoginResponse)
async def login(
    body: LoginRequest,
    response: Response,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> LoginResponse:
    """Login with {{ username_field }} and password."""
    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.{{ username_field }} == body.{{ username_field }})
    )
    user = result.scalar_one_or_none()

    if not user or not user.password_hash:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid {{ username_field }} or password.",
        )
{% if account_lockout %}

    _check_account_locked(user)
{% endif %}

    if not verify_password(body.password, user.password_hash):
{% if account_lockout %}
        _record_failed_login(user)
        await db.commit()
{% endif %}
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid {{ username_field }} or password.",
        )
{% if account_lockout %}

    _reset_failed_logins(user)
{% endif %}

{% if email_verification %}
    if not user.email_verified:
        await db.commit()
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Please verify your email before logging in.",
        )
{% endif %}

    if not user.is_active:
        await db.commit()
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Account is inactive.",
        )

{% if mfa_enabled %}
    if user.mfa_enabled and user.mfa_secret:
        await db.commit()
        return LoginResponse(requires_mfa=True)
{% endif %}

    await db.commit()

    token = create_session_jwt(user)
    _set_session_cookie(response, token)
    return LoginResponse(
        requires_mfa=False,
        user=UserResponse.model_validate(user).model_dump(),
    )


{% if mfa_enabled %}
@router.post("/login/mfa", response_model=LoginResponse)
async def login_mfa(
    body: LoginMFARequest,
    response: Response,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> LoginResponse:
    """Complete MFA login with TOTP code."""
    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.{{ username_field }} == body.{{ username_field }})
    )
    user = result.scalar_one_or_none()

    if not user or not user.mfa_secret:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid credentials.",
        )
{% if account_lockout %}

    _check_account_locked(user)
{% endif %}

    if not verify_totp(user.mfa_secret, body.code):
{% if account_lockout %}
        _record_failed_login(user)
        await db.commit()
{% endif %}
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid TOTP code.",
        )
{% if account_lockout %}

    _reset_failed_logins(user)
{% endif %}
    await db.commit()

    token = create_session_jwt(user)
    _set_session_cookie(response, token)
    return LoginResponse(
        requires_mfa=False,
        user=UserResponse.model_validate(user).model_dump(),
    )
{% endif %}


{% if password_reset %}
# ── Password reset ──────────────────────────────────────────────


@router.post("/forgot-password")
async def forgot_password(
    body: ForgotPasswordRequest,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict[str, str]:
    """Send password reset email. Always returns 200 to prevent email enumeration."""
    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.{{ username_field }} == body.{{ username_field }})
    )
    user = result.scalar_one_or_none()

    if user:
        token = generate_token()
        user.password_reset_token = token
        user.password_reset_token_expires_at = datetime.now(UTC) + timedelta(
            hours={{ password_reset_token_hours }}
        )
        await db.commit()
        send_password_reset_email(body.{{ username_field }}, token)

    return {"message": "If an account exists, a password reset email has been sent."}


@router.post("/reset-password")
async def reset_password(
    body: ResetPasswordRequest,
    response: Response,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict:
    """Reset password using token and auto-login."""
    pw_error = validate_password_strength(body.password)
    if pw_error:
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=pw_error)

    result = await db.execute(
        select({{ user_model }}).where({{ user_model }}.password_reset_token == body.token)
    )
    user = result.scalar_one_or_none()

    if not user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired reset token.",
        )

    if (
        user.password_reset_token_expires_at
        and user.password_reset_token_expires_at < datetime.now(UTC)
    ):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Reset token has expired.",
        )

    user.password_hash = hash_password(body.password)
    user.password_reset_token = None
    user.password_reset_token_expires_at = None
{% if account_lockout %}
    _reset_failed_logins(user)
{% endif %}
    await db.commit()
    await db.refresh(user)

    send_password_changed_notification(user.{{ username_field }})

    token = create_session_jwt(user)
    _set_session_cookie(response, token)

    return {"message": "Password reset", "user": UserResponse.model_validate(user).model_dump()}
{% endif %}


{% if mfa_enabled %}
# ── MFA / TOTP ──────────────────────────────────────────────────


@router.post("/mfa/setup", response_model=MFASetupResponse)
async def mfa_setup(
    current_user: CurrentActiveUser,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> MFASetupResponse:
    """Generate TOTP secret and URI for QR code. Requires authentication."""
    secret = generate_totp_secret()
    current_user.mfa_secret = secret
    await db.commit()

    uri = get_totp_uri(secret, current_user.{{ username_field }})
    return MFASetupResponse(totp_uri=uri, secret=secret)


@router.post("/mfa/verify-setup")
async def mfa_verify_setup(
    body: MFAVerifySetupRequest,
    current_user: CurrentActiveUser,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict[str, str]:
    """Verify TOTP code and enable MFA."""
    if not current_user.mfa_secret:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="MFA setup not started. Call /auth/mfa/setup first.",
        )

    if not verify_totp(current_user.mfa_secret, body.code):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid TOTP code.",
        )

    current_user.mfa_enabled = True
    await db.commit()
    return {"message": "MFA enabled successfully."}


@router.post("/mfa/disable")
async def mfa_disable(
    body: MFADisableRequest,
    current_user: CurrentActiveUser,
    db: Annotated[AsyncSession, Depends(get_db)],
) -> dict[str, str]:
    """Disable MFA. Requires password verification."""
    if not current_user.password_hash or not verify_password(
        body.password, current_user.password_hash
    ):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid password.",
        )

    current_user.mfa_enabled = False
    current_user.mfa_secret = None
    await db.commit()
    return {"message": "MFA disabled."}
{% endif %}


{% for provider in oauth_providers %}
{% if provider.provider == "github" %}
# ── GitHub OAuth ────────────────────────────────────────────────

GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
GITHUB_USER_API = "https://api.github.com/user"
GITHUB_EMAILS_API = "https://api.github.com/user/emails"

_oauth_states: dict[str, float] = {}


def _cleanup_expired_states() -> None:
    now = datetime.now(UTC).timestamp()
    expired = [k for k, v in _oauth_states.items() if now - v > 300]
    for k in expired:
        _oauth_states.pop(k, None)


@router.get("/github/login")
async def github_login() -> RedirectResponse:
    """Redirect to GitHub OAuth authorization page."""
    if not auth_settings.github_client_id:
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="GitHub OAuth is not configured",
        )

    _cleanup_expired_states()
    state = secrets.token_urlsafe(32)
    _oauth_states[state] = datetime.now(UTC).timestamp()

    params = urlencode(
        {
            "client_id": auth_settings.github_client_id,
            "redirect_uri": auth_settings.github_redirect_uri,
            "scope": "{{ provider.scopes | join(' ') or 'user:email' }}",
            "state": state,
        }
    )
    return RedirectResponse(
        url=f"{GITHUB_AUTHORIZE_URL}?{params}",
        status_code=status.HTTP_302_FOUND,
    )


@router.get("/github/callback")
async def github_callback(
    db: Annotated[AsyncSession, Depends(get_db)],
    code: str | None = Query(None),
    state: str | None = Query(None),
    error: str | None = Query(None),
) -> RedirectResponse:
    """Handle GitHub OAuth callback."""
    if error:
        return RedirectResponse(url=f"/login?error={error}", status_code=status.HTTP_302_FOUND)

    if not code or not state:
        return RedirectResponse(
            url="/auth/callback?error=missing_params", status_code=status.HTTP_302_FOUND
        )

    if state not in _oauth_states:
        return RedirectResponse(
            url="/auth/callback?error=invalid_state", status_code=status.HTTP_302_FOUND
        )
    _oauth_states.pop(state, None)

    async with httpx.AsyncClient() as client:
        token_resp = await client.post(
            GITHUB_TOKEN_URL,
            data={
                "client_id": auth_settings.github_client_id,
                "client_secret": auth_settings.github_client_secret,
                "code": code,
                "redirect_uri": auth_settings.github_redirect_uri,
            },
            headers={"Accept": "application/json"},
        )
        if token_resp.status_code != 200:
            logger.error("GitHub token exchange failed: %s", token_resp.text)
            return RedirectResponse(
                url="/auth/callback?error=token_exchange_failed",
                status_code=status.HTTP_302_FOUND,
            )

        token_data = token_resp.json()
        access_token = token_data.get("access_token")
        if not access_token:
            return RedirectResponse(
                url="/auth/callback?error=no_access_token",
                status_code=status.HTTP_302_FOUND,
            )

        auth_headers = {"Authorization": f"Bearer {access_token}"}

        user_resp = await client.get(GITHUB_USER_API, headers=auth_headers)
        if user_resp.status_code != 200:
            return RedirectResponse(
                url="/auth/callback?error=github_user_failed",
                status_code=status.HTTP_302_FOUND,
            )
        gh_user = user_resp.json()

        email = gh_user.get("email")
        if not email:
            emails_resp = await client.get(GITHUB_EMAILS_API, headers=auth_headers)
            if emails_resp.status_code == 200:
                for em in emails_resp.json():
                    if em.get("primary") and em.get("verified"):
                        email = em["email"]
                        break

        if not email:
            return RedirectResponse(
                url="/auth/callback?error=no_email", status_code=status.HTTP_302_FOUND
            )

    username = gh_user.get("login", email.split("@")[0])
    github_id = str(gh_user.get("id", ""))

    result = await db.execute(select({{ user_model }}).where({{ user_model }}.github_id == github_id))
    user = result.scalar_one_or_none()

    if not user:
        result = await db.execute(select({{ user_model }}).where({{ user_model }}.email == email))
        user = result.scalar_one_or_none()

    if user:
        if not user.github_id:
            user.github_id = github_id
            await db.commit()
    else:
        user = {{ user_model }}(
            email=email,
            username=username,
            github_id=github_id,
            email_verified=True,
            is_active=True,
            roles=["{{ default_role }}"],
        )
        db.add(user)
        await db.commit()
        await db.refresh(user)

    jwt_token = create_session_jwt(user)
    redirect = RedirectResponse(url="/auth/callback", status_code=status.HTTP_302_FOUND)
    _set_session_cookie(redirect, jwt_token)
    return redirect
{% endif %}
{% if provider.provider == "google" %}
# ── Google OAuth ────────────────────────────────────────────────
# TODO: Implement Google OAuth flow similar to GitHub
{% endif %}
{% endfor %}


# ── Session endpoints ───────────────────────────────────────────


@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
    current_user: CurrentActiveUser,
) -> UserResponse:
    """Get current authenticated user information."""
    return UserResponse.model_validate(current_user)


@router.post("/logout")
async def logout_post(
    response: Response,
    session_token: str | None = Cookie(None, alias=auth_settings.session_cookie_name),
) -> dict[str, str]:
    """Logout: clear JWT session cookie."""
    _clear_session_cookie(response)
    return {"message": "Logged out successfully"}


@router.get("/logout")
async def logout_get(
    session_token: str | None = Cookie(None, alias=auth_settings.session_cookie_name),
) -> RedirectResponse:
    """Logout via GET: clear cookie and redirect to login."""
    from fastapi.responses import RedirectResponse

    redirect = RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
    redirect.delete_cookie(
        key=auth_settings.session_cookie_name,
        httponly=True,
        secure=_is_secure_context(),
        samesite="lax",
    )
    return redirect
