"""Seat auth — the Bearer token identifies the seat (docs/V3-PLAN.md §1.7).

Tailnet membership gates *reach*; the Bearer decides *who* is calling and what
they may do. Seats are declared in ``config.vault_mcp.seats`` as
``{id, scope: read-write|read-only, secret: <ENV_NAME>}`` — secrets are
names-only (config.py's guardrail), the values live in the off-repo
``alexandria.env`` the wrapper sources. A vault with no seats block gets the
implicit owner seat (``VAULT_MCP_BEARER``, read-write). When
``VAULT_MCP_VIEWER_BEARER`` is set, the read-only viewer seat is appended
whether or not the block is explicit (the rack's per-tenant VIEW_KEY lands
there — see DEFAULT_VIEWER_SEAT for why additive).

Fail-closed: a seat whose env var is unset simply doesn't exist, and a server
with zero resolvable seats refuses to start (:class:`SeatConfigError`) rather
than serve unauthenticated.
"""
import os
from dataclasses import dataclass

READ_WRITE = "read-write"
READ_ONLY = "read-only"

# The implicit owner seat, used when config.vault_mcp.seats is absent/empty.
DEFAULT_OWNER_SEAT = {"id": "owner", "scope": READ_WRITE, "secret": "VAULT_MCP_BEARER"}

# The viewer seat, off unless its env var is set. This is what makes a
# rack-minted VIEW_KEY a *read-only* credential: the rack injects it as
# VAULT_MCP_VIEWER_BEARER and the brain itself caps the scope — no router or
# proxy has to be trusted for that. Unlike the owner default it is ADDITIVE
# (load_seats appends it even beside an explicit seats block): rendered vaults
# spell their seats out (config.example.json), so "implicit default only"
# would quietly disable the viewer on every real vault. Setting the env var is
# the explicit act; a config overrides it by binding this secret name itself,
# or by claiming the "viewer" id.
DEFAULT_VIEWER_SEAT = {"id": "viewer", "scope": READ_ONLY, "secret": "VAULT_MCP_VIEWER_BEARER"}


class SeatConfigError(RuntimeError):
    """No usable seat — the server must not start unauthenticated."""


@dataclass(frozen=True)
class Seat:
    """One authenticated client identity."""
    id: str
    scope: str  # READ_WRITE | READ_ONLY

    @property
    def can_write(self) -> bool:
        return self.scope == READ_WRITE


def load_seats(config, env=None) -> dict:
    """``{token_value: Seat}`` for every declared seat whose secret is set.

    Unset env vars are skipped (that seat is simply off); an empty result raises
    :class:`SeatConfigError` so a misconfigured box fails loudly, not open.
    """
    env = os.environ if env is None else env
    declared = list(((config.get("vault_mcp") or {}).get("seats"))
                    or [DEFAULT_OWNER_SEAT])
    if env.get(DEFAULT_VIEWER_SEAT["secret"]) and not any(
            e.get("secret") == DEFAULT_VIEWER_SEAT["secret"]
            or e.get("id") == DEFAULT_VIEWER_SEAT["id"] for e in declared):
        declared.append(DEFAULT_VIEWER_SEAT)
    seats, missing = {}, []
    for i, entry in enumerate(declared):
        sid = entry.get("id") or f"seat-{i}"
        secret_name = entry.get("secret")
        scope = entry.get("scope") or READ_WRITE
        if scope not in (READ_WRITE, READ_ONLY):
            raise SeatConfigError(f"vault_mcp.seats[{sid}].scope {scope!r} is not "
                                  f"{READ_WRITE!r} or {READ_ONLY!r} (validate() flags this)")
        token = env.get(secret_name or "")
        if not token:
            missing.append(f"{sid} (${secret_name})")
            continue
        seats[token] = Seat(id=sid, scope=scope)
    if not seats:
        raise SeatConfigError(
            "no seat has its Bearer set — export at least the owner's "
            f"(missing: {', '.join(missing) or 'no seats declared'}); refusing to "
            "serve unauthenticated")
    return seats
