"""Authentication via BW Auth (Pattern B, "Sign in with BW").

SubmitStream is a hybrid host: the public submit endpoints (/f/…, /embed.js,
/health) stay open; the dashboard requires a BW-authenticated session. The host is
NOT id-auth-gated (it owns /auth/*). Flow:
  /auth/start    → interactive OAuth (PKCE) redirect to auth.bowden.works
  /auth/callback → verify + map BW identity → app session → /dashboard
  /logout        → clear the app session (local; BW SSO left intact)

Identity mapping is REJECT-UNTIL-PROVISIONED: a BW account only gets in if an
account row already exists for it (matched by bw_username, then email). Admins/
managers provision users up front (create by email + grant forms).
"""

from __future__ import annotations

from html import escape

from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse

import auth
import bw_auth
import deps
from db import execute, query_one

router = APIRouter()


def _apply_login_cookies(resp, pending: dict) -> None:
    """Attach the PKCE verifier + state cookies collected by start_login()."""
    for name, (value, max_age) in pending.items():
        resp.set_cookie(name, value, max_age=max_age, httponly=True, secure=True, samesite="lax")


def _set_session(resp, client_id: int):
    resp.set_cookie(auth.SESSION_COOKIE, auth.create_session_cookie(client_id),
                    max_age=auth.SESSION_TTL_DAYS * 86400, httponly=True, secure=True,
                    samesite="lax")
    return resp


def _establish_or_reject(claims: dict) -> int | None:
    """Map a verified BW identity to an account row. Returns client_id, or None if
    the BW account isn't provisioned in SubmitStream (reject-until-provisioned).

    Match order: bw_username (the stable BW key) → email (links a pre-provisioned
    row on first BW login). Role and name are app-owned and left untouched."""
    username = claims["username"]
    email = (claims.get("email") or "").strip().lower()
    row = query_one("SELECT * FROM clients WHERE bw_username = ?", (username,))
    if not row and email:
        row = query_one("SELECT * FROM clients WHERE lower(email) = ?", (email,))
    if not row:
        return None
    if not row["bw_username"]:
        execute("UPDATE clients SET bw_username = ? WHERE id = ?", (username, row["id"]))
    execute("UPDATE clients SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?", (row["id"],))
    try:
        bw_auth.report_role(username, row["role"])  # display-only mirror to the hub
    except bw_auth.BWAuthError:
        pass
    return row["id"]


@router.get("/auth/start")
def auth_start(request: Request):
    """Explicit 'Sign in with BW' click → interactive OAuth (PKCE)."""
    pending: dict = {}

    def set_cookie(name, value, max_age):
        pending[name] = (value, max_age)

    url, _state = bw_auth.start_login(set_cookie, silent=False)
    resp = RedirectResponse(url, status_code=303)
    _apply_login_cookies(resp, pending)
    return resp


@router.get("/auth/callback")
def auth_callback(request: Request):
    """id-auth redirects the browser back here with ?code (or ?error=login_required
    for a silent-probe miss — we don't probe, so just send them to the landing)."""
    if bw_auth.is_login_required(request.query_params):
        return RedirectResponse("/", status_code=303)

    code = request.query_params.get("code", "")
    state = request.query_params.get("state", "")
    expected_state = request.cookies.get(bw_auth.STATE_COOKIE, "")
    verifier = request.cookies.get(bw_auth.PKCE_COOKIE, "")
    if not code or not state or not expected_state or state != expected_state:
        raise HTTPException(status_code=400, detail="invalid or missing state")
    if not verifier:
        raise HTTPException(status_code=400, detail="missing PKCE verifier (session expired?)")

    try:
        claims = bw_auth.finish_login(code, verifier)  # exchange + verify id_token
    except bw_auth.BWAuthError:
        raise HTTPException(status_code=401, detail="sign-in failed")

    client_id = _establish_or_reject(claims)
    if client_id is None:
        who = escape(claims.get("email") or claims.get("username") or "your BW account")
        resp = HTMLResponse(deps.jinja_env.get_template("message.html").render(
            heading="No SubmitStream access",
            body=f"You signed in as {who}, but that account hasn't been given access to "
                 f"SubmitStream yet. Ask an administrator to add you.",
            app_version=deps.VERSION), status_code=403)
    else:
        resp = RedirectResponse("/dashboard", status_code=303)
        _set_session(resp, client_id)
    resp.delete_cookie(bw_auth.PKCE_COOKIE)
    resp.delete_cookie(bw_auth.STATE_COOKIE)
    return resp


@router.post("/logout")
def logout(request: Request, csrf: str = Form("")):
    """Local logout: clear the app session. The BW SSO session is left intact (so
    you stay signed into other BW sites); the next 'Sign in with BW' is one click."""
    resp = RedirectResponse("/", status_code=303)
    resp.delete_cookie(auth.SESSION_COOKIE)
    resp.delete_cookie(auth.VIEWAS_COOKIE)
    return resp
