"""
Auth context for the API.

Real, self-contained JWT auth: app/routers/auth.py issues a signed token
(app/core/auth.py::create_access_token) after verifying an email+password
against users.password_hash; every other route depends on get_auth_context
below, which requires and verifies that token via the same module. There is
no more header-trust path — a caller without a valid, unexpired,
correctly-signed token gets a 401 regardless of what headers it sends.

Every tenant-scoped table is additionally protected by Postgres Row-Level
Security keyed on the tenant_id/vendor_id this module extracts from the
token (see db/schema.sql), so this is defense in depth, not the only layer.
"""

import uuid
from dataclasses import dataclass

from fastapi import Header, HTTPException, status

from app.core import observability
from app.core.auth import TokenError, decode_access_token


@dataclass(frozen=True)
class AuthContext:
    tenant_id: uuid.UUID
    user_id: uuid.UUID | None
    vendor_id: uuid.UUID | None
    # How this session signed in: "pwd", "otp" (a second factor), "sso".
    amr: tuple[str, ...] = ()
    # The sign-in this token belongs to (app/crud/sessions.py), and when the
    # token was issued (seconds since the epoch). None for tokens made
    # without a sign-in, such as the test suite's.
    session_id: uuid.UUID | None = None
    issued_at: int | None = None


@dataclass(frozen=True)
class PlatformAdminContext:
    admin_id: uuid.UUID


def _parse_uuid(value: str, field: str) -> uuid.UUID:
    try:
        return uuid.UUID(value)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=f"{field} must be a UUID") from exc


async def get_auth_context(
    authorization: str | None = Header(None),
) -> AuthContext:
    if not authorization or not authorization.lower().startswith("bearer "):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing or malformed Authorization header — expected 'Bearer <token>'.",
            headers={"WWW-Authenticate": "Bearer"},
        )
    token = authorization.split(" ", 1)[1].strip()
    try:
        claims = decode_access_token(token)
    except TokenError as exc:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired session — please sign in again.",
            headers={"WWW-Authenticate": "Bearer"},
        ) from exc

    if "platform_admin_id" in claims or "purpose" in claims or "tenant_id" not in claims:
        # A Super Admin token, or a single-purpose one (e.g. the SMS
        # first-login code challenge — app/crud/user_verification.py),
        # replayed against a regular route — reject rather than silently
        # ignoring the extra claim or failing on the missing one.
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired session — please sign in again.",
            headers={"WWW-Authenticate": "Bearer"},
        )

    vendor_id_claim = claims.get("vendor_id")
    ctx = AuthContext(
        tenant_id=_parse_uuid(claims["tenant_id"], "tenant_id"),
        user_id=_parse_uuid(claims["user_id"], "user_id") if claims.get("user_id") else None,
        vendor_id=_parse_uuid(vendor_id_claim, "vendor_id") if vendor_id_claim else None,
        amr=tuple(claims.get("amr") or ()),
        session_id=_parse_uuid(claims["sid"], "sid") if claims.get("sid") else None,
        issued_at=int(claims["iat"]) if isinstance(claims.get("iat"), (int, float)) else None,
    )
    # Every log line from here on in this request names who it was for.
    observability.bind(tenant_id=ctx.tenant_id, user_id=ctx.user_id, vendor_id=ctx.vendor_id)
    return ctx


async def get_platform_admin_context(
    authorization: str | None = Header(None),
) -> PlatformAdminContext:
    """Mirrors get_auth_context above, but for the separate Super Admin
    token shape (create_platform_admin_token) — requires "platform_admin_id"
    and rejects any token carrying "tenant_id", so a regular session token
    can never be replayed against an admin-only route either."""
    if not authorization or not authorization.lower().startswith("bearer "):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing or malformed Authorization header — expected 'Bearer <token>'.",
            headers={"WWW-Authenticate": "Bearer"},
        )
    token = authorization.split(" ", 1)[1].strip()
    try:
        claims = decode_access_token(token)
    except TokenError as exc:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired session — please sign in again.",
            headers={"WWW-Authenticate": "Bearer"},
        ) from exc

    if "tenant_id" in claims or "platform_admin_id" not in claims:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired session — please sign in again.",
            headers={"WWW-Authenticate": "Bearer"},
        )

    return PlatformAdminContext(admin_id=_parse_uuid(claims["platform_admin_id"], "platform_admin_id"))
