"""
cortex/api/deps.py — Shared dependencies for the Cortex HTTP API.

Provides:
    - verify_api_key: FastAPI dependency for authentication (JWT or API key)
    - get_current_user: FastAPI dependency returning full AuthResult
    - get_plugin: FastAPI dependency for accessing the shared CortexPlugin
    - _serialise: Recursive JSON serialisation helper
    - PREFIX: API version prefix constant
    - plugin_holder: Shared mutable dict for the plugin instance
    - auth_holder: Shared mutable dict for the AuthProvider singleton
"""

from __future__ import annotations

import hmac
import logging
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

try:
    from fastapi import Depends, Header, HTTPException, Request, status
    _FASTAPI_AVAILABLE = True
except ImportError:
    _FASTAPI_AVAILABLE = False

# API version prefix
PREFIX = "/api/v1"

# Shared plugin instance holder (populated at startup by create_app lifespan)
plugin_holder: Dict[str, Any] = {}

# Shared auth provider holder (populated at app creation by create_app)
auth_holder: Dict[str, Any] = {}

# Shared DBManager holder (populated in hosted mode by create_app lifespan)
db_manager_holder: Dict[str, Any] = {}

# Shared StatsDB holder (populated in hosted mode by create_app lifespan)
stats_db_holder: Dict[str, Any] = {}


def get_plugin_holder() -> Dict[str, Any]:
    """Return the shared plugin holder dict."""
    return plugin_holder


def _serialise(obj: Any) -> Any:
    """Recursively convert objects to JSON-serialisable types."""
    if obj is None or isinstance(obj, (bool, int, float, str)):
        return obj
    if isinstance(obj, dict):
        return {k: _serialise(v) for k, v in obj.items() if not k.startswith("_")}
    if isinstance(obj, (list, tuple)):
        return [_serialise(item) for item in obj]
    if hasattr(obj, "__dict__"):
        return _serialise({k: v for k, v in obj.__dict__.items() if not k.startswith("_")})
    return str(obj)


def make_auth_dependency(
    api_key: Optional[str],
    auth_provider: Optional[Any] = None,
    require_auth: bool = False,
    public_endpoints: Optional[list] = None,
):
    """
    Create the verify_api_key FastAPI dependency.

    When *auth_provider* is provided (an :class:`~cortex.api.auth.AuthProvider`),
    JWT-first authentication is used.  The legacy API-key-only path is kept as
    a fallback for backward compatibility.

    Args:
        api_key:         Legacy API key string (used when auth_provider is None).
        auth_provider:   Optional :class:`~cortex.api.auth.AuthProvider` instance.
                         When set, JWT + API key auth is used.
        require_auth:    When True, open-access requests are rejected with 401.
        public_endpoints: List of path prefixes that bypass auth entirely.
    """
    _public = set(public_endpoints or [])

    if auth_provider is not None:
        # New JWT-first auth path
        from cortex.api.auth import AuthError

        async def verify_api_key(request: Request):  # type: ignore[misc]
            """JWT or API key authentication — JWT takes priority."""
            path = request.url.path
            # Public endpoints bypass auth
            if any(path == ep or path.startswith(ep.rstrip("/") + "/") for ep in _public):
                return None

            try:
                result = auth_provider.authenticate(request.headers)
            except AuthError as exc:
                # Log failed auth: IP + reason, never the token/key (#542)
                client_ip = request.client.host if request.client else "unknown"
                logger.warning(
                    "Auth failed from %s on %s: %s",
                    client_ip, path, str(exc),
                )
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Authentication failed",
                ) from exc

            # Reject open access when require_auth is enabled
            if require_auth and result.method == "open":
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Authentication required",
                )

            # Expose identity to downstream handlers
            request.state.auth = result
            request.state.owner_id = result.owner_id
            return result

    else:
        # Legacy API key–only path (backward compat for tests and self-host)
        async def verify_api_key(  # type: ignore[misc]
            request: Request,
            x_api_key: Optional[str] = Header(None),
        ):
            """Validate X-API-Key header when auth is configured."""
            path = request.url.path
            if any(path == ep or path.startswith(ep.rstrip("/") + "/") for ep in _public):
                return None
            if api_key is None:
                return None
            if x_api_key is None or not hmac.compare_digest(x_api_key, api_key):
                raise HTTPException(
                    status_code=status.HTTP_401_UNAUTHORIZED,
                    detail="Invalid or missing X-API-Key",
                )

    return verify_api_key


async def get_current_user(request: "Request") -> Any:
    """
    FastAPI dependency that returns the authenticated user for the current request.

    Reads the :class:`~cortex.api.auth.AuthResult` stored on ``request.state.auth``
    by the :func:`make_auth_dependency` gate.  If not set (e.g. public endpoint or
    no-auth mode), returns an open-access result with ``owner_id='default'``.

    Returns:
        :class:`~cortex.api.auth.AuthResult`
    """
    auth = getattr(request.state, "auth", None)
    if auth is not None:
        return auth

    # Fallback: run auth now (handles routes that don't use verify_api_key gate)
    provider = auth_holder.get("auth_provider")
    if provider is not None:
        from cortex.api.auth import AuthError, AuthResult
        try:
            result = provider.authenticate(request.headers)
        except AuthError:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Authentication required",
            )
        request.state.auth = result
        request.state.owner_id = result.owner_id
        return result

    # No auth configured — return open access result
    from cortex.api.auth import AuthResult
    return AuthResult(owner_id="default", method="open")


def get_plugin() -> Any:
    """Retrieve the shared CortexPlugin instance."""
    plugin = plugin_holder.get("plugin")
    if plugin is None:  # pragma: no cover
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Cortex plugin not initialised",
        )
    return plugin


async def get_user_storage(owner_id: str) -> Any:
    """
    FastAPI/async helper: resolve the correct SQLiteAdapter for *owner_id*.

    Hosted mode  (DBManager active) → ``await db_manager.get_adapter(owner_id)``
    Self-host mode (no DBManager)   → the plugin's single shared storage

    Args:
        owner_id: User identifier (e.g. from ``request.state.owner_id``).

    Returns:
        A :class:`~cortex.storage.sqlite_adapter.SQLiteAdapter` instance.

    Raises:
        HTTPException 503 if neither DBManager nor plugin storage is available.
    """
    manager = db_manager_holder.get("manager")
    if manager is not None:
        return await manager.get_adapter(owner_id)

    # Self-host fallback: use the plugin's single storage
    plugin = plugin_holder.get("plugin")
    if plugin is not None:
        storage = getattr(plugin, "storage", None)
        if storage is not None:
            return storage

    raise HTTPException(  # pragma: no cover
        status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
        detail="Storage not available",
    )
