"""Auth — opaque bearer tokens in SQLite. The papers check at the door.

A token is a random URL-safe string handed out ONCE at mint time; we store only
its sha256 hash, so a DB leak can't be replayed as credentials. Verification is
an indexed hash lookup (the hash is the secret-derived key — no timing side
channel on the plaintext). Scopes gate per-engine access; '*' is the wildcard.

The DEV_TOKEN env stays a recognized wildcard bootstrap for local dev ONLY
(prod never sets it) so `uvicorn` + curl works without minting first.
"""
import hashlib
import secrets
import time
from dataclasses import dataclass

import config
import db
import scopes as _scopes

TTL_RE_DAYS = 86400.0


def hash_token(plaintext: str) -> str:
    return hashlib.sha256(plaintext.encode()).hexdigest()


@dataclass
class Principal:
    """Who's calling, resolved from a valid token."""
    account: str
    scopes: frozenset[str]
    token_id: int | None      # None for the dev bootstrap
    expires_at: float | None

    def has_scope(self, scope: str) -> bool:
        """Hierarchical match per the platform convention (see scopes.py):
        a granted `api:audio:*` authorizes `api:audio:analyze:emotion`, etc."""
        return _scopes.match_any(self.scopes, scope)


# ── minting / management (used by the fourier CLI) ─────────────────────────────
def ensure_account(name: str, notes: str = "") -> int:
    with db.cursor() as c:
        row = c.execute("SELECT id FROM accounts WHERE name=?", (name,)).fetchone()
        if row:
            return row["id"]
        cur = c.execute("INSERT INTO accounts(name, created, notes) VALUES(?,?,?)",
                        (name, time.time(), notes))
        return cur.lastrowid


def mint(account: str, scopes: str = "*", ttl_days: float | None = 31,
         unlimited: bool = True, notes: str = "") -> str:
    """Create a token; return the PLAINTEXT once (never recoverable after).
    ttl_days=None → never expires. unlimited=True → quota_json NULL (no cap)."""
    acc_id = ensure_account(account, notes)
    plaintext = secrets.token_urlsafe(32)
    expires = None if ttl_days is None else time.time() + ttl_days * TTL_RE_DAYS
    quota = None if unlimited else "{}"
    with db.cursor() as c:
        c.execute(
            "INSERT INTO tokens(token_hash, prefix, account_id, scopes, quota_json, "
            "created, expires_at, revoked) VALUES(?,?,?,?,?,?,?,0)",
            (hash_token(plaintext), plaintext[:6], acc_id, scopes.strip(), quota,
             time.time(), expires))
    return plaintext


def revoke(token_id: int) -> bool:
    with db.cursor() as c:
        cur = c.execute("UPDATE tokens SET revoked=1 WHERE id=?", (token_id,))
        return cur.rowcount > 0


def list_tokens() -> list[dict]:
    with db.cursor() as c:
        rows = c.execute(
            "SELECT t.id, t.prefix, a.name AS account, t.scopes, t.quota_json, "
            "t.created, t.expires_at, t.revoked FROM tokens t "
            "JOIN accounts a ON a.id=t.account_id ORDER BY t.id").fetchall()
        return [dict(r) for r in rows]


# ── verification (the hot path) ─────────────────────────────────────────────────
def verify(plaintext: str) -> Principal | None:
    """Resolve a bearer token to a Principal, or None if invalid/expired/revoked."""
    if not plaintext:
        return None
    # dev bootstrap: env wildcard, local only
    if config.DEV_TOKEN and secrets.compare_digest(plaintext, config.DEV_TOKEN):
        return Principal("dev", frozenset("*"), None, None)
    with db.cursor() as c:
        row = c.execute(
            "SELECT t.id, a.name AS account, t.scopes, t.expires_at, t.revoked "
            "FROM tokens t JOIN accounts a ON a.id=t.account_id WHERE t.token_hash=?",
            (hash_token(plaintext),)).fetchone()
    if not row or row["revoked"]:
        return None
    if row["expires_at"] is not None and row["expires_at"] < time.time():
        return None
    return Principal(row["account"], frozenset(row["scopes"].split()),
                     row["id"], row["expires_at"])
