# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/routers/tokens.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Mihai Criveti

JWT Token Catalog API endpoints.
Provides comprehensive API token management with scoping, revocation, and analytics.
"""

# Standard
import logging
from typing import List, Optional

# Third-Party
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

# First-Party
from mcpgateway.auth_context import get_user_email
from mcpgateway.common.validators import SecurityValidator
from mcpgateway.db import get_db
from mcpgateway.middleware.rbac import get_current_user_with_permissions, require_permission
from mcpgateway.schemas import TokenCreateRequest, TokenCreateResponse, TokenListResponse, TokenResponse, TokenRevokeRequest, TokenUpdateRequest, TokenUsageStatsResponse
from mcpgateway.services.permission_service import PermissionService
from mcpgateway.services.token_catalog_service import TokenCatalogService, TokenScope
from mcpgateway.utils.error_formatter import PublicValidationError, safe_error_detail, should_expose_error_details

logger = logging.getLogger(__name__)

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


def _handle_token_integrity_error(err_str: str) -> None:
    """Handle IntegrityError for token creation with sanitized error messages.

    Extracts duplicated logic for handling token name uniqueness constraint
    violations. Raises HTTPException with appropriate status and detail.

    Args:
        err_str: The error string from the IntegrityError

    Raises:
        HTTPException: 409 CONFLICT with appropriate detail message
    """
    # Match the specific name constraint: PostgreSQL reports the constraint name
    # (either the db.py name or the Alembic migration name); SQLite reports column paths.
    if (
        "uq_email_api_tokens_user_name_team" in err_str
        or "uq_email_api_tokens_user_name" in err_str
        or "uq_email_api_tokens_user_name_global" in err_str
        or "uq_email_api_tokens_user_email_name" in err_str
        or ("email_api_tokens.user_email" in err_str and "email_api_tokens.name" in err_str)
    ):
        if should_expose_error_details():
            detail = "A token with this name already exists for this user in the same team scope. Token names must be unique per user per team. Please choose a different name."
        else:
            detail = "A token with this name already exists. Please choose a different name."
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)

    # Generic conflict error
    if should_expose_error_details():
        detail = "Token creation failed due to a conflict. Please try again."
    else:
        detail = "Request could not be completed. Please try again."
    raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)


def _require_authenticated_session(current_user: dict) -> None:
    """Block anonymous, unauthenticated, and API-token access to token management endpoints.

    Enforces Management Plane isolation: only interactive sessions (JWT from web
    login, SSO, or OAuth) may create, list, or revoke tokens.  API tokens are
    Data Plane credentials and must never be able to manage other tokens
    (token-chaining attack vector).

    Args:
        current_user: User context from get_current_user_with_permissions

    Raises:
        HTTPException: 403 if auth_method is None, anonymous, or api_token
    """
    auth_method = current_user.get("auth_method")

    # Fail-secure: block if auth_method not set (indicates incomplete auth flow)
    if auth_method is None:
        logger.warning("Token management blocked: auth_method not set. This indicates an auth code path that needs to set request.state.auth_method")
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Token management requires authentication. Authentication method could not be determined.",
        )

    # Block anonymous users (missing proxy header or unauthenticated)
    if auth_method == "anonymous":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Token management requires authentication. Anonymous access is not permitted.",
        )

    # Block API tokens from managing other tokens (Management Plane isolation).
    # Token CRUD endpoints require an interactive session (JWT from web login or SSO).
    # Allowing API tokens here would let a compromised token create new long-lived
    # tokens and escalate persistence — a token-chaining attack.
    if auth_method == "api_token":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail=("Token management requires an interactive session (JWT from web login or SSO). " "API tokens cannot create, list, or revoke other tokens."),
        )


async def _get_caller_permissions(
    db: Session,
    current_user: dict,
    team_id: Optional[str] = None,
) -> Optional[List[str]]:
    """Get caller's effective permissions for scope containment.

    Args:
        db: Database session
        current_user: User context
        team_id: Team context for permission lookup

    Returns:
        List of permissions, or ["*"] for admins
    """
    # SECURITY: Only treat admin as unrestricted when token is un-narrowed.
    # Narrowed or public-only admin sessions must derive permissions through
    # the token-aware path to enforce Layer 1 scope containment.
    token_teams = current_user.get("token_teams")
    if current_user.get("is_admin") and token_teams is None:
        return ["*"]  # Un-narrowed admins can grant anything

    permission_service = PermissionService(db)
    permissions = await permission_service.get_user_permissions(
        user_email=current_user["email"],
        team_id=team_id,
        token_teams=token_teams,  # SECURITY: Respect token narrowing
    )
    return list(permissions) if permissions else None


@router.post("", response_model=TokenCreateResponse, status_code=status.HTTP_201_CREATED)
@require_permission("tokens.create")
async def create_token(
    request: TokenCreateRequest,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenCreateResponse:
    """Create a new API token for the current user or another user (admin only).

    Args:
        request: Token creation request with name, description, scoping, etc.
        current_user: Authenticated user from JWT
        db: Database session

    Returns:
        TokenCreateResponse: Created token details with raw token

    Raises:
        HTTPException: If token name already exists, validation fails, or insufficient permissions

    Examples:
        >>> import asyncio
        >>> asyncio.iscoroutinefunction(create_token)
        True
    """
    _require_authenticated_session(current_user)

    caller_email = get_user_email(current_user)

    # Determine target user for token creation.
    # When the caller passes their own email (even with different casing),
    # use the canonical caller_email to avoid case-drift in the database.
    if request.user_email and request.user_email.lower() != caller_email.lower():
        target_user_email = request.user_email
    else:
        target_user_email = caller_email

    # If creating token for different user, require un-narrowed platform admin
    if request.user_email and request.user_email.lower() != caller_email.lower():
        # Require platform admin privilege
        if not current_user.get("is_admin"):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Admin access required to create tokens for other users",
            )

        # Require un-narrowed admin access (token_teams=None)
        # Narrowed admin sessions cannot delegate token creation
        token_teams = current_user.get("token_teams")
        if token_teams is not None:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Admin-delegated token creation requires un-narrowed admin access. Your session is narrowed to specific teams.",
            )

        logger.info(
            "Admin %s creating token for user %s",
            caller_email,
            target_user_email,
        )

    # Auto-inherit team_id from the caller's single team when not explicitly provided.
    # This prevents tokens from being silently scoped to public-only (team_id=None)
    # when the user belongs to exactly one team, maintaining RBAC context at token level.
    # Multi-team users must specify team_id explicitly to avoid ambiguity.
    # Admins with teams=null are exempt and may still create global-scope tokens.
    effective_team_id = request.team_id
    caller_token_teams = current_user.get("token_teams")
    # Only un-narrowed admins (token_teams=None) are exempt from auto-inheritance.
    # Narrowed admin sessions use the same team-scoping logic as non-admins.
    is_unrestricted_admin = current_user.get("is_admin") and caller_token_teams is None
    if effective_team_id is None and not is_unrestricted_admin:
        user_teams = caller_token_teams or []
        if len(user_teams) == 1:
            effective_team_id = user_teams[0]
            logger.debug("Auto-inherited team_id=%s for token creation by %s", effective_team_id, current_user["email"])

    service = TokenCatalogService(db)

    # CRITICAL: Always fetch caller_permissions for admin bypass check.
    # The service needs this to determine if the caller is an un-narrowed admin
    # who can bypass team membership requirements, regardless of whether a custom
    # scope is provided.
    caller_permissions = await _get_caller_permissions(db, current_user, effective_team_id)
    is_admin = current_user.get("is_admin", False)

    # Convert request to TokenScope if provided
    scope = None
    if request.scope:
        scope = TokenScope(
            server_id=request.scope.server_id,
            permissions=request.scope.permissions,
            ip_restrictions=request.scope.ip_restrictions,
            time_restrictions=request.scope.time_restrictions,
            usage_limits=request.scope.usage_limits,
        )

    try:
        token_record, raw_token = await service.create_token(
            user_email=target_user_email,
            name=request.name,
            description=request.description,
            scope=scope,
            expires_in_days=request.expires_in_days,
            tags=request.tags,
            team_id=effective_team_id,
            caller_permissions=caller_permissions,
            is_admin=is_admin,
            caller_token_teams=caller_token_teams,
            caller_token_teams_provided=True,
            is_active=request.is_active,
            caller_email=caller_email,
        )

        # Create TokenResponse for the token info
        token_response = TokenResponse(
            id=token_record.id,
            name=token_record.name,
            description=token_record.description,
            user_email=token_record.user_email,
            team_id=token_record.team_id,
            server_id=token_record.server_id,
            resource_scopes=token_record.resource_scopes or [],
            ip_restrictions=token_record.ip_restrictions or [],
            time_restrictions=token_record.time_restrictions or {},
            usage_limits=token_record.usage_limits or {},
            created_at=token_record.created_at,
            expires_at=token_record.expires_at,
            last_used=token_record.last_used,
            is_active=token_record.is_active,
            tags=token_record.tags or [],
        )

        db.commit()
        db.close()
        return TokenCreateResponse(
            token=token_response,
            access_token=raw_token,
        )
    except PublicValidationError as e:
        logger.error("Token creation validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
    except ValueError as e:
        logger.error("Token creation validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=safe_error_detail(e))
    except IntegrityError as e:
        db.rollback()
        err_str = str(e.orig) if hasattr(e, "orig") and e.orig else str(e)
        logger.error("Token creation integrity error: %s", SecurityValidator.sanitize_log_message(err_str))
        _handle_token_integrity_error(err_str)


@router.get("", response_model=TokenListResponse)
@require_permission("tokens.read")
async def list_tokens(
    include_inactive: bool = False,
    limit: int = 50,
    offset: int = 0,
    db: Session = Depends(get_db),
    current_user=Depends(get_current_user_with_permissions),
) -> TokenListResponse:
    """List API tokens for the current user.

    Args:
        include_inactive: Include inactive/expired tokens
        limit: Maximum number of tokens to return (default 50)
        offset: Number of tokens to skip for pagination
        current_user: Authenticated user from JWT
        db: Database session

    Returns:
        TokenListResponse: List of user's API tokens

    Examples:
        >>> import asyncio
        >>> asyncio.iscoroutinefunction(list_tokens)
        True
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)
    tokens = await service.list_user_and_team_tokens(
        user_email=current_user["email"],
        include_inactive=include_inactive,
        limit=limit,
        offset=offset,
    )

    total_count = await service.count_user_and_team_tokens(
        user_email=current_user["email"],
        include_inactive=include_inactive,
    )

    # Batch fetch revocation info (single query instead of N+1)
    revocation_map = await service.get_token_revocations_batch([t.jti for t in tokens])

    token_responses = []
    for token in tokens:
        revocation_info = revocation_map.get(token.jti)

        token_responses.append(
            TokenResponse(
                id=token.id,
                name=token.name,
                description=token.description,
                user_email=token.user_email,
                team_id=token.team_id,
                created_at=token.created_at,
                expires_at=token.expires_at,
                last_used=token.last_used,
                is_active=token.is_active,
                is_revoked=revocation_info is not None,
                revoked_at=revocation_info.revoked_at if revocation_info else None,
                revoked_by=revocation_info.revoked_by if revocation_info else None,
                revocation_reason=revocation_info.reason if revocation_info else None,
                tags=token.tags,
                server_id=token.server_id,
                resource_scopes=token.resource_scopes,
                ip_restrictions=token.ip_restrictions,
                time_restrictions=token.time_restrictions,
                usage_limits=token.usage_limits,
            )
        )

    db.commit()
    db.close()
    return TokenListResponse(tokens=token_responses, total=total_count, limit=limit, offset=offset)


