"""Department-scoped request context (U4 — Admin Console).

This module provides the FastAPI dependency :func:`get_department_context`
that resolves the current user's department membership from the auth DB
and returns a :class:`DepartmentContext` describing which departments
the request should be scoped to.

The dependency is intentionally *per-route* (via ``Depends()``) rather
than a global middleware. This keeps the department lookup close to the
routes that actually need it (skills, KB, usage) and avoids paying the
DB round-trip for whitelisted paths (``/health``, ``/docs``, etc.).

Admin users (``role == "admin"``) bypass department filtering entirely
— :class:`DepartmentContext` is returned with ``is_admin=True`` and an
empty ``department_ids`` list, signalling to the filtering helpers in
:mod:`agentkit.server.admin.filtering` that no scoping should be
applied.

Skills/KB with NO department binding are *global* (visible to all
users, including unauthenticated API-key clients). The filtering
helpers in :mod:`agentkit.server.admin.filtering` are responsible for
preserving this global-visibility invariant.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import aiosqlite
from fastapi import HTTPException, Request

from agentkit.server.auth.models import DEFAULT_AUTH_DB_PATH

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Dataclass
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class DepartmentContext:
    """Request-scoped department context.

    Attributes:
        user_id: The authenticated user's id, or ``None`` for API-key
            clients / unauthenticated requests.
        department_ids: The user's department ids (union of all
            ``user_departments`` rows). Empty for admins (who bypass
            filtering) and for users with no department assignments.
        is_admin: ``True`` if the user has the ``admin`` role. Admins
            bypass department filtering — they see all resources.
    """

    user_id: str | None = None
    department_ids: list[str] = field(default_factory=list)
    is_admin: bool = False

    @property
    def should_filter(self) -> bool:
        """Return ``True`` if the caller should be department-filtered.

        Admins and unauthenticated callers (no user_id) are NOT
        filtered — admins see everything, and unauthenticated callers
        only see global resources (the filtering helpers handle this by
        returning only the global set when ``department_ids`` is empty
        and ``is_admin`` is False).
        """
        return not self.is_admin


# ---------------------------------------------------------------------------
# DB path resolution (mirrors routes.admin._resolve_db_path)
# ---------------------------------------------------------------------------


def _resolve_db_path(request: Request) -> Path:
    """Resolve the auth DB path from ``app.state`` or the default."""
    path = getattr(request.app.state, "auth_db_path", None)
    return Path(path) if path else DEFAULT_AUTH_DB_PATH


# ---------------------------------------------------------------------------
# Department-id lookup
# ---------------------------------------------------------------------------


async def _fetch_user_department_ids(db_path: Path, user_id: str) -> list[str]:
    """Return the user's department ids from ``user_departments``.

    Only *active* departments are included — if a department is
    disabled (``is_active=0``), its bindings no longer grant access to
    its resources, so we drop it from the user's effective department
    set here. This matches the plan's "disabled department → users
    cannot access department resources" rule.
    """
    async with aiosqlite.connect(str(db_path)) as db:
        cursor = await db.execute(
            "SELECT ud.department_id "
            "FROM user_departments ud "
            "INNER JOIN departments d ON d.id = ud.department_id "
            "WHERE ud.user_id = ? AND d.is_active = 1 "
            "ORDER BY ud.department_id ASC",
            (user_id,),
        )
        rows = await cursor.fetchall()
    return [row[0] for row in rows]


# ---------------------------------------------------------------------------
# FastAPI dependencies
# ---------------------------------------------------------------------------


async def get_department_context(request: Request) -> DepartmentContext:
    """Resolve the :class:`DepartmentContext` for the current request.

    Reads ``request.state.current_user`` (set by :class:`AuthMiddleware`):

    - **Admin** (``role == "admin"``): returns
      ``DepartmentContext(user_id, [], is_admin=True)`` — admins bypass
      filtering.
    - **API-key client / unauthenticated** (``user_id is None``):
      returns ``DepartmentContext(None, [], is_admin=False)`` — the
      filtering helpers will return only global resources.
    - **Regular user**: queries ``user_departments`` for the user's
      active department ids and returns
      ``DepartmentContext(user_id, department_ids, is_admin=False)``.

    If ``request.state.current_user`` is missing entirely (e.g. the
    auth middleware was not installed), returns an empty context
    equivalent to the unauthenticated case.

    Fail-closed (KTD-1): if the DB lookup fails for a regular user,
    raises ``HTTPException(503)``. Returning an empty list would make
    quota enforcement skip the check (fail-open), which is a security
    bug. Admins and API-key clients are unaffected (no DB lookup).
    """
    current_user: dict[str, Any] | None = getattr(request.state, "current_user", None)
    if current_user is None:
        return DepartmentContext(user_id=None, department_ids=[], is_admin=False)

    user_id = current_user.get("user_id")
    role = current_user.get("role")

    # Admins bypass department filtering.
    if role == "admin":
        return DepartmentContext(
            user_id=user_id,
            department_ids=[],
            is_admin=True,
        )

    # API-key clients have user_id=None — they see only global resources.
    if not user_id:
        return DepartmentContext(user_id=None, department_ids=[], is_admin=False)

    # Regular user: look up their active department ids.
    # Fail-closed on DB errors — returning an empty list would bypass
    # quota enforcement (fail-open), which is a security vulnerability.
    db_path = _resolve_db_path(request)
    try:
        department_ids = await _fetch_user_department_ids(db_path, user_id)
    except Exception:
        logger.exception(
            "Failed to fetch department ids for user %s — fail-closed (503)",
            user_id,
        )
        raise HTTPException(
            status_code=503,
            detail={
                "error": "department_lookup_failed",
                "detail": "Cannot verify department membership — refusing request",
            },
        ) from None

    return DepartmentContext(
        user_id=user_id,
        department_ids=department_ids,
        is_admin=False,
    )


async def get_department_context_optional(request: Request) -> DepartmentContext | None:
    """Optional variant: returns ``None`` if no ``current_user`` is set.

    Use this on routes where auth is optional (e.g. public skill
    listing) — the route can short-circuit to "global only" when the
    context is ``None``.
    """
    current_user: dict[str, Any] | None = getattr(request.state, "current_user", None)
    if current_user is None:
        return None
    return await get_department_context(request)
