//! HTMX authentication endpoints — passwordless OTP flow.
//!
//! Mirrors the Leptos OTP login flow as server-rendered HTML fragments so
//! the HTMX shell has identical login UX without WASM.
//!
//! Routes mounted under `/api/htmx/auth`:
//!
//! - `GET  /login-modal`    — injects the OTP email step into `#modal-slot`.
//! - `GET  /close`          — collapses the modal (empty `#modal-slot`).
//! - `POST /otp/request`    — sends OTP; on success swaps in the code form.
//! - `POST /otp/verify`     — verifies the code; on success `HX-Redirect: /` or
//!   swaps in the GDPR consent form for new accounts.
//! - `POST /otp/gdpr`       — accepts consent; creates account; `HX-Redirect:
//!   /`.
//! - `POST /logout`         — clears session cookie; `HX-Redirect: /`.
//!
//! Legacy password-based routes (`/login`, `/signup`, `/2fa/verify`) are kept
//! for compatibility but are no longer surfaced via the nav button.

#[cfg(feature = "auth")]
mod imp {
    use std::sync::Arc;

    use axum::extract::State;
    use axum::http::{HeaderMap, HeaderValue, StatusCode};
    use axum::response::{IntoResponse, Response};
    use axum::routing::{get, post};
    use axum::{Form, Router};
    use rustelo_core_lib::auth::{LoginCredentials, RegisterUserData};
    use serde::Deserialize;
    use tower_cookies::Cookies;

    use crate::api::htmx_sse::{publish_toast, ToastEvent, ToastLevel};
    use crate::auth::AuthService;

    /// Form payload accepted by `POST /login`. Matches the `name=` attributes
    /// emitted by [`render_login_form`].
    #[derive(Debug, Deserialize)]
    pub(super) struct LoginForm {
        pub(super) email: String,
        pub(super) password: String,
        #[serde(default)]
        pub(super) remember_me: Option<String>,
    }

    /// Form payload accepted by `POST /signup`.
    #[derive(Debug, Deserialize)]
    pub(super) struct SignupForm {
        pub(super) email: String,
        pub(super) username: String,
        pub(super) password: String,
        #[serde(default)]
        pub(super) display_name: Option<String>,
    }

    /// Form payload accepted by `POST /2fa/verify`.
    #[derive(Debug, Deserialize)]
    pub(super) struct TwoFactorForm {
        pub(super) email: String,
        pub(super) code: String,
    }

    /// Mount the HTMX auth endpoints onto a router with shared
    /// [`AuthService`](AuthService) state.
    pub fn htmx_auth_routes() -> Router<Arc<AuthService>> {
        Router::new()
            .route("/login-modal", get(login_modal))
            .route("/close", get(modal_close))
            // OTP / passwordless (primary login path — mirrors Leptos flow)
            .route("/otp/request", post(otp_request))
            .route("/otp/verify", post(otp_verify))
            .route("/otp/gdpr", post(otp_gdpr))
            // Legacy password-based (kept for compatibility)
            .route("/login", post(login))
            .route("/signup", post(signup))
            .route("/2fa/verify", post(two_factor_verify))
            .route("/oauth/{provider}/start", get(oauth_start))
            .route("/logout", post(logout))
    }

    /// Query parameters accepted by `GET /login-modal`.
    #[derive(Debug, serde::Deserialize, Default)]
    pub(super) struct LoginModalQuery {
        #[serde(default)]
        pub(super) lang: Option<String>,
        /// Post-login redirect target. Must be a relative path starting with
        /// `/`.
        #[serde(default, rename = "return")]
        pub(super) return_url: Option<String>,
    }

    async fn login_modal(
        axum::extract::Query(q): axum::extract::Query<LoginModalQuery>,
    ) -> Response {
        let lang = q.lang.as_deref().unwrap_or("en");
        let html = render_otp_modal(lang, q.return_url.as_deref());
        (StatusCode::OK, axum::response::Html(html)).into_response()
    }

