"""Authentication API router for Mantimon TCG.

This module provides endpoints for OAuth authentication:
- OAuth login redirects (Google, Discord)
- OAuth callbacks (token exchange)
- Token refresh
- Logout

OAuth Flow:
    1. Client calls GET /auth/{provider} to get redirect URL
    2. Client redirects user to OAuth provider
    3. Provider redirects back to GET /auth/{provider}/callback
    4. Server exchanges code for tokens, creates/fetches user
    5. Server returns JWT access + refresh tokens

Example:
    # Start OAuth flow
    GET /api/auth/google?redirect_uri=https://play.mantimon.com/login/callback

    # After OAuth callback, refresh tokens
    POST /api/auth/refresh
    {"refresh_token": "..."}

    # Logout
    POST /api/auth/logout
    {"refresh_token": "..."}
"""

import secrets

from fastapi import APIRouter, HTTPException, Query, status
from fastapi.responses import RedirectResponse

from app.api.deps import CurrentUser, UserServiceDep
from app.config import settings
from app.db.redis import get_redis
from app.schemas.auth import RefreshTokenRequest, TokenResponse
from app.services.jwt_service import (
    create_access_token,
    create_refresh_token,
    get_refresh_token_expiration,
    get_token_expiration_seconds,
    verify_refresh_token,
)
from app.services.oauth.discord import DiscordOAuthError, discord_oauth
from app.services.oauth.google import GoogleOAuthError, google_oauth
from app.services.token_store import token_store
from app.services.user_service import AccountLinkingError

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

# OAuth state TTL (5 minutes)
OAUTH_STATE_TTL = 300


async def _store_oauth_state(state: str, provider: str, redirect_uri: str) -> None:
    """Store OAuth state in Redis for CSRF validation."""
    async with get_redis() as redis:
        key = f"oauth_state:{state}"
        value = f"{provider}:{redirect_uri}"
        await redis.setex(key, OAUTH_STATE_TTL, value)


async def _validate_oauth_state(state: str, provider: str) -> str | None:
    """Validate and consume OAuth state, returning redirect_uri if valid."""
    async with get_redis() as redis:
        key = f"oauth_state:{state}"
        value = await redis.get(key)
        if not value:
            return None

        # Delete state (one-time use)
        await redis.delete(key)

        # Parse and validate
        stored_provider, redirect_uri = value.split(":", 1)
        if stored_provider != provider:
            return None

        return redirect_uri


async def _create_tokens_for_user(user_id) -> TokenResponse:
    """Create access and refresh tokens for a user."""
    from uuid import UUID

    if isinstance(user_id, str):
        user_id = UUID(user_id)

    access_token = create_access_token(user_id)
    refresh_token, jti = create_refresh_token(user_id)

    # Store refresh token in Redis for revocation tracking
    expires_at = get_refresh_token_expiration()
    await token_store.store_refresh_token(user_id, jti, expires_at)

    return TokenResponse(
        access_token=access_token,
        refresh_token=refresh_token,
        expires_in=get_token_expiration_seconds(),
    )


# =============================================================================
# Google OAuth
# =============================================================================


@router.get("/google")
async def google_auth_redirect(
    redirect_uri: str = Query(..., description="URI to redirect to after OAuth completes"),
) -> RedirectResponse:
    """Start Google OAuth flow.

    Redirects the user to Google's OAuth consent screen.

    Args:
        redirect_uri: Where to redirect after successful authentication.
                     This is YOUR app's callback, not the OAuth callback.

    Returns:
        Redirect to Google OAuth authorization URL.

    Raises:
        HTTPException: 501 if Google OAuth is not configured.
    """
    if not google_oauth.is_configured():
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="Google OAuth is not configured",
        )

    # Generate state for CSRF protection
    state = secrets.token_urlsafe(32)
    await _store_oauth_state(state, "google", redirect_uri)

    # Build OAuth callback URL (our server endpoint)
    # The redirect_uri param here is where Google sends the code
    # Must be an absolute URL for OAuth providers
    oauth_callback = f"{settings.base_url}/api/auth/google/callback"

    # Get authorization URL
    auth_url = google_oauth.get_authorization_url(
        redirect_uri=oauth_callback,
        state=state,
    )

    return RedirectResponse(url=auth_url, status_code=status.HTTP_302_FOUND)


