//! Edge authentication layer — the SDK's perimeter for Bearer-token
//! authenticated HTTP traffic.
//!
//! Mounts at the HTTP edge of any axum router and produces a single,
//! consumer-shaped session value as a request extension that handlers
//! consume by capability passing rather than re-authenticating in body.
//!
//! ## Why the SDK ships this Layer
//!
//! Phase 11 — chat-auth was the 1st integrator (`chat-auth::middleware`),
//! RCW was the 2nd (`rollcall_web::infrastructure::auth::bearer_auth_layer`),
//! CTW was the 3rd (`classytime_web::infrastructure::auth::bearer_auth_layer`).
//! All three carried near-identical mechanism — Authorization > cookie
//! extraction, three HTTP dispositions (200 / 401 / 503), add-based
//! cookie clearance on dead-session 401, verbatim cookie value passing
//! to the validator. With **N=3 evidence** in hand, the mechanism
//! collapses into one SDK Layer and consumers ship only the substrate
//! adapter (`AuthProvider<S>` impl) + their cookie-name constant +
//! their clearance closure.
//!
//! ## Surface — four public types
//!
//! - [`AuthProvider<S>`] — the trait that consumers implement. One
//!   async method, generic over the consumer's perimeter session type
//!   `S`. Cryptographic verify + substrate liveness happen inside the
//!   impl as one atomic decision; the Layer never spells substrate
//!   internals.
//! - [`VerifyError`] — exactly two HTTP dispositions. Richer error
//!   taxonomies (per-substrate break-glass dashboards) collapse here
//!   at the SDK boundary.
//! - [`BearerAuthConfig`] — `&'static str` cookie name + an
//!   `Arc<dyn Fn(CookieJar) -> CookieJar + Send + Sync>` clearance
//!   closure. A value struct so future fields (e.g. `audit_sink`) can
//!   be added with `Default` without breaking call sites.
//! - [`BearerAuthLayer<Sess, P>`] — the tower [`Layer`] wired into
//!   axum routers via `.layer(...)`.
//!
//! ## Sources (RFC 6750 + RFC 9700 §6.3)
//!
//! - `Authorization: Bearer <token>` HTTP header — preferred. RFC 6750
//!   §2.1. Used by any non-browser caller (CLI clients, server-to-
//!   server requests, native gRPC over h2 with a synthesised Bearer).
//! - HttpOnly cookie identified by [`BearerAuthConfig::access_cookie_name`]
//!   — fallback. RFC 9700 §6.3 browser-context BCP. Each consumer ships
//!   its own `__Host-*_at` constant (cookie names are domain-scoped per
//!   RFC 6265bis); the SDK never spells the literal.
//!
//! `Authorization` wins precedence: a request carrying both is treated
//! as header-source.
//!
//! ## Errors
//!
//! - Missing Bearer source (no header, no cookie) → 401 without cookie
//!   clearance. The browser may simply have not yet authenticated; no
//!   session exists to clear.
//! - Token rejected ([`VerifyError::Rejected`]) → 401 + add-based
//!   cookie clearance via the consumer's [`BearerAuthConfig::on_clear`]
//!   closure. Browsers stop replaying a stale token.
//! - Substrate transient ([`VerifyError::SubstrateTransient`]) → 503
//!   with cookies preserved. The browser may retry; the session is
//!   still valid even if the validator can't currently say so.
//!
//! ## Why no SDK perimeter session type
//!
//! The trait's generic `S` IS the perimeter session — there is no SDK
//! `AuthClaims` or `BearerAuthSession` for the consumer to project from.
//! The consumer's `AuthProvider<MySession>::verify_token` impl produces
//! `MySession` directly, in one atomic step. Eliminates the otherwise-
//! tempting pass-through `claims → session` projection, and lets each
//! consumer keep its native session shape without SDK opinion. RCW/CTW
//! ship `BearerAuthSession` (3 fields); chat-auth ships `AuthSession`
//! (5 fields including `account_type`/`scopes`/`audience` for OAuth
//! 3rd-party scope-gating). Both coexist behind the same SDK Layer.
//!
//! See `RFC_2026-05-07_oidc-rp-phase-11x-rcw-ctw-migration.md` §11 for
//! the contract-lock rationale.

use std::marker::PhantomData;
use std::sync::Arc;
use std::task::{Context, Poll};

use async_trait::async_trait;
use axum::body::Body;
use axum::http::header::{AUTHORIZATION, COOKIE};
use axum::http::{HeaderMap, Request, Response, StatusCode};
use axum::response::IntoResponse;
use axum_extra::extract::cookie::CookieJar;
use tower::{Layer, Service};

