"""Single Sign-On (SSO) endpoints."""

import json
import secrets
import uuid
from datetime import UTC, datetime, timedelta

import jwt
import redis
from fastapi import (
  APIRouter,
  Cookie,
  Depends,
  HTTPException,
  Request,
  Response,
  status,
)
from sqlalchemy.orm import Session

from ...config import env
from ...config.constants import JWT_EXPIRY_HOURS, TOKEN_GRACE_PERIOD_MINUTES
from ...database import get_async_db_session
from ...logger import logger
from ...middleware.auth.distributed_lock import get_sso_lock_manager
from ...middleware.auth.jwt import (
  create_jwt_token,
  create_sso_token,
  get_async_redis_client,
  verify_jwt_claims,
)
from ...middleware.rate_limits import sso_rate_limit_dependency
from ...models.api.auth import (
  AuthResponse,
  SSOCompleteRequest,
  SSOExchangeRequest,
  SSOExchangeResponse,
  SSOTokenResponse,
)
from ...models.api.common import COMMON_ERROR_RESPONSES
from ...models.core import User
from ...security import SecurityAuditLogger, SecurityEventType
from ...security.device_fingerprinting import extract_device_fingerprint
from .utils import (
  AVAILABLE_APPS,
  SSO_SESSION_EXPIRY_SECONDS,
  SSO_TOKEN_EXPIRY_SECONDS,
  Config,
)

# Create router for SSO endpoints
router = APIRouter()


@router.post(
  "/sso-token",
  response_model=SSOTokenResponse,
  summary="Generate SSO Token",
  description="Step 1 of 3 in the cross-app SSO flow. Issues a single-use token (60s TTL) for handoff to a target application.",
  operation_id="generateSSOToken",
  responses={**COMMON_ERROR_RESPONSES},
)
async def generate_sso_token(
  request: Request,
  auth_token: str | None = Cookie(None, alias="auth-token"),  # Backward compatibility
  session: Session = Depends(get_async_db_session),
  _rate_limit: None = Depends(sso_rate_limit_dependency),
) -> SSOTokenResponse:
  try:
    # Extract JWT token from Authorization header (doesn't show in OpenAPI params) or fall back to cookie
    authorization = request.headers.get("authorization")
    jwt_token = None
    if authorization and authorization.startswith("Bearer "):
      jwt_token = authorization[7:]  # Remove "Bearer " prefix
    elif auth_token:
      jwt_token = auth_token  # Fallback to cookie for backward compatibility

    if not jwt_token:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Not authenticated",
        headers={"WWW-Authenticate": "Bearer"},
      )

    # Verify current JWT token (with device fingerprint binding)
    device_fingerprint = extract_device_fingerprint(request)
    verify_result = verify_jwt_claims(jwt_token, device_fingerprint)
    if not verify_result:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or expired token",
        headers={"WWW-Authenticate": "Bearer"},
      )
    user_id, token_session_version = verify_result

    # Get user
    user = User.get_by_id(user_id, session)
    if not user or not user.is_active:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive"
      )

    if int(user.session_version or 0) != int(token_session_version):
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Token session has been invalidated",
        headers={"WWW-Authenticate": "Bearer"},
      )

    # Create temporary SSO token
    sso_token, token_id = create_sso_token(user.id, session=session)
    expires_at = datetime.now(UTC) + timedelta(seconds=SSO_TOKEN_EXPIRY_SECONDS)

    # Store token ID in Valkey for single-use tracking with distributed locking
    try:
      redis_client = await get_async_redis_client()

      # Use distributed lock to prevent race conditions during token creation
      lock_manager = get_sso_lock_manager()
      if lock_manager:
        async with lock_manager.lock_sso_token(token_id, "token_creation"):
          # Atomic token storage
          existing_token = await redis_client.get(f"sso_token:{token_id}")
          if existing_token:
            # Token ID collision (very unlikely with UUID4)
            SecurityAuditLogger.log_security_event(
              event_type=SecurityEventType.SUSPICIOUS_ACTIVITY,
              details={
                "action": "sso_token_id_collision",
                "token_id": token_id[:8] + "...",
                "user_id": user.id,
              },
              risk_level="high",
            )
            raise HTTPException(
              status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
              detail="Token generation conflict",
            )

          await redis_client.setex(
            f"sso_token:{token_id}",
            SSO_TOKEN_EXPIRY_SECONDS,
            user.id,
          )
      else:
        # Fallback without locking if lock manager unavailable
        logger.warning(
          "SSO lock manager unavailable, proceeding without distributed locking"
        )
        await redis_client.setex(
          f"sso_token:{token_id}",
          SSO_TOKEN_EXPIRY_SECONDS,
          user.id,
        )

    except redis.RedisError as e:
      logger.error(f"Failed to store SSO token in Redis: {e!s}")
      SecurityAuditLogger.log_security_event(
        event_type=SecurityEventType.SUSPICIOUS_ACTIVITY,
        details={
          "action": "sso_token_storage_failed",
          "error": str(e),
          "user_id": user.id,
        },
        risk_level="high",
      )
      raise HTTPException(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        detail="SSO token generation failed",
      )

    # Use available apps constant
    available_apps = AVAILABLE_APPS

    return SSOTokenResponse(
      token=sso_token,
      expires_at=expires_at,
      apps=available_apps,
    )

  except HTTPException:
    raise
  except Exception as e:
    logger.error(f"SSO token generation error: {e!s}")
    raise HTTPException(
      status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
      detail="SSO token generation failed",
    )


