"""The single place identity is resolved. Routers never read headers themselves.

**P0/P1 (now).** The site sits behind the server's id-auth gate, which is the
actual security boundary. Inside that boundary the SPA declares which technician
is acting via the `X-CP-Actor` header, chosen from a name picker. That is
attribution, not authentication: it guards nothing, grants nothing, and stores no
credential. See agents.md - it must never grow a password field.

**P2 (next).** `resolve_actor` starts verifying the signed BW identity that
Pattern B forwards, and `CurrentActor.role` comes from the BW account instead of
the technicians table. Because every router depends on this function rather than
on request headers, that swap touches this file only.
"""

from __future__ import annotations

from dataclasses import dataclass

from fastapi import Depends, Header, HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.db import get_db
from app.models.tables import Technician

ACTOR_HEADER = "X-CP-Actor"


@dataclass(frozen=True)
class CurrentActor:
    id: int
    name: str
    role: str


def resolve_actor(
    x_cp_actor: str | None = Header(default=None, alias=ACTOR_HEADER),
    db: Session = Depends(get_db),
) -> CurrentActor:
    if not x_cp_actor:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={
                "error_code": "ACTOR_REQUIRED",
                "summary": "No acting technician was declared.",
                "details": (
                    "Pick who you are on the capture screen; the app sends that "
                    "as the X-CP-Actor header."
                ),
            },
        )

    try:
        actor_id = int(x_cp_actor)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": "ACTOR_MALFORMED",
                "summary": "The acting technician header was not a technician id.",
                "details": None,
            },
        ) from None

    technician = db.scalar(
        select(Technician).where(Technician.id == actor_id, Technician.active.is_(True))
    )
    if technician is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": "ACTOR_UNKNOWN",
                "summary": "That technician is not on file.",
                "details": "Reselect who you are on the capture screen.",
            },
        )

    return CurrentActor(id=technician.id, name=technician.name, role=technician.role)


def require_planner(actor: CurrentActor = Depends(resolve_actor)) -> CurrentActor:
    """Planner-only routes.

    At P0 role comes from the seed table, so this is an organisational
    guard-rail rather than a security control - the gate is. At P2 the role
    arrives on the signed BW identity and this becomes a real authorization
    check with no other code change.
    """
    if actor.role not in ("planner", "owner"):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail={
                "error_code": "PLANNER_REQUIRED",
                "summary": "That action is limited to maintenance planners.",
                "details": None,
            },
        )
    return actor
