//! CSRF-token validation for state-changing POSTs under `/web/`.
//!
//! The `WebSession` carries a `csrf_token` minted once at login or
//! magic-link consume. Browser forms include a hidden `csrf_token`
//! field populated from the session. Handlers that mutate server
//! state use [`CsrfForm<T>`] instead of `Form<T>` to enforce that the
//! submitted token matches the session's token. The session cookie
//! alone is not sufficient: `SameSite=Strict` already blocks most
//! cross-site POSTs, but the token is the defence that also holds
//! against attackers with cross-site injection on a same-site
//! subdomain or misconfigured browser.

use std::sync::Arc;
use std::time::Instant;

use axum::extract::{FromRef, FromRequest, Request};
use axum_extra::extract::{CookieJar, Form};
use serde::de::DeserializeOwned;
use subtle::ConstantTimeEq;
use tracing::warn;

use crate::config::Config;
use crate::error::AppError;
use crate::web_session::{SESSION_COOKIE, SessionStore};

/// Every form struct protected by CSRF implements this trait so the
/// extractor can pull the token out of the decoded body. A plain
/// `#[serde(default)] pub csrf_token: String` plus a three-line impl
/// of this trait is all the boilerplate each form needs.
pub trait HasCsrfToken {
    fn csrf_token(&self) -> &str;
}

/// Wrapping extractor — a drop-in replacement for `Form<T>` on
/// state-changing `/web/` POST handlers. Validates the form body's
/// `csrf_token` against the session's csrf_token via a constant-time
/// comparison.
///
/// Rejection mapping:
/// - Missing or expired session → 302 redirect to `/web/login` (so a
///   second-tab logout shows "bitte neu anmelden", not "csrf failed").
/// - Form body malformed → 400 `BadRequest`.
/// - Token present but mismatched → 403 `Forbidden`.
pub struct CsrfForm<T>(pub T);

impl<S, T> FromRequest<S> for CsrfForm<T>
where
    S: Send + Sync,
    T: DeserializeOwned + HasCsrfToken + Send + 'static,
    SessionStore: FromRef<S>,
    Arc<Config>: FromRef<S>,
{
    type Rejection = AppError;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let store = SessionStore::from_ref(state);

        // Look up the expected token first (headers only — the body
        // extractor below will consume `req`).
        let cookie_token = CookieJar::from_headers(req.headers())
            .get(SESSION_COOKIE)
            .map(|c| c.value().to_owned())
            .ok_or_else(|| AppError::Redirect("/web/login".into()))?;

        let expected = {
            let r = store.read().await;
            let session = r
                .get(&cookie_token)
                .ok_or_else(|| AppError::Redirect("/web/login".into()))?;
            if session.expires_at <= Instant::now() {
                return Err(AppError::Redirect("/web/login".into()));
            }
            session.csrf_token.clone()
        };

        // Consume the body. `axum_extra::extract::Form` supports
        // repeated fields (BulkForm's `Vec<String>`); plain
        // `axum::extract::Form` does not.
        let Form(form): Form<T> = Form::from_request(req, state)
            .await
            .map_err(|e| AppError::BadRequest(format!("form parse: {e}")))?;

        // Constant-time compare. `ConstantTimeEq` on byte slices
        // returns `Choice(0)` when the lengths differ, so empty or
        // malformed tokens fall through here without a special case.
        let provided = form.csrf_token();
        if !bool::from(provided.as_bytes().ct_eq(expected.as_bytes())) {
            warn!(
                provided_len = provided.len(),
                expected_len = expected.len(),
                "csrf token mismatch"
            );
            return Err(AppError::Forbidden("CSRF-Token ungültig oder fehlt".into()));
        }

        Ok(CsrfForm(form))
    }
}

/// Form body with only a CSRF token — for handlers that are
/// state-changing but carry no other user-submitted data
/// (close/reopen/reset/analyze/logout). Using one shared type beats
/// five wordless copy-paste structs.
///
/// `return_to` is optional and only consulted by handlers that route
/// the user back to the source page (analyze, reset). Other handlers
/// ignore it. Validation (must start with `/web/`) lives at the call
/// site.
#[derive(serde::Deserialize)]
pub struct CsrfOnlyForm {
    #[serde(default)]
    pub csrf_token: String,
    #[serde(default)]
    pub return_to: Option<String>,
}

impl HasCsrfToken for CsrfOnlyForm {
    fn csrf_token(&self) -> &str {
        &self.csrf_token
    }
}