@router.post(
  "/sso-exchange",
  response_model=SSOExchangeResponse,
  summary="SSO Token Exchange",
  description="Step 2 of 3. Exchanges the SSO token for a short-lived session ID. Avoids passing tokens in redirect URLs.",
  operation_id="ssoTokenExchange",
  responses={**COMMON_ERROR_RESPONSES},
)
async def sso_token_exchange(
  request: SSOExchangeRequest,
  session: Session = Depends(get_async_db_session),
  _rate_limit: None = Depends(sso_rate_limit_dependency),
) -> SSOExchangeResponse:
  try:
    # Validate token structure
    if not request.token or not isinstance(request.token, str):
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token format"
      )

    # Validate target app
    if request.target_app not in AVAILABLE_APPS:
      raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid target application"
      )

    # Verify SSO token
    secret_key = Config.get_jwt_secret()

    try:
      payload = jwt.decode(
        request.token,
        secret_key,
        algorithms=["HS256"],
        issuer=env.JWT_ISSUER,
        audience=env.JWT_AUDIENCE,
      )
      user_id = payload.get("user_id")
      is_sso = payload.get("sso", False)
      token_id = payload.get("token_id")

      if not user_id or not is_sso or not token_id:
        raise HTTPException(
          status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token"
        )

      # Check if token has already been used with distributed locking
      try:
        redis_client = await get_async_redis_client()
        lock_manager = get_sso_lock_manager()

        if lock_manager:
          async with lock_manager.lock_sso_token(token_id, "token_exchange"):
            # Atomic token verification and exchange marking
            stored_user_id = await redis_client.get(f"sso_token:{token_id}")

            if not stored_user_id:
              SecurityAuditLogger.log_security_event(
                event_type=SecurityEventType.AUTHORIZATION_DENIED,
                details={
                  "action": "sso_token_exchange_not_found",
                  "token_id": token_id[:8] + "...",
                  "user_id": user_id,
                },
                risk_level="medium",
              )
              raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="SSO token expired or already used",
              )

            if not secrets.compare_digest(stored_user_id, user_id):
              SecurityAuditLogger.log_security_event(
                event_type=SecurityEventType.AUTHORIZATION_DENIED,
                details={
                  "action": "sso_token_exchange_user_mismatch",
                  "token_id": token_id[:8] + "...",
                  "expected_user": user_id,
                },
                risk_level="high",
              )
              raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token"
              )

            # Check if token is already being exchanged
            exchange_marker = await redis_client.get(f"sso_token_exchange:{token_id}")
            if exchange_marker:
              SecurityAuditLogger.log_security_event(
                event_type=SecurityEventType.SUSPICIOUS_ACTIVITY,
                details={
                  "action": "sso_token_double_exchange_attempt",
                  "token_id": token_id[:8] + "...",
                  "user_id": user_id,
                },
                risk_level="high",
              )
              raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="SSO token is already being processed",
              )

            # Atomically mark token as being exchanged to prevent race conditions
            await redis_client.setex(
              f"sso_token_exchange:{token_id}", SSO_SESSION_EXPIRY_SECONDS, "exchanged"
            )

            # Log successful token exchange marking
            SecurityAuditLogger.log_security_event(
              event_type=SecurityEventType.AUTH_SUCCESS,
              details={
                "action": "sso_token_exchange_marked",
                "token_id": token_id[:8] + "...",
                "user_id": user_id,
              },
              risk_level="low",
            )
        else:
          # Fallback without locking
          logger.warning("SSO lock manager unavailable for token exchange")
          stored_user_id = await redis_client.get(f"sso_token:{token_id}")

          if not stored_user_id:
            raise HTTPException(
              status_code=status.HTTP_401_UNAUTHORIZED,
              detail="SSO token expired or already used",
            )

          if not secrets.compare_digest(stored_user_id, user_id):
            raise HTTPException(
              status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token"
            )

          await redis_client.setex(
            f"sso_token_exchange:{token_id}", SSO_SESSION_EXPIRY_SECONDS, "exchanged"
          )

      except redis.RedisError as e:
        logger.error(f"Redis error during SSO token exchange: {e!s}")
        raise HTTPException(
          status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
          detail="SSO token validation failed",
        )

    except jwt.ExpiredSignatureError:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="SSO token expired"
      )
    except jwt.InvalidTokenError:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token"
      )

    # Verify user exists and is active
    user = User.get_by_id(user_id, session)
    if not user or not user.is_active:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive"
      )

    try:
      token_session_version = int(payload.get("session_version", 0))
    except (TypeError, ValueError):
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid SSO token"
      )

    try:
      current_session_version = int(getattr(user, "session_version", 0) or 0)
    except (TypeError, ValueError):
      current_session_version = 0

    if token_session_version != current_session_version:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="SSO token session has been invalidated",
      )

    # Create temporary session ID for secure handoff
    session_id = str(uuid.uuid4())
    expires_at = datetime.now(UTC) + timedelta(
      seconds=SSO_SESSION_EXPIRY_SECONDS
    )  # Very short

    # Store session mapping in Redis with distributed locking
    try:
      redis_client = await get_async_redis_client()
      lock_manager = get_sso_lock_manager()

      if lock_manager:
        async with lock_manager.lock_sso_session(session_id, "session_creation"):
          # Check for session ID collision
          existing_session = await redis_client.get(f"sso_session:{session_id}")
          if existing_session:
            SecurityAuditLogger.log_security_event(
              event_type=SecurityEventType.SUSPICIOUS_ACTIVITY,
              details={
                "action": "sso_session_id_collision",
                "session_id": session_id[:8] + "...",
                "user_id": user_id,
              },
              risk_level="high",
            )
            raise HTTPException(
              status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
              detail="Session creation conflict",
            )

          session_data = {
            "user_id": user_id,
            "token_id": token_id,  # Reference to original SSO token
            "target_app": request.target_app,
            "return_url": request.return_url,
            "created_at": datetime.now(UTC).isoformat(),
          }

          # Atomically store session data
          await redis_client.setex(
            f"sso_session:{session_id}",
            SSO_SESSION_EXPIRY_SECONDS,
            json.dumps(session_data),
          )

          # Log successful session creation
          SecurityAuditLogger.log_security_event(
            event_type=SecurityEventType.AUTH_SUCCESS,
            details={
              "action": "sso_session_created",
              "session_id": session_id[:8] + "...",
              "user_id": user_id,
              "target_app": request.target_app,
            },
            risk_level="low",
          )
      else:
        # Fallback without locking
        logger.warning("SSO lock manager unavailable for session creation")
        session_data = {
          "user_id": user_id,
          "token_id": token_id,
          "target_app": request.target_app,
          "return_url": request.return_url,
        }

        await redis_client.setex(
          f"sso_session:{session_id}",
          SSO_SESSION_EXPIRY_SECONDS,
          json.dumps(session_data),
        )

    except redis.RedisError as e:
      logger.error(f"Failed to store SSO session in Redis: {e!s}")
      raise HTTPException(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        detail="Session creation failed",
      )

    # Build app-specific redirect URLs
    app_urls = Config.get_app_urls()

    base_url = app_urls.get(request.target_app)
    if not base_url:
      raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST, detail="Target app URL not configured"
      )

    # Create secure handoff URL - uses POST endpoint instead of URL params
    redirect_url = f"{base_url}/auth/sso-complete"

    return SSOExchangeResponse(
      session_id=session_id,
      redirect_url=redirect_url,
      expires_at=expires_at,
    )

  except HTTPException:
    raise
  except Exception as e:
    logger.error(f"SSO token exchange error: {e!s}")
    raise HTTPException(
      status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="SSO exchange failed"
    )


