"""The app's OWN session — a short-TTL, HMAC-signed, HttpOnly cookie.

Follows the kit's token construction (bw_auth id_token format:
base64url(compact-json)|hmac_hex) rather than pulling in itsdangerous —
same crypto, zero dependencies, and one shape to audit.

Why stateless + short (ADR #002): PATTERN-B keeps the id_token one-shot
and tells the app to hold its own session. We keep that session SHORT
(12h) so id-auth-side revocation converges quickly, and rely on silent
re-establishment: an expired cookie just 302s back into /auth/login,
and a visitor with a live BW SSO session round-trips auth.bowden.works
without seeing a login form.

Signing key precedence:
  1. SESSION_SECRET env (explicit override, if ever staged)
  2. derived from BW_CLIENT_SECRET (HMAC-SHA256 with a fixed context
     string) — the production default: no extra secret to stage, and
     rotating the client secret also revokes every app session
  3. an ephemeral per-process random key (dev/tests without BW config;
     sessions die with the process, which is correct there)
"""

import base64
import hashlib
import hmac
import json
import os
import secrets
import time

SESSION_COOKIE = "with_session"
NEXT_COOKIE = "with_next"          # post-login destination, set by /auth/login
SESSION_TTL = 12 * 60 * 60         # 12 hours (ADR #002)

_KEY_CONTEXT = b"with-session-key-v1"
_EPHEMERAL_KEY = secrets.token_bytes(32)


def _session_key() -> bytes:
    explicit = os.environ.get("SESSION_SECRET", "")
    if explicit:
        return explicit.encode("utf-8")
    client_secret = os.environ.get("BW_CLIENT_SECRET", "")
    if client_secret:
        return hmac.new(client_secret.encode("utf-8"), _KEY_CONTEXT, hashlib.sha256).digest()
    return _EPHEMERAL_KEY


def _b64url_nopad(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")


def _b64url_decode(value: str) -> bytes:
    pad = "=" * (-len(value) % 4)
    return base64.urlsafe_b64decode(value + pad)


def mint_session(
    username: str,
    email: str = "",
    is_super_admin: bool = False,
    *,
    ttl: int = SESSION_TTL,
    now: int | None = None,
) -> str:
    """Sign a session token for a provisioned user. Set it as an
    HttpOnly + Secure + SameSite=Lax cookie named SESSION_COOKIE."""
    issued = int(now if now is not None else time.time())
    claims = {
        "username": username,
        "email": email,
        "is_super_admin": bool(is_super_admin),
        "iat": issued,
        "exp": issued + int(ttl),
    }
    value = _b64url_nopad(json.dumps(claims, separators=(",", ":")).encode("utf-8"))
    sig = hmac.new(_session_key(), value.encode("utf-8"), hashlib.sha256).hexdigest()
    return f"{value}|{sig}"


def verify_session(token: str, *, now: int | None = None) -> dict | None:
    """Return the session claims, or None for anything invalid — a bad
    signature, garbage, or an expired session all read as "no session"
    (the middleware then routes into silent re-establishment). Never
    raises: an attacker-controlled cookie must not 500 the app."""
    if not token or "|" not in token:
        return None
    value, sig = token.rsplit("|", 1)
    expected = hmac.new(_session_key(), value.encode("utf-8"), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        return None
    try:
        claims = json.loads(_b64url_decode(value).decode("utf-8"))
    except (ValueError, UnicodeDecodeError):
        return None
    if not isinstance(claims, dict) or not claims.get("username"):
        return None
    current = int(now if now is not None else time.time())
    try:
        if int(claims.get("exp", 0)) <= current:
            return None
    except (TypeError, ValueError):
        return None
    return claims