    /// `GET /close` — replaces `#modal-slot` with an empty slot, closing the
    /// modal.
    async fn modal_close() -> Response {
        (
            StatusCode::OK,
            axum::response::Html(r#"<div id="modal-slot"></div>"#),
        )
            .into_response()
    }

    async fn login(
        State(auth_service): State<Arc<AuthService>>,
        cookies: Cookies,
        Form(payload): Form<LoginForm>,
    ) -> Response {
        let creds = LoginCredentials {
            email: payload.email.clone(),
            password: payload.password.clone(),
            remember_me: payload.remember_me.is_some(),
        };
        match auth_service.login(creds, Some(cookies)).await {
            Ok(resp) if resp.requires_2fa => {
                // Return a placeholder 2FA fragment. Full 2FA UX is layered
                // on top once F-06 adds the verify endpoint.
                let html = render_2fa_form(&payload.email);
                (StatusCode::OK, axum::response::Html(html)).into_response()
            }
            Ok(_) => {
                publish_toast(ToastEvent {
                    level: ToastLevel::Success,
                    message: "Signed in".to_string(),
                    id: None,
                });
                // Success: tell htmx to navigate to home so the page refreshes
                // with the new session cookie in place.
                let mut resp = StatusCode::OK.into_response();
                resp.headers_mut()
                    .insert("HX-Redirect", HeaderValue::from_static("/"));
                resp
            }
            Err(err) => {
                let html = render_login_form_with_error(&payload.email, &err.to_string());
                (StatusCode::OK, axum::response::Html(html)).into_response()
            }
        }
    }

    async fn signup(
        State(auth_service): State<Arc<AuthService>>,
        Form(payload): Form<SignupForm>,
    ) -> Response {
        let display = payload
            .display_name
            .clone()
            .filter(|s| !s.trim().is_empty());
        let data = RegisterUserData {
            email: payload.email.clone(),
            username: payload.username.clone(),
            password: payload.password.clone(),
            display_name: display,
            first_name: None,
            last_name: None,
        };
        match auth_service.register_user(data).await {
            Ok(_) => {
                publish_toast(ToastEvent {
                    level: ToastLevel::Success,
                    message: "Account created — please sign in".into(),
                    id: None,
                });
                let mut resp = StatusCode::OK.into_response();
                resp.headers_mut()
                    .insert("HX-Redirect", HeaderValue::from_static("/"));
                resp
            }
            Err(err) => {
                let html = render_signup_form_with_error(
                    &payload.email,
                    &payload.username,
                    &err.to_string(),
                );
                (StatusCode::OK, axum::response::Html(html)).into_response()
            }
        }
    }

    async fn two_factor_verify(
        State(auth_service): State<Arc<AuthService>>,
        cookies: Cookies,
        Form(payload): Form<TwoFactorForm>,
    ) -> Response {
        use rustelo_core_lib::auth::Login2FARequest;
        let req = Login2FARequest {
            email: payload.email.clone(),
            code: payload.code.clone(),
            remember_me: false,
        };
        match auth_service
            .login_with_2fa(req, Some(cookies), None, None)
            .await
        {
            Ok(_) => {
                publish_toast(ToastEvent {
                    level: ToastLevel::Success,
                    message: "Signed in".into(),
                    id: None,
                });
                let mut resp = StatusCode::OK.into_response();
                resp.headers_mut()
                    .insert("HX-Redirect", HeaderValue::from_static("/"));
                resp
            }
            Err(err) => {
                let html = render_2fa_form_with_error(&payload.email, &err.to_string());
                (StatusCode::OK, axum::response::Html(html)).into_response()
            }
        }
    }

    async fn oauth_start(axum::extract::Path(provider): axum::extract::Path<String>) -> Response {
        // OAuth dance is delegated to the existing /api/auth/oauth/{provider}/start
        // JSON endpoint. We just emit HX-Redirect so htmx triggers a real
        // browser navigation that follows the 302 chain to the provider.
        let target = format!("/api/auth/oauth/{provider}/start");
        let mut resp = StatusCode::OK.into_response();
        if let Ok(value) = HeaderValue::from_str(&target) {
            resp.headers_mut().insert("HX-Redirect", value);
        }
        resp
    }

    async fn logout(cookies: Cookies) -> Response {
        // Clear the session cookie regardless of whether one was present.
        cookies.add(
            tower_cookies::Cookie::build(("session", ""))
                .path("/")
                .max_age(time::Duration::seconds(0))
                .same_site(tower_cookies::cookie::SameSite::Lax)
                .build(),
        );
        publish_toast(ToastEvent {
            level: ToastLevel::Info,
            message: "Signed out".to_string(),
            id: None,
        });
        let mut resp = StatusCode::OK.into_response();
        resp.headers_mut()
            .insert("HX-Redirect", HeaderValue::from_static("/"));
        resp
    }

    // ── OTP form payloads ────────────────────────────────────────────────

    #[derive(Debug, Deserialize)]
    struct OtpRequestForm {
        email: String,
        #[serde(default)]
        lang: Option<String>,
        #[serde(default)]
        return_url: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    struct OtpVerifyForm {
        email: String,
        code: String,
        #[serde(default)]
        lang: Option<String>,
        #[serde(default)]
        return_url: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    struct OtpGdprForm {
        pending_token: String,
        #[serde(default)]
        gdpr_required: Option<String>,
        #[serde(default)]
        #[allow(dead_code)]
        gdpr_marketing: Option<String>,
        #[serde(default)]
        return_url: Option<String>,
    }

    // ── OTP handlers ─────────────────────────────────────────────────────

    /// `POST /otp/request` — send OTP to the submitted email address.
    ///
    /// On success swaps `#login-form` with the code-entry step.
    /// On failure re-renders the email step with an inline error.
    async fn otp_request(
        State(auth_service): State<Arc<AuthService>>,
        Form(payload): Form<OtpRequestForm>,
    ) -> Response {
        let lang = payload.lang.as_deref().unwrap_or("en");
        let return_url = payload.return_url.as_deref();
        let otp = match &auth_service.otp_service {
            Some(svc) => svc,
            None => {
                return (
                    StatusCode::OK,
                    axum::response::Html(render_otp_email_form(
                        None,
                        Some("OTP service not configured"),
                        lang,
                        return_url,
                    )),
                )
                    .into_response();
            }
        };
        let cfg = &auth_service.otp_config;
        match otp
            .request_otp(
                &payload.email,
                cfg.otp_code_length,
                cfg.otp_expiry_minutes,
                lang,
            )
            .await
        {
            Ok(()) => (
                StatusCode::OK,
                axum::response::Html(render_otp_code_form(&payload.email, None, lang, return_url)),
            )
                .into_response(),
            Err(e) => (
                StatusCode::OK,
                axum::response::Html(render_otp_email_form(
                    Some(&payload.email),
                    Some(&e.to_string()),
                    lang,
                    return_url,
                )),
            )
                .into_response(),
        }
    }

    /// `POST /otp/verify` — verify a 6-digit OTP code.
    ///
    /// Success (existing user): set session cookie + `HX-Redirect: /`.
    /// Success (new user): swap `#login-form` with the GDPR consent step.
    /// Failure: re-render code form with inline error.
    async fn otp_verify(
        State(auth_service): State<Arc<AuthService>>,
        Form(payload): Form<OtpVerifyForm>,
    ) -> Response {
        let lang = payload.lang.as_deref().unwrap_or("en");
        let return_url = payload.return_url.as_deref();
        let otp = match &auth_service.otp_service {
            Some(svc) => svc,
            None => {
                return (
                    StatusCode::OK,
                    axum::response::Html(render_otp_code_form(
                        &payload.email,
                        Some("OTP service not configured"),
                        lang,
                        return_url,
                    )),
                )
                    .into_response();
            }
        };
        let max_attempts = auth_service.otp_config.otp_max_attempts;
        use crate::auth::otp::OtpVerifyOutcome;
        match otp
            .verify_otp(&payload.email, &payload.code, max_attempts)
            .await
        {
            Ok(OtpVerifyOutcome::Authenticated(auth_resp)) => {
                let redirect_to = safe_return_url(payload.return_url.clone());
                let mut resp = StatusCode::OK.into_response();
                let cookie = format!(
                    "session_token={}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400",
                    auth_resp.access_token
                );
                if let Ok(v) = axum::http::HeaderValue::from_str(&cookie) {
                    resp.headers_mut().insert(axum::http::header::SET_COOKIE, v);
                }
                if let Ok(v) = axum::http::HeaderValue::try_from(redirect_to) {
                    resp.headers_mut().insert("HX-Redirect", v);
                }
                resp
            }
            Ok(OtpVerifyOutcome::NeedsConsent { pending_token }) => (
                StatusCode::OK,
                axum::response::Html(render_otp_gdpr_form(&pending_token, None, lang, return_url)),
            )
                .into_response(),
            Err(e) => (
                StatusCode::OK,
                axum::response::Html(render_otp_code_form(
                    &payload.email,
                    Some(&e.to_string()),
                    lang,
                    return_url,
                )),
            )
                .into_response(),
        }
    }

    /// `POST /otp/gdpr` — accept GDPR consent for a new account.
    ///
    /// Validates `pending_token`, creates the account, sets session cookie,
    /// then `HX-Redirect: /`.
    async fn otp_gdpr(
        State(auth_service): State<Arc<AuthService>>,
        Form(payload): Form<OtpGdprForm>,
    ) -> Response {
        let return_url = payload.return_url.as_deref();
        if payload.gdpr_required.is_none() {
            return (
                StatusCode::OK,
                axum::response::Html(render_otp_gdpr_form(
                    &payload.pending_token,
                    Some("You must accept the Terms of Service to continue."),
                    "en",
                    return_url,
                )),
            )
                .into_response();
        }
        let otp = match &auth_service.otp_service {
            Some(svc) => svc,
            None => {
                return (
                    StatusCode::OK,
                    axum::response::Html(render_otp_gdpr_form(
                        &payload.pending_token,
                        Some("OTP service not configured"),
                        "en",
                        return_url,
                    )),
                )
                    .into_response();
            }
        };
        match otp.accept_gdpr(&payload.pending_token).await {
            Ok(auth_resp) => {
                let redirect_to = safe_return_url(payload.return_url.clone());
                let mut resp = StatusCode::OK.into_response();
                let cookie = format!(
                    "session_token={}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400",
                    auth_resp.access_token
                );
                if let Ok(v) = axum::http::HeaderValue::from_str(&cookie) {
                    resp.headers_mut().insert(axum::http::header::SET_COOKIE, v);
                }
                if let Ok(v) = axum::http::HeaderValue::try_from(redirect_to) {
                    resp.headers_mut().insert("HX-Redirect", v);
                }
                resp
            }
            Err(e) => (
                StatusCode::OK,
                axum::response::Html(render_otp_gdpr_form(
                    &payload.pending_token,
                    Some(&e.to_string()),
                    "en",
                    return_url,
                )),
            )
                .into_response(),
        }
    }

    // ── OTP label helpers ─────────────────────────────────────────────────

    fn otp_labels(lang: &str) -> OtpLabels {
        if lang == "es" {
            OtpLabels {
                modal_title: "Iniciar sesión o registrarse",
                modal_close: "Cerrar",
                email_subtitle: "Introduce tu email para continuar",
                email_placeholder: "tu@ejemplo.com",
                continue_btn: "Continuar",
                check_inbox: "Comprueba tu bandeja de entrada",
                code_placeholder: "Código",
                resend_btn: "Reenviar código",
                back_btn: "← Cambiar email",
                verify_btn: "Continuar",
                gdpr_title: "Crea tu cuenta",
                gdpr_description: "Antes de crear tu cuenta, necesitamos tu consentimiento para \
                                   procesar tus datos de acuerdo con nuestra Política de \
                                   Privacidad.",
                gdpr_required_label: r#"Acepto los <a href="/privacidad" class="link link-primary" target="_blank" rel="noopener noreferrer">Términos de Servicio y la Política de Privacidad (obligatorio)</a>"#,
                gdpr_marketing_label: "Acepto recibir boletines ocasionales y novedades del \
                                       producto (opcional)",
                gdpr_submit_btn: "Crear cuenta",
            }
        } else {
            OtpLabels {
                modal_title: "Login or Sign up",
                modal_close: "Close",
                email_subtitle: "Enter your email to continue",
                email_placeholder: "you@example.com",
                continue_btn: "Continue",
                check_inbox: "Check your inbox",
                code_placeholder: "Code",
                resend_btn: "Resend code",
                back_btn: "← Change email",
                verify_btn: "Continue",
                gdpr_title: "Create your account",
                gdpr_description: "Before we create your account, we need your consent to process \
                                   your data in accordance with our Privacy Policy.",
                gdpr_required_label: r#"I accept the <a href="/privacy" class="link link-primary" target="_blank" rel="noopener noreferrer">Terms of Service and Privacy Policy (required)</a>"#,
                gdpr_marketing_label: "I agree to receive occasional newsletters and product \
                                       updates (optional)",
                gdpr_submit_btn: "Create account",
            }
        }
    }

    struct OtpLabels {
        modal_title: &'static str,
        modal_close: &'static str,
        email_subtitle: &'static str,
        email_placeholder: &'static str,
        continue_btn: &'static str,
        check_inbox: &'static str,
        code_placeholder: &'static str,
        resend_btn: &'static str,
        back_btn: &'static str,
        verify_btn: &'static str,
        gdpr_title: &'static str,
        gdpr_description: &'static str,
        gdpr_required_label: &'static str,
        gdpr_marketing_label: &'static str,
        gdpr_submit_btn: &'static str,
    }

    // ── OTP fragment renderers ────────────────────────────────────────────
    //
    // These functions render via Minijinja templates registered through
    // `rustelo_pages_htmx::register_fragment_env`. Templates live in
    // `rustelo_pages_htmx/templates/partials/auth/` and can be overridden
    // from the project's own templates directory.

    fn render_via_template(template: &str, ctx: minijinja::Value) -> Option<String> {
        let env = rustelo_pages_htmx::fragment_env()?;
        match env
            .get_template(template)
            .and_then(|t| t.render(ctx).map_err(Into::into))
        {
            Ok(html) => Some(html),
            Err(e) => {
                tracing::error!(template, error = %e, "auth template render failed");
                None
            }
        }
    }

    /// Validates that `url` is a safe relative redirect target (starts with
    /// `/`, not `//`). Prevents open redirect attacks from
    /// attacker-controlled form values.
    fn safe_return_url(url: Option<String>) -> String {
        url.filter(|u| u.starts_with('/') && !u.starts_with("//"))
            .unwrap_or_else(|| "/".to_string())
    }

    /// Full modal wrapper with the OTP email step as initial content.
    fn render_otp_modal(lang: &str, return_url: Option<&str>) -> String {
        let lb = otp_labels(lang);
        let form_html = render_otp_email_form(None, None, lang, return_url);
        let ctx = minijinja::context! {
            title => lb.modal_title,
            close_label => lb.modal_close,
            form_html => form_html,
        };
        render_via_template("partials/auth/otp_modal.j2", ctx).unwrap_or_else(|| {
            format!(r##"<div id="modal-slot"><div hx-get="/api/htmx/auth/close" hx-target="#modal-slot" hx-swap="outerHTML"></div><div class="fixed inset-0 z-[85] flex items-center justify-center p-4"><div class="bg-base-100 p-8 w-full max-w-md">{form_html}</div></div></div>"##,
                form_html = form_html)
        })
    }

    /// Step 1: email input form.
    fn render_otp_email_form(
        email: Option<&str>,
        error: Option<&str>,
        lang: &str,
        return_url: Option<&str>,
    ) -> String {
        let lb = otp_labels(lang);
        let ctx = minijinja::context! {
            lang => lang,
            subtitle => lb.email_subtitle,
            placeholder => lb.email_placeholder,
            continue_btn => lb.continue_btn,
            email => email.unwrap_or(""),
            error => error.unwrap_or(""),
            return_url => return_url.unwrap_or(""),
        };
        render_via_template("partials/auth/otp_email.j2", ctx).unwrap_or_else(|| {
            let ret_field = return_url.map(|r| format!(
                r#"<input type="hidden" name="return_url" value="{}">"#,
                html_escape::encode_double_quoted_attribute(r)
            )).unwrap_or_default();
            format!(
                r##"<form id="login-form" hx-post="/api/htmx/auth/otp/request" hx-target="#login-form" hx-swap="outerHTML"><input type="hidden" name="lang" value="{lang}">{ret_field}<input type="email" name="email" required placeholder="{ph}"><button type="submit">{btn}</button></form>"##,
                ph = html_escape::encode_double_quoted_attribute(lb.email_placeholder),
                btn = html_escape::encode_text(lb.continue_btn),
            )
        })
    }

    /// Step 2: OTP code entry form.
    fn render_otp_code_form(
        email: &str,
        error: Option<&str>,
        lang: &str,
        return_url: Option<&str>,
    ) -> String {
        let lb = otp_labels(lang);
        let sent_to = format!(
            "Enter the verification code we sent to <strong>{}</strong>",
            html_escape::encode_text(email)
        );
        // Back button re-fetches the modal, preserving return_url in the query.
        let back_btn_url = match return_url {
            Some(r) => format!(
                "/api/htmx/auth/login-modal?lang={}&return={}",
                html_escape::encode_double_quoted_attribute(lang),
                html_escape::encode_double_quoted_attribute(r),
            ),
            None => format!(
                "/api/htmx/auth/login-modal?lang={}",
                html_escape::encode_double_quoted_attribute(lang)
            ),
        };
        let ctx = minijinja::context! {
            lang => lang,
            email => email,
            sent_to => sent_to,
            check_inbox => lb.check_inbox,
            placeholder => lb.code_placeholder,
            verify_btn => lb.verify_btn,
            resend_btn => lb.resend_btn,
            back_btn => lb.back_btn,
            back_btn_url => back_btn_url,
            error => error.unwrap_or(""),
            return_url => return_url.unwrap_or(""),
        };
        render_via_template("partials/auth/otp_code.j2", ctx).unwrap_or_else(|| {
            let ret_field = return_url.map(|r| format!(
                r#"<input type="hidden" name="return_url" value="{}">"#,
                html_escape::encode_double_quoted_attribute(r)
            )).unwrap_or_default();
            format!(
                r##"<form id="login-form" hx-post="/api/htmx/auth/otp/verify" hx-target="#login-form" hx-swap="outerHTML"><input type="hidden" name="email" value="{email}"><input type="hidden" name="lang" value="{lang}">{ret_field}<input type="text" name="code" inputmode="numeric" maxlength="6" required><button type="submit">{btn}</button></form>"##,
                email = html_escape::encode_double_quoted_attribute(email),
                btn = html_escape::encode_text(lb.verify_btn),
            )
        })
    }

    /// Step 3: GDPR consent form (new accounts only).
    fn render_otp_gdpr_form(
        pending_token: &str,
        error: Option<&str>,
        lang: &str,
        return_url: Option<&str>,
    ) -> String {
        let lb = otp_labels(lang);
        let ctx = minijinja::context! {
            pending_token => pending_token,
            gdpr_title => lb.gdpr_title,
            gdpr_description => lb.gdpr_description,
            gdpr_required_label => lb.gdpr_required_label,
            gdpr_marketing_label => lb.gdpr_marketing_label,
            gdpr_submit_btn => lb.gdpr_submit_btn,
            error => error.unwrap_or(""),
            return_url => return_url.unwrap_or(""),
        };
        render_via_template("partials/auth/otp_gdpr.j2", ctx).unwrap_or_else(|| {
            let ret_field = return_url.map(|r| format!(
                r#"<input type="hidden" name="return_url" value="{}">"#,
                html_escape::encode_double_quoted_attribute(r)
            )).unwrap_or_default();
            format!(
                r##"<form id="login-form" hx-post="/api/htmx/auth/otp/gdpr" hx-target="#login-form" hx-swap="outerHTML"><input type="hidden" name="pending_token" value="{token}">{ret_field}<label><input type="checkbox" name="gdpr_required" value="1" required><span>{label}</span></label><button type="submit">{btn}</button></form>"##,
                token = html_escape::encode_double_quoted_attribute(pending_token),
                label = lb.gdpr_required_label,
                btn = html_escape::encode_text(lb.gdpr_submit_btn),
            )
        })
    }

    #[cfg(test)]
    fn render_login_modal(lang: &str) -> String {
        let (sign_in_label, close_label, ..) = modal_labels(lang);
        // Backdrop and panel are siblings so click events on the panel do NOT
        // bubble to the backdrop — no stopPropagation needed. Clicking the
        // backdrop calls GET /close which returns an empty #modal-slot.
        format!(
            r##"<div id="modal-slot"><div class="fixed inset-0 z-[80] bg-black opacity-75 backdrop-blur-sm cursor-pointer" hx-get="/api/htmx/auth/close" hx-target="#modal-slot" hx-swap="outerHTML" aria-hidden="true"></div><div class="fixed inset-0 z-[85] flex items-center justify-center p-4 pointer-events-none"><div role="dialog" aria-modal="true" aria-labelledby="login-modal-title" class="relative w-full max-w-md bg-base-100 rounded-2xl shadow-xl p-8 pointer-events-auto"><div class="flex justify-between items-center mb-6"><h2 id="login-modal-title" class="text-xl font-semibold ds-text">{sign_in}</h2><button type="button" class="absolute top-4 right-4 btn btn-ghost btn-sm btn-circle" hx-get="/api/htmx/auth/close" hx-target="#modal-slot" hx-swap="outerHTML" aria-label="{close}" title="{close}">✕</button></div>{form}</div></div></div>"##,
            sign_in = html_escape::encode_text(sign_in_label),
            close = html_escape::encode_double_quoted_attribute(close_label),
            form = render_login_form_inner(None, None, lang)
        )
    }

    /// Returns (sign_in, close, email, password, remember, submit) label tuple.
    fn modal_labels(
        lang: &str,
    ) -> (
        &'static str,
        &'static str,
        &'static str,
        &'static str,
        &'static str,
        &'static str,
    ) {
        if lang == "es" {
            (
                "Iniciar sesión",
                "Cerrar",
                "Correo electrónico",
                "Contraseña",
                "Recordarme",
                "Entrar",
            )
        } else {
            (
                "Sign in",
                "Close",
                "Email",
                "Password",
                "Remember me",
                "Sign in",
            )
        }
    }

    fn render_login_form(email: Option<&str>, error: Option<&str>, lang: &str) -> String {
        format!(
            r#"<div id="login-form">{}</div>"#,
            render_login_form_inner(email, error, lang)
        )
    }

    fn render_login_form_with_error(email: &str, error: &str) -> String {
        render_login_form(Some(email), Some(error), "en")
    }

    fn render_login_form_inner(email: Option<&str>, error: Option<&str>, lang: &str) -> String {
        let (_, _, email_label, password_label, remember_label, submit_label) = modal_labels(lang);
        let email_attr = email
            .map(html_escape::encode_double_quoted_attribute)
            .unwrap_or_default();
        let error_block = error
            .map(|e| {
                format!(
                    r#"<div class="ds-alert-error" role="alert">{}</div>"#,
                    html_escape::encode_text(e)
                )
            })
            .unwrap_or_default();
        format!(
            r##"<form id="login-form" hx-post="/api/htmx/auth/login" hx-target="#login-form" hx-swap="outerHTML" class="space-y-4">{error_block}<div class="ds-field"><label for="login-email">{email_label}</label><input type="email" id="login-email" name="email" required value="{email_attr}"></div><div class="ds-field"><label for="login-password">{password_label}</label><input type="password" id="login-password" name="password" required></div><div class="ds-field flex items-center gap-2"><input type="checkbox" id="login-remember" name="remember_me" value="1"><label for="login-remember" class="text-sm">{remember_label}</label></div><button type="submit" class="ds-btn-primary w-full">{submit_label}</button></form>"##,
            email_label = html_escape::encode_text(email_label),
            password_label = html_escape::encode_text(password_label),
            remember_label = html_escape::encode_text(remember_label),
            submit_label = html_escape::encode_text(submit_label),
        )
    }

    fn render_2fa_form(email: &str) -> String {
        render_2fa_form_inner(email, None)
    }

    fn render_2fa_form_with_error(email: &str, error: &str) -> String {
        render_2fa_form_inner(email, Some(error))
    }

    fn render_2fa_form_inner(email: &str, error: Option<&str>) -> String {
        let email_attr = html_escape::encode_double_quoted_attribute(email);
        let error_block = error
            .map(|e| {
                format!(
                    r#"<div class="ds-alert-error" role="alert">{}</div>"#,
                    html_escape::encode_text(e)
                )
            })
            .unwrap_or_default();
        format!(
            r##"<form id="login-form" hx-post="/api/htmx/auth/2fa/verify" hx-target="#login-form" hx-swap="outerHTML" class="space-y-4">{error_block}<p class="text-sm">Two-factor authentication required.</p><input type="hidden" name="email" value="{email_attr}"><div class="ds-field"><label for="2fa-code">Authenticator code</label><input type="text" id="2fa-code" name="code" inputmode="numeric" pattern="[0-9]{{6}}" required autofocus></div><button type="submit" class="ds-btn-primary w-full">Verify</button></form>"##
        )
    }

    fn render_signup_form(
        email: Option<&str>,
        username: Option<&str>,
        error: Option<&str>,
    ) -> String {
        let email_attr = email
            .map(html_escape::encode_double_quoted_attribute)
            .unwrap_or_default();
        let username_attr = username
            .map(html_escape::encode_double_quoted_attribute)
            .unwrap_or_default();
        let error_block = error
            .map(|e| {
                format!(
                    r#"<div class="ds-alert-error" role="alert">{}</div>"#,
                    html_escape::encode_text(e)
                )
            })
            .unwrap_or_default();
        format!(
            r##"<form id="signup-form" hx-post="/api/htmx/auth/signup" hx-target="#signup-form" hx-swap="outerHTML" class="space-y-4">{error_block}<div class="ds-field"><label for="signup-email">Email</label><input type="email" id="signup-email" name="email" required value="{email_attr}"></div><div class="ds-field"><label for="signup-username">Username</label><input type="text" id="signup-username" name="username" required value="{username_attr}"></div><div class="ds-field"><label for="signup-password">Password</label><input type="password" id="signup-password" name="password" required minlength="8"></div><div class="ds-field"><label for="signup-display">Display name (optional)</label><input type="text" id="signup-display" name="display_name"></div><button type="submit" class="ds-btn-primary w-full">Create account</button></form>"##
        )
    }

    fn render_signup_form_with_error(email: &str, username: &str, error: &str) -> String {
        render_signup_form(Some(email), Some(username), Some(error))
    }

    /// `HX-Request: true` aware. Returns whether the caller is an HTMX swap.
    #[allow(dead_code)]
    fn is_htmx(headers: &HeaderMap) -> bool {
        headers
            .get("hx-request")
            .and_then(|v| v.to_str().ok())
            .map(|s| s.eq_ignore_ascii_case("true"))
            .unwrap_or(false)
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn modal_carries_slot_id_and_form_endpoint() {
            let html = render_login_modal("en");
            assert!(html.contains(r#"id="modal-slot""#));
            assert!(html.contains(r#"hx-post="/api/htmx/auth/login""#));
        }

        #[test]
        fn modal_close_button_targets_close_endpoint() {
            let html = render_login_modal("en");
            assert!(html.contains(r#"hx-get="/api/htmx/auth/close""#));
        }

        #[test]
        fn modal_backdrop_click_closes() {
            let html = render_login_modal("en");
            assert!(html.contains("fixed inset-0"));
            assert!(html.contains(r#"hx-get="/api/htmx/auth/close""#));
        }

        #[test]
        fn modal_spanish_labels() {
            let html = render_login_modal("es");
            assert!(html.contains("Iniciar sesión"));
        }

        #[test]
        fn form_with_error_renders_alert() {
            let html = render_login_form_with_error("u@x", "Invalid credentials");
            assert!(html.contains("Invalid credentials"));
            assert!(html.contains(r#"role="alert""#));
            assert!(html.contains(r#"value="u@x""#));
        }

        #[test]
        fn fresh_form_omits_error() {
            let html = render_login_form_inner(None, None, "en");
            assert!(!html.contains(r#"role="alert""#));
        }

        #[test]
        fn two_fa_form_uses_pattern_input() {
            let html = render_2fa_form("u@x");
            assert!(html.contains(r#"name="code""#));
            assert!(html.contains("pattern="));
        }
    }
}

#[cfg(feature = "auth")]
pub use imp::htmx_auth_routes;