@router.post(
  "/sso-complete",
  response_model=AuthResponse,
  summary="Complete SSO Authentication",
  description="Step 3 of 3. Exchanges the session ID for a full JWT token. Called by the target app after redirect.",
  operation_id="completeSSOAuth",
  responses={**COMMON_ERROR_RESPONSES},
)
async def sso_complete(
  request: Request,
  body: SSOCompleteRequest,
  response: Response,
  session: Session = Depends(get_async_db_session),
  _rate_limit: None = Depends(sso_rate_limit_dependency),
) -> AuthResponse:
  try:
    session_id = body.session_id

    # Retrieve and validate session with distributed locking
    try:
      redis_client = await get_async_redis_client()
      lock_manager = get_sso_lock_manager()

      if lock_manager:
        async with lock_manager.lock_sso_session(session_id, "session_completion"):
          # Atomic session consumption
          session_data_str = await redis_client.get(f"sso_session:{session_id}")

          if not session_data_str:
            SecurityAuditLogger.log_security_event(
              event_type=SecurityEventType.AUTHORIZATION_DENIED,
              details={
                "action": "sso_session_not_found",
                "session_id": session_id[:8] + "...",
              },
              risk_level="medium",
            )
            raise HTTPException(
              status_code=status.HTTP_401_UNAUTHORIZED,
              detail="Session expired or invalid",
            )

          # Parse session data
          session_data = json.loads(session_data_str)
          user_id = session_data.get("user_id")
          token_id = session_data.get("token_id")

          if not user_id or not token_id:
            SecurityAuditLogger.log_security_event(
              event_type=SecurityEventType.SUSPICIOUS_ACTIVITY,
              details={
                "action": "sso_session_invalid_data",
                "session_id": session_id[:8] + "...",
                "session_data": session_data,
              },
              risk_level="high",
            )
            raise HTTPException(
              status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session data"
            )

          # Atomically delete session and related tokens (single use)
          await redis_client.delete(f"sso_session:{session_id}")
          await redis_client.delete(f"sso_token:{token_id}")
          await redis_client.delete(f"sso_token_exchange:{token_id}")

          # Log successful session completion
          SecurityAuditLogger.log_security_event(
            event_type=SecurityEventType.AUTH_SUCCESS,
            details={
              "action": "sso_session_completed",
              "session_id": session_id[:8] + "...",
              "user_id": user_id,
              "target_app": session_data.get("target_app"),
            },
            risk_level="low",
          )
      else:
        # Fallback without locking
        logger.warning("SSO lock manager unavailable for session completion")
        session_data_str = await redis_client.get(f"sso_session:{session_id}")

        if not session_data_str:
          raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Session expired or invalid",
          )

        session_data = json.loads(session_data_str.decode("utf-8"))
        user_id = session_data.get("user_id")
        token_id = session_data.get("token_id")

        if not user_id or not token_id:
          raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session data"
          )

        await redis_client.delete(f"sso_session:{session_id}")
        await redis_client.delete(f"sso_token:{token_id}")
        await redis_client.delete(f"sso_token_exchange:{token_id}")

    except redis.RedisError as e:
      logger.error(f"Redis error during SSO completion: {e!s}")
      raise HTTPException(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        detail="Session validation failed",
      )
    except json.JSONDecodeError as e:
      logger.error(f"Invalid session data format: {e!s}")
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session data"
      )

    # Verify user exists and is active
    user = User.get_by_id(user_id, session)
    if not user or not user.is_active:
      raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive"
      )

    # Create new JWT token for this session with device binding
    device_fingerprint = extract_device_fingerprint(request)
    jwt_token = create_jwt_token(user.id, device_fingerprint, session=session)

    # No longer setting auth cookies - using Bearer token authentication
    # Token is returned in the response body for the frontend to store

    expires_in = int(JWT_EXPIRY_HOURS * 3600)
    refresh_threshold = int(TOKEN_GRACE_PERIOD_MINUTES * 60)

    return AuthResponse(
      user={
        "id": user.id,
        "name": user.name,
        "email": user.email,
        "email_verified": user.email_verified,
      },
      message="SSO authentication completed successfully",
      token=jwt_token,  # Return JWT for Bearer authentication
      expires_in=expires_in,
      refresh_threshold=refresh_threshold,
    )

  except HTTPException:
    raise
  except Exception as e:
    logger.error(f"SSO completion error: {e!s}")
    raise HTTPException(
      status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="SSO completion failed"
    )
