"""Bearer-token auth path for CLI / agent callers (non-browser clients).

Browsers authenticate with Pattern B grants (X-Auth-Grant, verified by
``AuthMiddleware`` in ``app/auth.py``). CLI tools and agents — the diff
harness hitting ``GET /api/export``, backup scripts pulling the media
manifest — cannot mint grants, so they present a static token instead::

    Authorization: Bearer <AGENT_API_TOKEN>

Security properties (all load-bearing):

- The token lives ONLY in the environment (``AGENT_API_TOKEN``, set via the
  mode-600 ``.env``). It is deliberately NOT in ``app/config.py``'s Settings
  so it never appears in ``secret_status()`` plumbing or generated clients;
  this module reads ``os.environ`` directly at call time (never import time,
  so tests can monkeypatch and rotation needs no code reload).
- Comparison is ``hmac.compare_digest`` — constant-time, no timing oracle.
- An UNSET or empty ``AGENT_API_TOKEN`` means the agent path is CLOSED
  (misconfiguration = hard close, never an open door — same rule as
  ``AuthMiddleware``'s missing-secret behavior).
- The token is only honored on the EXPLICIT path allowlist
  ``AGENT_TOKEN_PATHS`` (exact matches, no prefixes) — per the plan, agent
  reachability is opt-in per route, never broad.

INTEGRATION (for the coordinator wiring app/auth.py)
=====================================================

``AuthMiddleware`` is default-deny and runs BEFORE route dependencies, so
the route-level dependency below can never fire for a bare Bearer request
until the middleware learns about this path. The wiring is two lines in
``AuthMiddleware.dispatch`` — insert AFTER the ``_is_public(path)`` check
and BEFORE the grant-header check::

    from .services.agent_tokens import is_agent_token_request
    ...
    if is_agent_token_request(path, request.headers.get("Authorization", "")):
        # Valid agent token on an allowlisted path. request.state.auth_user
        # stays UNSET — downstream code distinguishes agent calls from user
        # calls by its absence, and the route-level dependency
        # require_grant_or_agent_token() re-validates (defense in depth).
        return await call_next(request)

Nothing else changes:

- ``tests/test_auth.py``'s walk-every-route default-deny check stays green —
  its bare requests carry no Authorization header, so
  ``is_agent_token_request`` returns False and the 401 path is unchanged.
- Do NOT add these paths to ``PUBLIC_PREFIXES``/``PUBLIC_EXACT`` — they are
  not public; they are token-gated.
- New agent-reachable routes are added by (a) appending the EXACT path to
  ``AGENT_TOKEN_PATHS`` here and (b) adding
  ``Depends(require_grant_or_agent_token)`` on the route/router. Both, always.
"""
from __future__ import annotations

import hmac
import os

from starlette.requests import Request

from ..errors import AppError

AGENT_TOKEN_ENV = "AGENT_API_TOKEN"

# Exact request paths where a valid agent token is accepted in lieu of a
# grant. EXACT string matches only — no prefixes, mirroring the "explicit
# allowlist, reviewed in tests" discipline of auth.py.
AGENT_TOKEN_PATHS: tuple[str, ...] = (
    "/api/export",
    "/api/export/media-manifest",
)

# --- error codes (this domain) ---
AUTH_AGENT_TOKEN_INVALID = "auth_agent_token_invalid"


def _configured_token() -> str:
    """The expected token, read from the environment at call time.

    Empty/unset means the agent path is closed (never an open door).
    """
    return os.environ.get(AGENT_TOKEN_ENV, "")


def _bearer_token(authorization_header: str) -> str:
    """Extract the token from an ``Authorization: Bearer <token>`` header.

    Returns "" for any other scheme or a malformed header.
    """
    if not authorization_header:
        return ""
    scheme, _, token = authorization_header.partition(" ")
    if scheme.lower() != "bearer":
        return ""
    return token.strip()


def agent_token_valid(presented: str) -> bool:
    """Constant-time comparison against AGENT_API_TOKEN. Unset env => False."""
    expected = _configured_token()
    if not expected or not presented:
        return False
    return hmac.compare_digest(expected, presented)


def is_agent_token_request(path: str, authorization_header: str) -> bool:
    """The AuthMiddleware hook: does this request carry a valid agent token
    for an explicitly agent-reachable path?

    Pure function of (path, header) — no request state — so it is trivially
    unit-testable and safe to call before any auth context exists.
    """
    if path not in AGENT_TOKEN_PATHS:
        return False
    return agent_token_valid(_bearer_token(authorization_header))


def require_grant_or_agent_token(request: Request) -> None:
    """Route dependency: pass if EITHER a Pattern B grant authenticated this
    request (``request.state.auth_user`` set by AuthMiddleware) OR a valid
    ``Authorization: Bearer <AGENT_API_TOKEN>`` header is present.

    Applied to the export routes only (explicit allowlist). This is the
    second half of a two-layer check: AuthMiddleware admits the request
    (grant, or agent token via ``is_agent_token_request``), then this
    dependency re-validates at the route so a future middleware regression
    cannot silently expose the route.
    """
    if getattr(request.state, "auth_user", None) is not None:
        return
    if agent_token_valid(_bearer_token(request.headers.get("Authorization", ""))):
        return
    raise AppError(
        AUTH_AGENT_TOKEN_INVALID,
        "requires an authenticated user grant or a valid agent bearer token",
        401,
    )