/// Perimeter token-validation port — the consumer-implemented seam.
///
/// One async method spans the consumer's full security decision —
/// cryptographic verification + substrate liveness lookup — and
/// produces the consumer's perimeter session type `S` directly.
/// `BearerAuthLayer` calls this once per authenticated request and
/// maps the two error variants to HTTP dispositions:
/// [`VerifyError::Rejected`] → 401 + cookie clearance,
/// [`VerifyError::SubstrateTransient`] → 503 with cookies preserved.
///
/// ## Why generic over `S` rather than returning a fixed claims type
///
/// chat-auth's perimeter session has 5 fields (`ppnum_id`, `ppnum`,
/// `account_type`, `scopes`, `audience`); RCW/CTW's has 3. A fixed
/// SDK-side `AuthClaims` would force chat-auth to run a second
/// "hydration" Layer after the perimeter to enrich the session,
/// breaking the single-perimeter invariant. The generic `S` lets
/// each consumer's substrate-aware impl produce its native session
/// directly in one step.
#[async_trait]
pub trait AuthProvider<S>: Send + Sync
where
    S: Clone + Send + Sync + 'static,
{
    /// Validate the Bearer token end-to-end. The SDK Layer passes the
    /// raw token verbatim — no decoding, no trimming. The impl owns
    /// the substrate (JWKS cache, session-row lookup, sv-axis cache,
    /// or whichever combination this consumer composes).
    async fn verify_token(&self, bearer: &str) -> Result<S, VerifyError>;
}

/// Verification failure surface — exactly two HTTP dispositions.
///
/// Richer per-substrate taxonomies (chat-auth's break-glass dashboard
/// distinguishes `JtiReplayed` / `SessionVersionStale` / etc.) stay
/// inside the consumer's [`AuthProvider`] impl and collapse to one of
/// these two variants at the SDK boundary. The Layer reads only the
/// disposition; the operator dashboard reads the substrate.
#[derive(Debug, Clone, thiserror::Error)]
pub enum VerifyError {
    /// Token rejected — invalid signature, expired, sub missing,
    /// session row missing or revoked, sv mismatch. Maps to **401 +
    /// add-based cookie clearance** at the perimeter so the browser
    /// stops replaying a dead session.
    #[error("token rejected: {0}")]
    Rejected(String),
    /// Substrate transient — JWKS fetch failed with no usable cache,
    /// session-row DB unreachable, sv-axis cache substrate down. Maps
    /// to **503 with cookies preserved**; the session may still be
    /// valid and the browser may retry.
    #[error("auth substrate transient: {0}")]
    SubstrateTransient(String),
}

/// Per-consumer cookie configuration carried into [`BearerAuthLayer`].
///
/// The Layer uses [`Self::access_cookie_name`] as the fallback bearer
/// source (when `Authorization` is absent) and the [`Self::on_clear`]
/// closure to mint Set-Cookie removals on dead-session 401s. Cookie
/// names are domain-scoped per RFC 6265bis; each consumer owns its
/// `__Host-*_at` literal in its own `cookies` module.
#[derive(Clone)]
pub struct BearerAuthConfig {
    /// Cookie name carrying the Bearer token in browser contexts —
    /// e.g. `"__Host-pcs_at"` (chat-auth), `"__Host-rcw_at"` (RCW),
    /// `"__Host-ctw_at"` (CTW).
    pub access_cookie_name: &'static str,
    /// Add-based session-cookie clearance closure. Invoked on
    /// dead-session 401 with a fresh [`CookieJar`]; the returned jar's
    /// add-list becomes the response's Set-Cookie headers. Consumers
    /// typically wrap their `cookies::clear_session_cookies` helper
    /// (which clears AT + RT, and possibly CSRF in future).
    pub on_clear: Arc<dyn Fn(CookieJar) -> CookieJar + Send + Sync>,
}

impl BearerAuthConfig {
    /// Build a config from the cookie name and clearance closure.
    #[must_use]
    pub fn new(
        access_cookie_name: &'static str,
        on_clear: Arc<dyn Fn(CookieJar) -> CookieJar + Send + Sync>,
    ) -> Self {
        Self {
            access_cookie_name,
            on_clear,
        }
    }
}