@router.get("/google/callback")
async def google_auth_callback(
    user_service: UserServiceDep,
    code: str = Query(..., description="Authorization code from Google"),
    state: str = Query(..., description="State parameter for CSRF validation"),
) -> TokenResponse:
    """Handle Google OAuth callback.

    Exchanges the authorization code for tokens and creates/fetches the user.

    Args:
        code: Authorization code from Google.
        state: State parameter for CSRF validation.

    Returns:
        JWT access and refresh tokens.

    Raises:
        HTTPException: 400 if state is invalid or OAuth fails.
    """
    # Validate state
    redirect_uri = await _validate_oauth_state(state, "google")
    if redirect_uri is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired state parameter",
        )

    try:
        # Exchange code for user info
        oauth_callback = f"{settings.base_url}/api/auth/google/callback"
        user_info = await google_oauth.get_user_info(code, oauth_callback)

        # Get or create user
        user, _created = await user_service.get_or_create_from_oauth(user_info)

        # Update last login
        await user_service.update_last_login(user.id)

        # Create tokens
        return await _create_tokens_for_user(user.id)

    except GoogleOAuthError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Google OAuth failed: {e}",
        ) from None


# =============================================================================
# Discord OAuth
# =============================================================================


@router.get("/discord")
async def discord_auth_redirect(
    redirect_uri: str = Query(..., description="URI to redirect to after OAuth completes"),
) -> RedirectResponse:
    """Start Discord OAuth flow.

    Redirects the user to Discord's OAuth consent screen.

    Args:
        redirect_uri: Where to redirect after successful authentication.

    Returns:
        Redirect to Discord OAuth authorization URL.

    Raises:
        HTTPException: 501 if Discord OAuth is not configured.
    """
    if not discord_oauth.is_configured():
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="Discord OAuth is not configured",
        )

    # Generate state for CSRF protection
    state = secrets.token_urlsafe(32)
    await _store_oauth_state(state, "discord", redirect_uri)

    # Build OAuth callback URL (must be absolute for OAuth providers)
    oauth_callback = f"{settings.base_url}/api/auth/discord/callback"

    # Get authorization URL
    auth_url = discord_oauth.get_authorization_url(
        redirect_uri=oauth_callback,
        state=state,
    )

    return RedirectResponse(url=auth_url, status_code=status.HTTP_302_FOUND)


@router.get("/discord/callback")
async def discord_auth_callback(
    user_service: UserServiceDep,
    code: str = Query(..., description="Authorization code from Discord"),
    state: str = Query(..., description="State parameter for CSRF validation"),
) -> RedirectResponse:
    """Handle Discord OAuth callback.

    Exchanges the authorization code for tokens and creates/fetches the user.
    Redirects to the frontend with tokens in URL fragment.

    Args:
        code: Authorization code from Discord.
        state: State parameter for CSRF validation.

    Returns:
        Redirect to frontend with access and refresh tokens.

    Raises:
        HTTPException: 400 if state is invalid or OAuth fails.
    """
    # Validate state
    redirect_uri = await _validate_oauth_state(state, "discord")
    if redirect_uri is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired state parameter",
        )

    try:
        # Exchange code for user info
        oauth_callback = f"{settings.base_url}/api/auth/discord/callback"
        user_info = await discord_oauth.get_user_info(code, oauth_callback)

        # Get or create user
        user, _created = await user_service.get_or_create_from_oauth(user_info)

        # Update last login
        await user_service.update_last_login(user.id)

        # Create tokens
        tokens = await _create_tokens_for_user(user.id)

        # Redirect to frontend with tokens in URL fragment (not query params for security)
        # Fragment is not sent to server, only accessible by frontend JavaScript
        fragment = f"access_token={tokens.access_token}&refresh_token={tokens.refresh_token}&expires_in={tokens.expires_in}"
        return RedirectResponse(
            url=f"{redirect_uri}#{fragment}",
            status_code=status.HTTP_302_FOUND,
        )

    except DiscordOAuthError as e:
        # Redirect to frontend with error
        return RedirectResponse(
            url=f"{redirect_uri}?error=oauth_failed&message={e}",
            status_code=status.HTTP_302_FOUND,
        )


# =============================================================================
# Token Management
# =============================================================================


