"""
JWT verification for Supabase auth tokens (ES256 + JWKS).

How it works:
1. Frontend sends `Authorization: Bearer <jwt>` on every authenticated request.
2. get_current_user() extracts the JWT, looks up the signing key in the cached
   JWKS, verifies the ES256 signature, checks expiration, and returns a
   CurrentUser object with user_id (sub) and email.
3. The JWKS is fetched once from Supabase and cached for 1 hour to avoid an
   HTTP round-trip on every request.

Usage in endpoints:
    from fastapi import Depends
    from app.core.security import get_current_user, CurrentUser

    @router.get("/me")
    async def me(user: CurrentUser = Depends(get_current_user)):
        return {"user_id": user.user_id, "email": user.email}
"""
import time
from typing import Optional

import httpx
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jwt import PyJWKClient
from pydantic import BaseModel

from app.core.config import settings
from app.core.supabase_client import supabase


# ----------------------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------------------

JWKS_URL = f"{settings.SUPABASE_URL}/auth/v1/.well-known/jwks.json"
JWT_AUDIENCE = "authenticated"  # Supabase sets aud='authenticated' for logged-in users
CACHE_TTL_SECONDS = 3600  # 1 hour


# ----------------------------------------------------------------------------
# JWKS cache (in-memory, module-level)
# ----------------------------------------------------------------------------

_jwks_client: Optional[PyJWKClient] = None
_jwks_fetched_at: float = 0.0


def _get_jwks_client() -> PyJWKClient:
    """
    Return a cached PyJWKClient. Refresh after CACHE_TTL_SECONDS.
    PyJWKClient itself caches keys internally but we wrap it with our own
    TTL to force periodic refresh (in case Supabase rotates keys).
    """
    global _jwks_client, _jwks_fetched_at
    now = time.time()
    if _jwks_client is None or (now - _jwks_fetched_at) > CACHE_TTL_SECONDS:
        _jwks_client = PyJWKClient(JWKS_URL, cache_keys=True, lifespan=CACHE_TTL_SECONDS)
        _jwks_fetched_at = now
    return _jwks_client


# ----------------------------------------------------------------------------
# Pydantic model returned to endpoints
# ----------------------------------------------------------------------------

class CurrentUser(BaseModel):
    """Verified user identity extracted from a valid JWT."""
    user_id: str
    email: Optional[str] = None
    role: Optional[str] = None  # 'authenticated', 'anon', etc. (Supabase JWT claim)


# ----------------------------------------------------------------------------
# FastAPI dependency
# ----------------------------------------------------------------------------

_bearer_scheme = HTTPBearer(auto_error=False)


def _unauthorized(detail: str) -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=detail,
        headers={"WWW-Authenticate": "Bearer"},
    )


def get_current_user(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(_bearer_scheme),
) -> CurrentUser:
    """
    FastAPI dependency. Verifies the Authorization Bearer JWT and returns a
    CurrentUser. Raises HTTP 401 on any failure (missing, invalid, expired).
    """
    if credentials is None or not credentials.credentials:
        raise _unauthorized("Missing Authorization header")

    token = credentials.credentials

    try:
        jwks_client = _get_jwks_client()
        signing_key = jwks_client.get_signing_key_from_jwt(token).key

        payload = jwt.decode(
            token,
            signing_key,
            algorithms=["ES256"],
            audience=JWT_AUDIENCE,
            options={"require": ["exp", "sub"]},
            # leeway=10 : tolérance d'horloge pour absorber un léger désync
            # entre l'horloge locale et les serveurs Supabase (évite un rejet
            # immédiat d'un token fraîchement émis)
            leeway=10,
        )
    except jwt.ExpiredSignatureError:
        raise _unauthorized("Token has expired")
    except jwt.InvalidAudienceError:
        raise _unauthorized("Invalid token audience")
    except jwt.InvalidTokenError as e:
        raise _unauthorized(f"Invalid token: {str(e)}")
    except httpx.HTTPError as e:
        # JWKS fetch failed (Supabase unreachable). Treat as auth failure but log distinctly.
        raise _unauthorized(f"Could not fetch JWKS: {str(e)}")

    user_id = payload.get("sub")
    if not user_id:
        raise _unauthorized("Token missing 'sub' claim")

    return CurrentUser(
        user_id=user_id,
        email=payload.get("email"),
        role=payload.get("role"),
    )


# ----------------------------------------------------------------------------
# Admin authorization dependency
# ----------------------------------------------------------------------------

def _forbidden() -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="Forbidden",
    )


def require_admin(
    current_user: CurrentUser = Depends(get_current_user),
) -> CurrentUser:
    """
    FastAPI dependency. Verifies the authenticated user has `role = 'admin'`
    in the `public.profiles` table.

    Chain: get_current_user (JWT valid) -> require_admin (role check).

    Returns the same CurrentUser unchanged if admin.
    Raises:
        - HTTP 401 (from get_current_user) if JWT invalid/missing/expired.
        - HTTP 403 'Forbidden' if user is authenticated but not admin,
          if the profile row is missing, or if the BDD lookup fails.

    Note: We re-fetch role on every call (no cache). This is intentional
    for MVP simplicity and to keep BDD as the source of truth. Latency
    overhead is ~30ms which is acceptable given low admin endpoint volume.
    """
    try:
        resp = (
            supabase.table("profiles")
            .select("role")
            .eq("id", current_user.user_id)
            .single()
            .execute()
        )
    except Exception:
        # Any BDD error (network, missing row via .single(), etc.) -> 403.
        # We log nothing here on purpose: an audit log layer can be added later
        # without leaking diagnostics in the response.
        raise _forbidden()

    data = resp.data
    if not data or data.get("role") != "admin":
        raise _forbidden()

    return current_user