/// Tower [`Layer`] mounting the perimeter authentication boundary.
///
/// Wrap an axum router with `.layer(BearerAuthLayer::new(provider, config))`.
/// Mount at HTTP-edge granularity, not per-route — every authenticated
/// transport should sit behind this layer. Routes that need anonymous
/// access (health endpoints, the OIDC RP sub-router itself, public
/// landing pages) stay outside.
///
/// Two generic parameters:
/// - `Sess` — the consumer's perimeter session type. Whatever
///   [`AuthProvider::verify_token`] returns lands in
///   `req.extensions()` for handlers to read via
///   `req.extensions().get::<Sess>()`.
/// - `P: ?Sized` — the [`AuthProvider`] impl. Both concrete types
///   (`Arc<MyProvider>`) and trait objects (`Arc<dyn AuthProvider<Sess>>`)
///   are supported.
pub struct BearerAuthLayer<Sess, P: ?Sized>
where
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    provider: Arc<P>,
    config: BearerAuthConfig,
    // `fn() -> Sess` is `Send + Sync` regardless of `Sess`'s own
    // auto-trait disposition, and signals "this layer logically
    // produces `Sess` values" rather than "owns" them.
    _session: PhantomData<fn() -> Sess>,
}

impl<Sess, P: ?Sized> BearerAuthLayer<Sess, P>
where
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    /// Build the Layer from an [`AuthProvider`] handle and a
    /// [`BearerAuthConfig`]. Production wiring at app boot looks like:
    ///
    /// ```ignore
    /// let provider: Arc<dyn AuthProvider<MySession>> =
    ///     Arc::new(MyConcreteProvider::new(verifier, db_pool));
    /// let layer = BearerAuthLayer::new(
    ///     provider,
    ///     BearerAuthConfig::new(
    ///         my_cookies::ACCESS_COOKIE,
    ///         Arc::new(|jar| my_cookies::clear_session_cookies(jar)),
    ///     ),
    /// );
    /// app.layer(layer)
    /// ```
    #[must_use]
    pub fn new(provider: Arc<P>, config: BearerAuthConfig) -> Self {
        Self {
            provider,
            config,
            _session: PhantomData,
        }
    }
}

// Manual `Clone` — `#[derive(Clone)]` would generate `where P: Clone`,
// which fails for `dyn AuthProvider<Sess>`. The manual impl needs only
// `Arc<P>: Clone` and `BearerAuthConfig: Clone`, both unconditional.
impl<Sess, P: ?Sized> Clone for BearerAuthLayer<Sess, P>
where
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    fn clone(&self) -> Self {
        Self {
            provider: self.provider.clone(),
            config: self.config.clone(),
            _session: PhantomData,
        }
    }
}

impl<Inner, Sess, P: ?Sized> Layer<Inner> for BearerAuthLayer<Sess, P>
where
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    type Service = BearerAuthService<Inner, Sess, P>;

    fn layer(&self, inner: Inner) -> Self::Service {
        BearerAuthService {
            inner,
            provider: self.provider.clone(),
            config: self.config.clone(),
            _session: PhantomData,
        }
    }
}

/// Tower [`Service`] produced by [`BearerAuthLayer::layer`].
///
/// One instance per `Layer::layer` call; cloned by axum across futures
/// per the standard tower middleware idiom.
pub struct BearerAuthService<Inner, Sess, P: ?Sized>
where
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    inner: Inner,
    provider: Arc<P>,
    config: BearerAuthConfig,
    _session: PhantomData<fn() -> Sess>,
}

impl<Inner, Sess, P: ?Sized> Clone for BearerAuthService<Inner, Sess, P>
where
    Inner: Clone,
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            provider: self.provider.clone(),
            config: self.config.clone(),
            _session: PhantomData,
        }
    }
}

impl<Inner, Sess, P: ?Sized> Service<Request<Body>> for BearerAuthService<Inner, Sess, P>
where
    Inner: Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static,
    Inner::Future: Send + 'static,
    Sess: Clone + Send + Sync + 'static,
    P: AuthProvider<Sess> + 'static,
{
    type Response = Response<Body>;
    type Error = Inner::Error;
    type Future = std::pin::Pin<
        Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
    >;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, mut req: Request<Body>) -> Self::Future {
        let provider = self.provider.clone();
        let cookie_name = self.config.access_cookie_name;
        let on_clear = self.config.on_clear.clone();
        // Standard tower middleware idiom: the future captures a clone
        // of `inner` so it doesn't borrow `self`. axum tolerates the
        // re-clone; the alternative (`mem::replace`) introduces more
        // ceremony than value at this layer.
        let mut inner = self.inner.clone();
        Box::pin(async move {
            let token = match extract_bearer(req.headers(), cookie_name) {
                Some(t) => t,
                None => return Ok(unauthenticated_response(false, &on_clear)),
            };

            match provider.verify_token(&token).await {
                Ok(session) => {
                    req.extensions_mut().insert(session);
                    inner.call(req).await
                }
                Err(VerifyError::SubstrateTransient(reason)) => {
                    tracing::warn!(reason = %reason, "auth substrate transient — 503");
                    Ok(transient_response())
                }
                Err(VerifyError::Rejected(reason)) => {
                    tracing::debug!(reason = %reason, "token rejected at perimeter — 401 + clear");
                    Ok(unauthenticated_response(true, &on_clear))
                }
            }
        })
    }
}