@router.post("/refresh", response_model=TokenResponse)
async def refresh_tokens(
    user_service: UserServiceDep,
    request: RefreshTokenRequest,
) -> TokenResponse:
    """Refresh access token using refresh token.

    Validates the refresh token and issues a new access token.
    The refresh token itself is NOT rotated (same token can be used
    until it expires or is revoked).

    Args:
        request: Contains the refresh token.

    Returns:
        New JWT access token (same refresh token).

    Raises:
        HTTPException: 401 if refresh token is invalid or revoked.
    """
    # Verify refresh token
    result = verify_refresh_token(request.refresh_token)
    if result is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token",
        )

    user_id, jti = result

    # Check if token is revoked
    is_valid = await token_store.is_token_valid(user_id, jti)
    if not is_valid:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Refresh token has been revoked",
        )

    # Verify user still exists
    user = await user_service.get_by_id(user_id)
    if user is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="User not found",
        )

    # Create new access token (keep same refresh token)
    access_token = create_access_token(user_id)

    return TokenResponse(
        access_token=access_token,
        refresh_token=request.refresh_token,  # Return same refresh token
        expires_in=get_token_expiration_seconds(),
    )


@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout(
    request: RefreshTokenRequest,
) -> None:
    """Logout by revoking the refresh token.

    The access token will continue to work until it expires,
    but the refresh token cannot be used to get new tokens.

    Args:
        request: Contains the refresh token to revoke.
    """
    # Verify refresh token to get user_id and jti
    result = verify_refresh_token(request.refresh_token)
    if result is None:
        # Token is invalid, but that's fine - user is effectively logged out
        return

    user_id, jti = result

    # Revoke the token
    await token_store.revoke_token(user_id, jti)


@router.post("/logout-all", status_code=status.HTTP_204_NO_CONTENT)
async def logout_all(
    user: CurrentUser,
) -> None:
    """Logout from all devices by revoking all refresh tokens.

    Requires authentication (uses current access token).
    All refresh tokens for the user will be revoked.

    Args:
        user: Current authenticated user.
    """
    from uuid import UUID

    user_id = UUID(user.id) if isinstance(user.id, str) else user.id
    await token_store.revoke_all_user_tokens(user_id)


# =============================================================================
# Account Linking
# =============================================================================


async def _store_link_state(state: str, provider: str, user_id: str, redirect_uri: str) -> None:
    """Store OAuth state for account linking (includes user_id)."""
    async with get_redis() as redis:
        key = f"oauth_link_state:{state}"
        value = f"{provider}:{user_id}:{redirect_uri}"
        await redis.setex(key, OAUTH_STATE_TTL, value)


async def _validate_link_state(state: str, provider: str) -> tuple[str, str] | None:
    """Validate and consume link state, returning (user_id, redirect_uri) if valid."""
    async with get_redis() as redis:
        key = f"oauth_link_state:{state}"
        value = await redis.get(key)
        if not value:
            return None

        # Delete state (one-time use)
        await redis.delete(key)

        # Parse and validate
        parts = value.split(":", 2)
        if len(parts) != 3:
            return None

        stored_provider, user_id, redirect_uri = parts
        if stored_provider != provider:
            return None

        return user_id, redirect_uri


@router.get("/link/google")
async def google_link_redirect(
    user: CurrentUser,
    redirect_uri: str = Query(..., description="URI to redirect to after linking"),
) -> RedirectResponse:
    """Start Google OAuth flow for account linking.

    Requires authentication. Links Google account to the current user.

    Args:
        redirect_uri: Where to redirect after linking completes.

    Returns:
        Redirect to Google OAuth authorization URL.

    Raises:
        HTTPException: 501 if Google OAuth is not configured.
        HTTPException: 400 if Google is already the primary provider.
    """
    if not google_oauth.is_configured():
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="Google OAuth is not configured",
        )

    # Check if Google is already their primary provider
    if user.oauth_provider == "google":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Google is already your primary login provider",
        )

    # Generate state for CSRF protection
    state = secrets.token_urlsafe(32)
    await _store_link_state(state, "google", str(user.id), redirect_uri)

    # Build OAuth callback URL for linking
    oauth_callback = f"{settings.base_url}/api/auth/link/google/callback"

    # Get authorization URL
    auth_url = google_oauth.get_authorization_url(
        redirect_uri=oauth_callback,
        state=state,
    )

    return RedirectResponse(url=auth_url, status_code=status.HTTP_302_FOUND)


