"""
middleware/auth_middleware.py
=============================
Hybrid auth dependency for FastAPI routes.

Auth precedence:
    1. `Authorization: Bearer <jwt>`  → preferred path; JWT minted by
       /api/auth/login (local DB participant lookup).
    2. `X-Access-Key: <access_key>`   → graceful fallback used after the
       external People Hub login succeeds. Resolves the access key to a
       participant row via the local DB.

The fallback exists because the candidate-portal landing page authenticates
against the external People Hub API and never round-trips through
/api/auth/login. Without this fallback every JWT-protected route (the
entire coding module) errors with "session expired" / 401 even though the
candidate is fully authenticated upstream — that was the root cause of the
visible "Session Expired" toast on the coding screen.

Security notes:
    • The access-key fallback still requires a successful DB lookup, so a
      forged value goes nowhere.
    • All code paths return a *normalized* candidate dict; routes never
      see raw DB rows or raw JWT claims.
    • Failures return a single opaque "Authentication required" message —
      we never tell an attacker which path failed.
"""
from __future__ import annotations

import re

from fastapi import HTTPException, Request, status

from services.auth_service import decode_token, get_candidate
from services import participant_repo
from utils.logger import get_logger

log = get_logger(__name__)


# Mirrors the validator on the external proxy so the two surfaces agree.
_ACCESS_KEY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{1,63}$")


def _bearer_token(request: Request) -> str:
    """Extract the JWT from the Authorization header without raising."""
    raw = request.headers.get("Authorization") or request.headers.get("authorization") or ""
    if not raw:
        return ""
    parts = raw.split(None, 1)
    if len(parts) != 2 or parts[0].lower() != "bearer":
        return ""
    return parts[1].strip()


def _candidate_from_jwt(token: str) -> dict | None:
    if not token:
        return None
    payload = decode_token(token)
    if not payload or not payload.get("sub"):
        return None
    return get_candidate(payload["sub"])


def _candidate_from_access_key(access_key: str) -> dict | None:
    key = (access_key or "").strip()
    if not key or not _ACCESS_KEY_RE.fullmatch(key):
        return None
    try:
        return participant_repo.find_by_access_key(key)
    except Exception as exc:  # noqa: BLE001
        # DB outage shouldn't leak details to the caller.
        log.warning("access_key fallback lookup failed: %s", exc)
        return None


def get_current_candidate(request: Request) -> dict:
    """FastAPI dependency. Resolves the candidate via JWT *or* access key.

    Routes use this by declaring a dependency:
        candidate: dict = Depends(get_current_candidate)
    """
    candidate = _candidate_from_jwt(_bearer_token(request))
    if candidate:
        return candidate

    access_key = (
        request.headers.get("X-Access-Key")
        or request.headers.get("x-access-key")
        or ""
    )
    candidate = _candidate_from_access_key(access_key)
    if candidate:
        return candidate

    # Single opaque failure message regardless of which path failed.
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Authentication required. Please log in again.",
        headers={"WWW-Authenticate": "Bearer"},
    )