@router.get("/{token_id}", response_model=TokenResponse)
@require_permission("tokens.read")
async def get_token(
    token_id: str,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenResponse:
    """Get details of a specific token.

    Args:
        token_id: Token ID to retrieve
        current_user: Authenticated user from JWT
        db: Database session

    Returns:
        TokenResponse: Token details

    Raises:
        HTTPException: If token not found or not owned by user

    Examples:
        >>> import asyncio
        >>> asyncio.iscoroutinefunction(get_token)
        True
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)
    token = await service.get_token(token_id, current_user["email"])

    if not token:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")

    db.commit()
    db.close()
    return TokenResponse(
        id=token.id,
        name=token.name,
        description=token.description,
        user_email=token.user_email,
        team_id=token.team_id,
        created_at=token.created_at,
        expires_at=token.expires_at,
        last_used=token.last_used,
        is_active=token.is_active,
        tags=token.tags,
        server_id=token.server_id,
        resource_scopes=token.resource_scopes,
        ip_restrictions=token.ip_restrictions,
        time_restrictions=token.time_restrictions,
        usage_limits=token.usage_limits,
    )


@router.put("/{token_id}", response_model=TokenResponse)
@require_permission("tokens.update")
async def update_token(
    token_id: str,
    request: TokenUpdateRequest,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenResponse:
    """Update an existing token.

    Args:
        token_id: Token ID to update
        request: Token update request
        current_user: Authenticated user from JWT
        db: Database session

    Returns:
        TokenResponse: Updated token details

    Raises:
        HTTPException: If token not found or validation fails
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)

    # For update, get caller permissions using token's team_id
    caller_permissions = None
    if request.scope and request.scope.permissions:
        # Get existing token to find its team_id
        existing_token = await service.get_token(token_id, current_user["email"])
        if not existing_token:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")
        # Use token's team_id for permission lookup
        caller_permissions = await _get_caller_permissions(db, current_user, existing_token.team_id)

    # Convert request to TokenScope if provided
    scope = None
    if request.scope:
        scope = TokenScope(
            server_id=request.scope.server_id,
            permissions=request.scope.permissions,
            ip_restrictions=request.scope.ip_restrictions,
            time_restrictions=request.scope.time_restrictions,
            usage_limits=request.scope.usage_limits,
        )

    try:
        token = await service.update_token(
            token_id=token_id,
            user_email=current_user["email"],
            name=request.name,
            description=request.description,
            scope=scope,
            tags=request.tags,
            caller_permissions=caller_permissions,
            is_active=request.is_active,
        )

        if not token:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")

        result = TokenResponse(
            id=token.id,
            name=token.name,
            description=token.description,
            user_email=token.user_email,
            team_id=token.team_id,
            created_at=token.created_at,
            expires_at=token.expires_at,
            last_used=token.last_used,
            is_active=token.is_active,
            tags=token.tags,
            server_id=token.server_id,
            resource_scopes=token.resource_scopes,
            ip_restrictions=token.ip_restrictions,
            time_restrictions=token.time_restrictions,
            usage_limits=token.usage_limits,
        )
        db.commit()
        db.close()
        return result
    except PublicValidationError as e:
        logger.error("Token update validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
    except ValueError as e:
        logger.error("Token update validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=safe_error_detail(e))


@router.delete("/{token_id}", status_code=status.HTTP_204_NO_CONTENT)
@require_permission("tokens.revoke")
async def revoke_token(
    token_id: str,
    request: Optional[TokenRevokeRequest] = None,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> None:
    """Revoke (delete) a token.

    Args:
        token_id: Token ID to revoke
        request: Optional revocation request with reason
        current_user: Authenticated user from JWT
        db: Database session

    Raises:
        HTTPException: If token not found
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)

    reason = request.reason if request else "Revoked by user"
    # SECURITY FIX: Pass user_email for ownership verification
    success = await service.revoke_token(
        token_id=token_id,
        user_email=current_user["email"],
        revoked_by=current_user["email"],
        reason=reason,
    )

    if not success:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")

    db.commit()
    db.close()


@router.get("/{token_id}/usage", response_model=TokenUsageStatsResponse)
@require_permission("tokens.read")
async def get_token_usage_stats(
    token_id: str,
    days: int = 30,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenUsageStatsResponse:
    """Get usage statistics for a specific token.

    Args:
        token_id: Token ID to get stats for
        days: Number of days to analyze (default 30)
        current_user: Authenticated user from JWT
        db: Database session

    Returns:
        TokenUsageStatsResponse: Token usage statistics

    Raises:
        HTTPException: If token not found or not owned by user
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)

    # Verify token ownership
    token = await service.get_token(token_id, current_user["email"])
    if not token:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")

    stats = await service.get_token_usage_stats(user_email=current_user["email"], token_id=token_id, days=days)

    db.commit()
    db.close()
    return TokenUsageStatsResponse(**stats)


# Admin endpoints for token oversight
@router.get("/admin/all", response_model=TokenListResponse, tags=["admin"])
async def list_all_tokens(
    user_email: Optional[str] = None,
    include_inactive: bool = False,
    limit: int = 100,
    offset: int = 0,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenListResponse:
    """Admin endpoint to list all tokens or tokens for a specific user.

    SECURITY: Requires un-narrowed admin access (token_teams must be None).
    Narrowed or public-only admin sessions must not access the token oversight surface.
    This prevents API tokens with team-scoped admin privileges from enumerating
    or managing tokens outside their authorized scope.

    Args:
        user_email: Filter tokens by user email (admin only)
        include_inactive: Include inactive/expired tokens
        limit: Maximum number of tokens to return
        offset: Number of tokens to skip
        current_user: Authenticated admin user
        db: Database session

    Returns:
        TokenListResponse: List of tokens

    Raises:
        HTTPException: If user is not admin or has narrowed token scope
    """
    _require_authenticated_session(current_user)

    if not current_user.get("is_admin"):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")

    # SECURITY: Require un-narrowed admin. Narrowed/public-only admin sessions
    # must not access the token oversight surface to prevent privilege escalation.
    token_teams = current_user.get("token_teams")
    if token_teams is not None:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Token oversight requires un-narrowed admin access")

    service = TokenCatalogService(db)

    if user_email:
        # Get tokens for specific user
        tokens = await service.list_user_tokens(
            user_email=user_email,
            include_inactive=include_inactive,
            limit=limit,
            offset=offset,
        )
        total_count = await service.count_user_tokens(
            user_email=user_email,
            include_inactive=include_inactive,
        )
    else:
        # Admin: get all tokens
        tokens = await service.list_all_tokens(
            include_inactive=include_inactive,
            limit=limit,
            offset=offset,
        )
        total_count = await service.count_all_tokens(
            include_inactive=include_inactive,
        )

    # Batch fetch revocation info (single query instead of N+1)
    revocation_map = await service.get_token_revocations_batch([t.jti for t in tokens])

    token_responses = []
    for token in tokens:
        revocation_info = revocation_map.get(token.jti)

        token_responses.append(
            TokenResponse(
                id=token.id,
                name=token.name,
                description=token.description,
                user_email=token.user_email,
                team_id=token.team_id,
                created_at=token.created_at,
                expires_at=token.expires_at,
                last_used=token.last_used,
                is_active=token.is_active,
                is_revoked=revocation_info is not None,
                revoked_at=revocation_info.revoked_at if revocation_info else None,
                revoked_by=revocation_info.revoked_by if revocation_info else None,
                revocation_reason=revocation_info.reason if revocation_info else None,
                tags=token.tags,
                server_id=token.server_id,
                resource_scopes=token.resource_scopes,
                ip_restrictions=token.ip_restrictions,
                time_restrictions=token.time_restrictions,
                usage_limits=token.usage_limits,
            )
        )

    db.commit()
    db.close()
    return TokenListResponse(tokens=token_responses, total=total_count, limit=limit, offset=offset)


@router.delete("/admin/{token_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["admin"])
async def admin_revoke_token(
    token_id: str,
    request: Optional[TokenRevokeRequest] = None,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> None:
    """Admin endpoint to revoke any token.

    SECURITY: Requires un-narrowed admin access (token_teams must be None).
    Narrowed or public-only admin sessions must not access the token oversight surface.
    This prevents API tokens with team-scoped admin privileges from revoking
    tokens outside their authorized scope.

    Args:
        token_id: Token ID to revoke
        request: Optional revocation request with reason
        current_user: Authenticated admin user
        db: Database session

    Raises:
        HTTPException: If user is not admin, has narrowed token scope, or token not found
    """
    _require_authenticated_session(current_user)

    if not current_user.get("is_admin"):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")

    # SECURITY: Require un-narrowed admin. Narrowed/public-only admin sessions
    # must not revoke arbitrary tokens to prevent privilege escalation.
    token_teams = current_user.get("token_teams")
    if token_teams is not None:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Token oversight requires un-narrowed admin access")

    service = TokenCatalogService(db)
    admin_email = current_user["email"]
    reason = request.reason if request else f"Revoked by admin {admin_email}"

    # Use admin method - no ownership check
    success = await service.admin_revoke_token(
        token_id=token_id,
        revoked_by=admin_email,
        reason=reason,
    )

    if not success:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Token not found")

    db.commit()
    db.close()


# Team-based token endpoints
@router.post("/teams/{team_id}", response_model=TokenCreateResponse, status_code=status.HTTP_201_CREATED)
@require_permission("tokens.create")
async def create_team_token(
    team_id: str,
    request: TokenCreateRequest,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenCreateResponse:
    """Create a new API token for a team.

    Team members and un-narrowed platform admins (is_admin=True with wildcard
    permissions) can create tokens. Narrowed admins and regular non-members
    still require active team membership.

    Args:
        team_id: Team ID to create token for
        request: Token creation request with name, description, scoping, etc.
        current_user: Authenticated user (must be active team member, or un-narrowed platform admin)
        db: Database session

    Returns:
        TokenCreateResponse: Created token details with raw token

    Raises:
        HTTPException: If user is not a team member (and admin bypass does not apply) or validation fails
    """
    _require_authenticated_session(current_user)

    caller_email = get_user_email(current_user)

    # Determine target user for token creation.
    # When the caller passes their own email (even with different casing),
    # use the canonical caller_email to avoid case-drift in the database.
    if request.user_email and request.user_email.lower() != caller_email.lower():
        target_user_email = request.user_email
    else:
        target_user_email = caller_email

    # If creating token for different user, require un-narrowed platform admin
    if request.user_email and request.user_email.lower() != caller_email.lower():
        # Require platform admin privilege
        if not current_user.get("is_admin"):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Admin access required to create tokens for other users",
            )

        # Require un-narrowed admin access (token_teams=None)
        # Narrowed admin sessions cannot delegate token creation
        token_teams = current_user.get("token_teams")
        if token_teams is not None:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Admin-delegated token creation requires un-narrowed admin access. Your session is narrowed to specific teams.",
            )

        logger.info(
            "Admin %s creating team token for user %s",
            caller_email,
            target_user_email,
        )

    service = TokenCatalogService(db)

    # CRITICAL: Always fetch caller_permissions for admin bypass check.
    # The service needs this to determine if the caller is an un-narrowed admin
    # who can bypass team membership requirements, regardless of whether a custom
    # scope is provided.
    caller_permissions = await _get_caller_permissions(db, current_user, team_id)
    is_admin = current_user.get("is_admin", False)
    caller_token_teams = current_user.get("token_teams")

    # Convert request to TokenScope if provided
    scope = None
    if request.scope:
        scope = TokenScope(
            server_id=request.scope.server_id,
            permissions=request.scope.permissions,
            ip_restrictions=request.scope.ip_restrictions,
            time_restrictions=request.scope.time_restrictions,
            usage_limits=request.scope.usage_limits,
        )

    try:
        token_record, raw_token = await service.create_token(
            user_email=target_user_email,
            name=request.name,
            description=request.description,
            scope=scope,
            expires_in_days=request.expires_in_days,
            tags=request.tags,
            team_id=team_id,
            caller_permissions=caller_permissions,
            is_admin=is_admin,
            caller_token_teams=caller_token_teams,
            caller_token_teams_provided=True,
            is_active=request.is_active,
            caller_email=caller_email,
        )

        # Create TokenResponse for the token info
        token_response = TokenResponse(
            id=token_record.id,
            name=token_record.name,
            description=token_record.description,
            user_email=token_record.user_email,
            team_id=token_record.team_id,
            server_id=token_record.server_id,
            resource_scopes=token_record.resource_scopes or [],
            ip_restrictions=token_record.ip_restrictions or [],
            time_restrictions=token_record.time_restrictions or {},
            usage_limits=token_record.usage_limits or {},
            created_at=token_record.created_at,
            expires_at=token_record.expires_at,
            last_used=token_record.last_used,
            is_active=token_record.is_active,
            tags=token_record.tags or [],
        )

        db.commit()
        db.close()
        return TokenCreateResponse(
            token=token_response,
            access_token=raw_token,
        )
    except PublicValidationError as e:
        logger.error("Team token creation validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
    except ValueError as e:
        logger.error("Team token creation validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=safe_error_detail(e))
    except IntegrityError as e:
        db.rollback()
        err_str = str(e.orig) if hasattr(e, "orig") and e.orig else str(e)
        logger.error("Team token creation integrity error: %s", SecurityValidator.sanitize_log_message(err_str))
        _handle_token_integrity_error(err_str)


@router.get("/teams/{team_id}", response_model=TokenListResponse)
@require_permission("tokens.read")
async def list_team_tokens(
    team_id: str,
    include_inactive: bool = False,
    limit: int = 50,
    offset: int = 0,
    current_user=Depends(get_current_user_with_permissions),
    db: Session = Depends(get_db),
) -> TokenListResponse:
    """List API tokens for a team.

    Team members and un-narrowed platform admins (is_admin=True with wildcard
    permissions) can list tokens. Narrowed admins and regular non-members
    still require active team membership.

    Args:
        team_id: Team ID to list tokens for
        include_inactive: Include inactive/expired tokens
        limit: Maximum number of tokens to return (default 50)
        offset: Number of tokens to skip for pagination
        current_user: Authenticated user (must be active team member, or un-narrowed platform admin)
        db: Database session

    Returns:
        TokenListResponse: List of team's API tokens

    Raises:
        HTTPException: If user is not a team member (and admin bypass does not apply)
    """
    _require_authenticated_session(current_user)

    service = TokenCatalogService(db)

    # Fetch caller permissions and admin status for potential admin bypass
    caller_permissions = await _get_caller_permissions(db, current_user, team_id)
    is_admin = current_user.get("is_admin", False)
    caller_token_teams = current_user.get("token_teams")

    try:
        tokens = await service.list_team_tokens(
            team_id=team_id,
            user_email=current_user["email"],
            include_inactive=include_inactive,
            limit=limit,
            offset=offset,
            caller_permissions=caller_permissions,
            is_admin=is_admin,
            caller_token_teams=caller_token_teams,
            caller_token_teams_provided=True,
        )

        total_count = await service.count_team_tokens(
            team_id=team_id,
            include_inactive=include_inactive,
        )

        # Batch fetch revocation info (single query instead of N+1)
        revocation_map = await service.get_token_revocations_batch([t.jti for t in tokens])

        token_responses = []
        for token in tokens:
            revocation_info = revocation_map.get(token.jti)

            token_responses.append(
                TokenResponse(
                    id=token.id,
                    name=token.name,
                    description=token.description,
                    user_email=token.user_email,
                    team_id=token.team_id,
                    created_at=token.created_at,
                    expires_at=token.expires_at,
                    last_used=token.last_used,
                    is_active=token.is_active,
                    is_revoked=revocation_info is not None,
                    revoked_at=revocation_info.revoked_at if revocation_info else None,
                    revoked_by=revocation_info.revoked_by if revocation_info else None,
                    revocation_reason=revocation_info.reason if revocation_info else None,
                    tags=token.tags,
                    server_id=token.server_id,
                    resource_scopes=token.resource_scopes,
                    ip_restrictions=token.ip_restrictions,
                    time_restrictions=token.time_restrictions,
                    usage_limits=token.usage_limits,
                )
            )

        db.commit()
        db.close()
        return TokenListResponse(tokens=token_responses, total=total_count, limit=limit, offset=offset)
    except PublicValidationError as e:
        logger.error("List team tokens validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
    except ValueError as e:
        logger.error("List team tokens validation error: %s", SecurityValidator.sanitize_log_message(str(e)))
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=safe_error_detail(e))