@router.get("/link/google/callback")
async def google_link_callback(
    user_service: UserServiceDep,
    code: str = Query(..., description="Authorization code from Google"),
    state: str = Query(..., description="State parameter for CSRF validation"),
) -> RedirectResponse:
    """Handle Google OAuth callback for account linking.

    Exchanges the authorization code and links the Google account to the user.

    Args:
        code: Authorization code from Google.
        state: State parameter for CSRF validation.

    Returns:
        Redirect to the original redirect_uri with success/error query params.
    """
    # Validate state
    result = await _validate_link_state(state, "google")
    if result is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired state parameter",
        )

    user_id_str, redirect_uri = result

    try:
        # Exchange code for user info
        oauth_callback = f"{settings.base_url}/api/auth/link/google/callback"
        oauth_info = await google_oauth.get_user_info(code, oauth_callback)

        # Get the user
        from uuid import UUID

        user_id = UUID(user_id_str)
        user = await user_service.get_by_id(user_id)
        if user is None:
            return RedirectResponse(
                url=f"{redirect_uri}?error=user_not_found",
                status_code=status.HTTP_302_FOUND,
            )

        # Link the account
        await user_service.link_oauth_account(user.id, user.oauth_provider, oauth_info)

        return RedirectResponse(
            url=f"{redirect_uri}?linked=google",
            status_code=status.HTTP_302_FOUND,
        )

    except GoogleOAuthError as e:
        return RedirectResponse(
            url=f"{redirect_uri}?error=oauth_failed&message={e}",
            status_code=status.HTTP_302_FOUND,
        )
    except AccountLinkingError as e:
        return RedirectResponse(
            url=f"{redirect_uri}?error=linking_failed&message={e}",
            status_code=status.HTTP_302_FOUND,
        )


@router.get("/link/discord")
async def discord_link_redirect(
    user: CurrentUser,
    redirect_uri: str = Query(..., description="URI to redirect to after linking"),
) -> RedirectResponse:
    """Start Discord OAuth flow for account linking.

    Requires authentication. Links Discord account to the current user.

    Args:
        redirect_uri: Where to redirect after linking completes.

    Returns:
        Redirect to Discord OAuth authorization URL.

    Raises:
        HTTPException: 501 if Discord OAuth is not configured.
        HTTPException: 400 if Discord is already the primary provider.
    """
    if not discord_oauth.is_configured():
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="Discord OAuth is not configured",
        )

    # Check if Discord is already their primary provider
    if user.oauth_provider == "discord":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Discord is already your primary login provider",
        )

    # Generate state for CSRF protection
    state = secrets.token_urlsafe(32)
    await _store_link_state(state, "discord", str(user.id), redirect_uri)

    # Build OAuth callback URL for linking
    oauth_callback = f"{settings.base_url}/api/auth/link/discord/callback"

    # Get authorization URL
    auth_url = discord_oauth.get_authorization_url(
        redirect_uri=oauth_callback,
        state=state,
    )

    return RedirectResponse(url=auth_url, status_code=status.HTTP_302_FOUND)


@router.get("/link/discord/callback")
async def discord_link_callback(
    user_service: UserServiceDep,
    code: str = Query(..., description="Authorization code from Discord"),
    state: str = Query(..., description="State parameter for CSRF validation"),
) -> RedirectResponse:
    """Handle Discord OAuth callback for account linking.

    Exchanges the authorization code and links the Discord account to the user.

    Args:
        code: Authorization code from Discord.
        state: State parameter for CSRF validation.

    Returns:
        Redirect to the original redirect_uri with success/error query params.
    """
    # Validate state
    result = await _validate_link_state(state, "discord")
    if result is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired state parameter",
        )

    user_id_str, redirect_uri = result

    try:
        # Exchange code for user info
        oauth_callback = f"{settings.base_url}/api/auth/link/discord/callback"
        oauth_info = await discord_oauth.get_user_info(code, oauth_callback)

        # Get the user
        from uuid import UUID

        user_id = UUID(user_id_str)
        user = await user_service.get_by_id(user_id)
        if user is None:
            return RedirectResponse(
                url=f"{redirect_uri}?error=user_not_found",
                status_code=status.HTTP_302_FOUND,
            )

        # Link the account
        await user_service.link_oauth_account(user.id, user.oauth_provider, oauth_info)

        return RedirectResponse(
            url=f"{redirect_uri}?linked=discord",
            status_code=status.HTTP_302_FOUND,
        )

    except DiscordOAuthError as e:
        return RedirectResponse(
            url=f"{redirect_uri}?error=oauth_failed&message={e}",
            status_code=status.HTTP_302_FOUND,
        )
    except AccountLinkingError as e:
        return RedirectResponse(
            url=f"{redirect_uri}?error=linking_failed&message={e}",
            status_code=status.HTTP_302_FOUND,
        )