/// Extract the Bearer token: Authorization header > consumer's cookie.
///
/// Authorization is preferred and case-insensitive on the scheme
/// (RFC 7235 §2.1). Cookie value is passed verbatim — no %-decoding,
/// no trimming, no transformation. Empty token strings are treated as
/// absent so an attacker can't replay an empty cookie to coerce
/// "clear cookies" behaviour.
fn extract_bearer(headers: &HeaderMap, cookie_name: &'static str) -> Option<String> {
    if let Some(value) = headers.get(AUTHORIZATION).and_then(|v| v.to_str().ok())
        && let Some(token) = value
            .strip_prefix("Bearer ")
            .or_else(|| value.strip_prefix("bearer "))
        && !token.is_empty()
    {
        return Some(token.to_owned());
    }
    let cookie_header = headers.get(COOKIE).and_then(|v| v.to_str().ok())?;
    cookie_header
        .split(';')
        .filter_map(|s| s.trim().split_once('='))
        .find(|(name, _)| *name == cookie_name)
        .map(|(_, value)| value.to_owned())
        .filter(|v| !v.is_empty())
}

/// 401 response. With `clear_cookies` set, the consumer's `on_clear`
/// closure is invoked on a fresh [`CookieJar`] and its add-list
/// becomes the response's Set-Cookie headers — independent of the
/// original request's cookie state.
fn unauthenticated_response(
    clear_cookies: bool,
    on_clear: &Arc<dyn Fn(CookieJar) -> CookieJar + Send + Sync>,
) -> Response<Body> {
    if clear_cookies {
        let jar = on_clear(CookieJar::new());
        (StatusCode::UNAUTHORIZED, jar, "unauthenticated").into_response()
    } else {
        (StatusCode::UNAUTHORIZED, "unauthenticated").into_response()
    }
}

/// 503 response — auth substrate transient. Cookies preserved; browser
/// may retry.
fn transient_response() -> Response<Body> {
    (
        StatusCode::SERVICE_UNAVAILABLE,
        "auth substrate temporarily unavailable",
    )
        .into_response()
}

// ─────────────────────────────────────────────────────────────────────
// Test-support adapter
// ─────────────────────────────────────────────────────────────────────

/// In-memory [`AuthProvider`] for boundary tests.
///
/// Configured with a closure mapping bearer-string → outcome, so each
/// test can pin the validator's verdict. Mirrors the
/// [`crate::token::MemoryBearerVerifier`] / [`crate::oidc::MemoryIdTokenVerifier`]
/// pattern: SDK ships the in-process test seam so consumer integration
/// tests don't redefine a `MockValidator` struct in every file.
///
/// Gated behind `cfg(any(test, feature = "test-support"))` — zero
/// runtime cost when disabled.
#[cfg(any(test, feature = "test-support"))]
pub struct MemoryAuthProvider<S> {
    outcome: Arc<dyn Fn(&str) -> Result<S, VerifyError> + Send + Sync>,
}

#[cfg(any(test, feature = "test-support"))]
impl<S> MemoryAuthProvider<S>
where
    S: Clone + Send + Sync + 'static,
{
    /// Build with a verdict closure. The closure runs on every
    /// `verify_token` call; tests can capture state (e.g. an
    /// `AtomicUsize` call counter) by `move`-capturing into a
    /// `Fn` body.
    pub fn new<F>(outcome: F) -> Self
    where
        F: Fn(&str) -> Result<S, VerifyError> + Send + Sync + 'static,
    {
        Self {
            outcome: Arc::new(outcome),
        }
    }
}

#[cfg(any(test, feature = "test-support"))]
#[async_trait]
impl<S> AuthProvider<S> for MemoryAuthProvider<S>
where
    S: Clone + Send + Sync + 'static,
{
    async fn verify_token(&self, bearer: &str) -> Result<S, VerifyError> {
        (self.outcome)(bearer)
    }
}
