"""FastAPI dependency providers.

All ``Depends()`` callables that inject shared resources (database
connection, settings, services, auth guard) are defined here.
Routers import directly from this module — never from ``app.state``
directly — to keep coupling explicit and testable.
"""

import time
from typing import Annotated, Protocol, cast

import aiosqlite
import structlog
from fastapi import Depends, HTTPException, Request, status

from app.config import Settings
from app.models.auth import Session
from app.utils.time_utils import utc_now

log: structlog.stdlib.BoundLogger = structlog.get_logger()


class AppState(Protocol):
    """Partial view of the FastAPI application state used by dependencies."""

    settings: Settings


_COOKIE_NAME = "bangui_session"

# ---------------------------------------------------------------------------
# Session validation cache
# ---------------------------------------------------------------------------

#: How long (seconds) a validated session token is served from the in-memory
#: cache without re-querying SQLite.  Eliminates repeated DB lookups for the
#: same token arriving in near-simultaneous parallel requests.
_SESSION_CACHE_TTL: float = 10.0

#: ``token → (Session, cache_expiry_monotonic_time)``
_session_cache: dict[str, tuple[Session, float]] = {}


def clear_session_cache() -> None:
    """Flush the entire in-memory session validation cache.

    Useful in tests to prevent stale state from leaking between test cases.
    """
    _session_cache.clear()


def invalidate_session_cache(token: str) -> None:
    """Evict *token* from the in-memory session cache.

    Must be called during logout so the revoked token is no longer served
    from cache without a DB round-trip.

    Args:
        token: The session token to remove.
    """
    _session_cache.pop(token, None)


async def get_db(request: Request) -> aiosqlite.Connection:
    """Provide the shared :class:`aiosqlite.Connection` from ``app.state``.

    Args:
        request: The current FastAPI request (injected automatically).

    Returns:
        The application-wide aiosqlite connection opened during startup.

    Raises:
        HTTPException: 503 if the database has not been initialised.
    """
    db: aiosqlite.Connection | None = getattr(request.app.state, "db", None)
    if db is None:
        log.error("database_not_initialised")
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="Database is not available.",
        )
    return db


async def get_settings(request: Request) -> Settings:
    """Provide the :class:`~app.config.Settings` instance from ``app.state``.

    Args:
        request: The current FastAPI request (injected automatically).

    Returns:
        The application settings loaded at startup.
    """
    state = cast("AppState", request.app.state)
    return state.settings


async def require_auth(
    request: Request,
    db: Annotated[aiosqlite.Connection, Depends(get_db)],
) -> Session:
    """Validate the session token and return the active session.

    The token is read from the ``bangui_session`` cookie or the
    ``Authorization: Bearer`` header.

    Validated tokens are cached in memory for :data:`_SESSION_CACHE_TTL`
    seconds so that concurrent requests sharing the same token avoid repeated
    SQLite round-trips.  The cache is bypassed on expiry and explicitly
    cleared by :func:`invalidate_session_cache` on logout.

    Args:
        request: The incoming FastAPI request.
        db: Injected aiosqlite connection.

    Returns:
        The active :class:`~app.models.auth.Session`.

    Raises:
        HTTPException: 401 if no valid session token is found.
    """
    from app.services import auth_service  # noqa: PLC0415

    token: str | None = request.cookies.get(_COOKIE_NAME)
    if not token:
        auth_header: str = request.headers.get("Authorization", "")
        if auth_header.startswith("Bearer "):
            token = auth_header[len("Bearer "):]

    if not token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication required.",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # Fast path: serve from in-memory cache when the entry is still fresh and
    # the session itself has not yet exceeded its own expiry time.
    cached = _session_cache.get(token)
    if cached is not None:
        session, cache_expires_at = cached
        if time.monotonic() < cache_expires_at and session.expires_at > utc_now().isoformat():
            return session
        # Stale cache entry — evict and fall through to DB.
        _session_cache.pop(token, None)

    try:
        session = await auth_service.validate_session(db, token)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=str(exc),
            headers={"WWW-Authenticate": "Bearer"},
        ) from exc

    _session_cache[token] = (session, time.monotonic() + _SESSION_CACHE_TTL)
    return session


# Convenience type aliases for route signatures.
DbDep = Annotated[aiosqlite.Connection, Depends(get_db)]
SettingsDep = Annotated[Settings, Depends(get_settings)]
AuthDep = Annotated[Session, Depends(require_auth)]
