use async_trait::async_trait;
use athena_auth_core::adapters::DatabaseAdapter;
use athena_auth_core::entity::{AuthSession, AuthUser};
use athena_auth_core::{AuthContext, AuthPlugin, AuthRoute};
use athena_auth_core::{AuthError, AuthResult};
use athena_auth_core::{
    AuthRequest, AuthResponse, CreateUser, CreateVerification, HttpMethod, PASSWORD_HASH_KEY,
};
use athena_auth_core::{SessionOps, UserOps};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::sync::Arc;
use tracing;
use validator::Validate;

use super::email_verification::EmailVerificationPlugin;
use super::helpers::{
    SessionWithServerVersion, extract_session_client_info, resolve_user_name_from_email,
    session_with_server_version, spawn_background_task, write_sign_in_audit_in_background,
};
use athena_auth_core::utils::cookie_utils::create_session_cookie;
use athena_auth_core::utils::password::{self as password_utils, PasswordHasher};
/// Email and password authentication plugin
pub struct EmailPasswordPlugin {
    config: EmailPasswordConfig,
    /// Optional reference to the email-verification plugin so that
    /// `send_on_sign_in` can be triggered during the sign-in flow.
    email_verification: Option<Arc<EmailVerificationPlugin>>,
}

#[derive(Clone)]
pub struct EmailPasswordConfig {
    pub enable_signup: bool,
    pub require_email_verification: bool,
    pub password_min_length: usize,
    /// Maximum password length (default: 128).
    pub password_max_length: usize,
    /// Whether to automatically sign in the user after sign-up (default: true).
    /// When false, sign-up returns the user but doesn't create a session.
    pub auto_sign_in: bool,
    /// Custom password hasher. When `None`, the default Argon2 hasher is used.
    pub password_hasher: Option<Arc<dyn PasswordHasher>>,
}

impl std::fmt::Debug for EmailPasswordConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("EmailPasswordConfig")
            .field("enable_signup", &self.enable_signup)
            .field(
                "require_email_verification",
                &self.require_email_verification,
            )
            .field("password_min_length", &self.password_min_length)
            .field("password_max_length", &self.password_max_length)
            .field("auto_sign_in", &self.auto_sign_in)
            .field(
                "password_hasher",
                &self.password_hasher.as_ref().map(|_| "custom"),
            )
            .finish()
    }
}

#[derive(Debug, Deserialize, Validate)]
#[allow(dead_code)]
pub(crate) struct SignUpRequest {
    name: Option<String>,
    #[validate(email(message = "Invalid email address"))]
    email: String,
    #[validate(length(min = 1, message = "Password is required"))]
    password: String,
    username: Option<String>,
    #[serde(rename = "displayUsername")]
    display_username: Option<String>,
    #[serde(rename = "callbackURL")]
    callback_url: Option<String>,
}

#[derive(Debug, Deserialize, Validate)]
#[allow(dead_code)]
pub(crate) struct SignInRequest {
    #[validate(email(message = "Invalid email address"))]
    email: String,
    #[validate(length(min = 1, message = "Password is required"))]
    password: String,
    #[serde(rename = "callbackURL")]
    callback_url: Option<String>,
    #[serde(rename = "rememberMe")]
    remember_me: Option<bool>,
}

#[derive(Debug, Deserialize, Validate)]
#[allow(dead_code)]
pub(crate) struct SignInUsernameRequest {
    #[validate(length(min = 1, message = "Username is required"))]
    username: String,
    #[validate(length(min = 1, message = "Password is required"))]
    password: String,
    #[serde(rename = "rememberMe")]
    remember_me: Option<bool>,
}

#[derive(Debug, Serialize)]
pub(crate) struct SignUpResponse<U: Serialize, S: Serialize> {
    token: Option<String>,
    user: U,
    #[serde(skip_serializing_if = "Option::is_none")]
    session: Option<SessionWithServerVersion<S>>,
}

#[derive(Debug, Serialize)]
pub(crate) struct SignInResponse<U: Serialize, S: Serialize> {
    redirect: bool,
    token: String,
    url: Option<String>,
    user: U,
    session: SessionWithServerVersion<S>,
}

/// 2FA redirect response returned when the user has 2FA enabled.
#[derive(Debug, Serialize)]
pub(crate) struct TwoFactorRedirectResponse {
    #[serde(rename = "twoFactorRedirect")]
    two_factor_redirect: bool,
    token: String,
}

/// Result of sign-in: either a successful session or a 2FA redirect.
pub(crate) enum SignInCoreResult<U: Serialize, S: Serialize> {
    Success(SignInResponse<U, S>, String),
    TwoFactorRedirect(TwoFactorRedirectResponse),
}

/// Aggregates dependencies and request metadata for the sign-in core path.
struct SignInCoreInput<'a, DB: DatabaseAdapter> {
    config: &'a EmailPasswordConfig,
    email_verification: Option<&'a EmailVerificationPlugin>,
    audit_action: &'static str,
    callback_url: Option<&'a str>,
    ip_address: Option<String>,
    user_agent: Option<String>,
    ctx: &'a AuthContext<DB>,
}

impl EmailPasswordPlugin {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            config: EmailPasswordConfig::default(),
            email_verification: None,
        }
    }

    pub fn with_config(config: EmailPasswordConfig) -> Self {
        Self {
            config,
            email_verification: None,
        }
    }

    /// Attach an [`EmailVerificationPlugin`] so that `send_on_sign_in` is
    /// automatically called when a user signs in with an unverified email.
    pub fn with_email_verification(mut self, plugin: Arc<EmailVerificationPlugin>) -> Self {
        self.email_verification = Some(plugin);
        self
    }

    pub fn enable_signup(mut self, enable: bool) -> Self {
        self.config.enable_signup = enable;
        self
    }

    pub fn require_email_verification(mut self, require: bool) -> Self {
        self.config.require_email_verification = require;
        self
    }

    pub fn password_min_length(mut self, length: usize) -> Self {
        self.config.password_min_length = length;
        self
    }

    pub fn password_max_length(mut self, length: usize) -> Self {
        self.config.password_max_length = length;
        self
    }

    pub fn auto_sign_in(mut self, auto: bool) -> Self {
        self.config.auto_sign_in = auto;
        self
    }

    pub fn password_hasher(mut self, hasher: Arc<dyn PasswordHasher>) -> Self {
        self.config.password_hasher = Some(hasher);
        self
    }

    async fn handle_sign_up<DB: DatabaseAdapter>(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<AuthResponse> {
        let signup_req: SignUpRequest = match athena_auth_core::validate_request_body(req) {
            Ok(v) => v,
            Err(resp) => return Ok(resp),
        };
        let (ip_address, user_agent) = extract_session_client_info(ctx, req);
        let (response, session_token) =
            sign_up_core(&signup_req, &self.config, ip_address, user_agent, ctx).await?;

        if let Some(token) = session_token {
            let cookie_header = create_session_cookie(&token, &ctx.config);
            Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
        } else {
            Ok(AuthResponse::json(200, &response)?)
        }
    }

    async fn handle_sign_in<DB: DatabaseAdapter>(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<AuthResponse> {
        let signin_req: SignInRequest = match athena_auth_core::validate_request_body(req) {
            Ok(v) => v,
            Err(resp) => return Ok(resp),
        };
        let (ip_address, user_agent) = extract_session_client_info(ctx, req);

        match sign_in_core(
            &signin_req,
            &self.config,
            self.email_verification.as_deref(),
            ip_address,
            user_agent,
            ctx,
        )
        .await?
        {
            SignInCoreResult::Success(response, token) => {
                let cookie_header = create_session_cookie(&token, &ctx.config);
                Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
            }
            SignInCoreResult::TwoFactorRedirect(redirect) => {
                Ok(AuthResponse::json(200, &redirect)?)
            }
        }
    }

    async fn handle_sign_in_username<DB: DatabaseAdapter>(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<AuthResponse> {
        let signin_req: SignInUsernameRequest = match athena_auth_core::validate_request_body(req) {
            Ok(v) => v,
            Err(resp) => return Ok(resp),
        };
        let (ip_address, user_agent) = extract_session_client_info(ctx, req);

        match sign_in_username_core(
            &signin_req,
            &self.config,
            self.email_verification.as_deref(),
            ip_address,
            user_agent,
            ctx,
        )
        .await?
        {
            SignInCoreResult::Success(response, token) => {
                let cookie_header = create_session_cookie(&token, &ctx.config);
                Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
            }
            SignInCoreResult::TwoFactorRedirect(redirect) => {
                Ok(AuthResponse::json(200, &redirect)?)
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Core functions — framework-agnostic business logic
// ---------------------------------------------------------------------------

/// Core sign-up logic.
///
/// Returns `(response, Option<session_token>)`. The session token is present
/// only when `auto_sign_in` is true.
pub(crate) async fn sign_up_core<DB: DatabaseAdapter>(
    body: &SignUpRequest,
    config: &EmailPasswordConfig,
    ip_address: Option<String>,
    user_agent: Option<String>,
    ctx: &AuthContext<DB>,
) -> AuthResult<(SignUpResponse<DB::User, DB::Session>, Option<String>)> {
    if !config.enable_signup {
        tracing::warn!("sign-up rejected: registration disabled");
        return Err(AuthError::forbidden("User registration is not enabled"));
    }

    password_utils::validate_password(
        &body.password,
        config.password_min_length,
        config.password_max_length,
        ctx,
    )?;

    // Check if user already exists
    if ctx.database.get_user_by_email(&body.email).await?.is_some() {
        tracing::warn!(email = %body.email, "sign-up rejected: email already registered");
        return Err(AuthError::conflict("A user with this email already exists"));
    }

    let password_hash: String = password_utils::hash_password_with_config(
        config.password_hasher.as_ref(),
        Some(&ctx.config.password.argon2_config),
        &body.password,
    )
    .await?;

    let metadata: Value = {
        let mut m: Map<String, Value> = Map::new();
        m.insert(PASSWORD_HASH_KEY.to_string(), Value::String(password_hash));
        Value::Object(m)
    };

    let resolved_name = resolve_user_name_from_email(&body.email, body.name.as_deref())?;

    let mut create_user: CreateUser = CreateUser::new()
        .with_email(&body.email)
        .with_name(resolved_name.clone());
    if let Some(ref username) = body.username {
        create_user = create_user.with_username(username.clone());
    }
    if let Some(ref display_username) = body.display_username {
        create_user.display_username = Some(display_username.clone());
    }
    create_user.metadata = Some(metadata);

    let user: <DB as UserOps>::User = ctx.database.create_user(create_user).await?;

    tracing::info!(
        user_id = %user.id(),
        email = %body.email,
        name = %resolved_name,
        auto_sign_in = config.auto_sign_in,
        "user registered (email/password)"
    );

    if config.auto_sign_in {
        let session_result = ctx
            .session_manager()
            .create_session_with_user_projection(&user, ip_address, user_agent)
            .await?;
        let session: <DB as SessionOps>::Session = session_result.session;
        let token: String = session.token().to_string();
        let user = session_result.updated_user.unwrap_or(user);

        let response: SignUpResponse<<DB as UserOps>::User, <DB as SessionOps>::Session> =
            SignUpResponse {
                token: Some(token.clone()),
                user,
                session: Some(session_with_server_version(session)),
            };
        Ok((response, Some(token)))
    } else {
        let response: SignUpResponse<<DB as UserOps>::User, <DB as SessionOps>::Session> =
            SignUpResponse {
                token: None,
                user,
                session: None,
            };
        Ok((response, None))
    }
}

/// Shared sign-in logic after user lookup: verify password, check 2FA, create session.
async fn sign_in_with_user_core<DB: DatabaseAdapter>(
    user: DB::User,
    password: &str,
    input: SignInCoreInput<'_, DB>,
) -> AuthResult<SignInCoreResult<DB::User, DB::Session>> {
    let SignInCoreInput {
        config,
        email_verification,
        audit_action,
        callback_url,
        ip_address,
        user_agent,
        ctx,
    } = input;

    let stored_hash: &str = user.password_hash().ok_or(AuthError::InvalidCredentials)?;

    if let Err(error) = password_utils::verify_password_with_config(
        config.password_hasher.as_ref(),
        Some(&ctx.config.password.argon2_config),
        password,
        stored_hash,
    )
    .await
    {
        let error_message = if matches!(&error, AuthError::InvalidCredentials) {
            tracing::warn!(
                user_id = %user.id(),
                email = %user.email().unwrap_or(""),
                "sign-in failed: invalid password"
            );
            "Invalid credentials".to_string()
        } else {
            tracing::error!(
                user_id = %user.id(),
                email = %user.email().unwrap_or(""),
                error = %error,
                "sign-in failed: password verification error"
            );
            error.to_string()
        };

        write_sign_in_audit_in_background(
            ctx.clone(),
            audit_action.to_string(),
            Some(user.id().to_string()),
            None,
            Some("user_id".to_string()),
            Some(user.id().to_string()),
            false,
            Some(error_message),
            ip_address.clone(),
            user_agent.clone(),
            Some(serde_json::json!({
                "email": user.email(),
            })),
        );

        return Err(error);
    }

    if user.two_factor_enabled() {
        tracing::info!(
            user_id = %user.id(),
            email = %user.email().unwrap_or(""),
            "sign-in: 2FA challenge issued"
        );
        let pending_token: String = format!("2fa_{}", uuid::Uuid::new_v4());
        ctx.database
            .create_verification(CreateVerification {
                identifier: format!("2fa_pending:{}", pending_token),
                value: user.id().to_string(),
                expires_at: chrono::Utc::now() + chrono::Duration::minutes(5),
            })
            .await?;
        write_sign_in_audit_in_background(
            ctx.clone(),
            audit_action.to_string(),
            Some(user.id().to_string()),
            None,
            Some("user_id".to_string()),
            Some(user.id().to_string()),
            false,
            Some("Two-factor authentication required".to_string()),
            ip_address.clone(),
            user_agent.clone(),
            Some(serde_json::json!({
                "email": user.email(),
                "twoFactorRequired": true,
            })),
        );
        return Ok(SignInCoreResult::TwoFactorRedirect(
            TwoFactorRedirectResponse {
                two_factor_redirect: true,
                token: pending_token,
            },
        ));
    }

    // Send verification email on sign-in if configured
    if let Some(ev) = email_verification {
        ev.spawn_send_verification_on_sign_in(
            user.clone(),
            callback_url.map(str::to_string),
            ctx.clone(),
        );
    }

    let audit_ip = ip_address.clone();
    let audit_user_agent = user_agent.clone();
    let session_result = ctx
        .session_manager()
        .create_session_with_user_projection(&user, ip_address, user_agent)
        .await?;
    maybe_spawn_password_rehash(&user, stored_hash, password, config, ctx);
    let session: <DB as SessionOps>::Session = session_result.session;
    let token: String = session.token().to_string();
    let user_id_for_logs = user.id().to_string();
    let user_email_for_logs = user.email().unwrap_or("").to_string();
    let response_user = session_result.updated_user.unwrap_or(user);

    tracing::info!(
        user_id = %user_id_for_logs,
        email = %user_email_for_logs,
        "sign-in succeeded, session created"
    );
    write_sign_in_audit_in_background(
        ctx.clone(),
        audit_action.to_string(),
        Some(user_id_for_logs.clone()),
        Some(token.clone()),
        Some("user_id".to_string()),
        Some(user_id_for_logs.clone()),
        true,
        None,
        audit_ip,
        audit_user_agent,
        Some(serde_json::json!({
            "email": user_email_for_logs,
        })),
    );

    let response: SignInResponse<<DB as UserOps>::User, <DB as SessionOps>::Session> =
        SignInResponse {
            redirect: false,
            token: token.clone(),
            url: None,
            user: response_user,
            session: session_with_server_version(session),
        };
    Ok(SignInCoreResult::Success(response, token))
}

fn maybe_spawn_password_rehash<DB: DatabaseAdapter>(
    user: &DB::User,
    stored_hash: &str,
    password: &str,
    config: &EmailPasswordConfig,
    ctx: &AuthContext<DB>,
) {
    if config.password_hasher.is_some() {
        return;
    }

    let needs_rehash = match athena_auth_core::password_hash_needs_rehash(
        stored_hash,
        &ctx.config.password.argon2_config,
    ) {
        Ok(needs_rehash) => needs_rehash,
        Err(error) => {
            tracing::warn!(
                user_id = %user.id(),
                error = %error,
                "skipping password rehash because stored hash params could not be inspected"
            );
            return;
        }
    };

    if !needs_rehash {
        return;
    }

    let ctx = ctx.clone();
    let user_id = user.id().to_string();
    let user_email = user.email().unwrap_or("").to_string();
    let password = password.to_string();
    let mut metadata = user.metadata().clone();

    spawn_background_task("password_rehash", async move {
        let new_hash = match password_utils::hash_password_with_config(
            None,
            Some(&ctx.config.password.argon2_config),
            &password,
        )
        .await
        {
            Ok(hash) => hash,
            Err(error) => {
                tracing::warn!(
                    user_id = %user_id,
                    email = %user_email,
                    error = %error,
                    "password rehash failed during sign-in background upgrade"
                );
                return;
            }
        };

        match &mut metadata {
            Value::Object(object) => {
                object.insert(PASSWORD_HASH_KEY.to_string(), Value::String(new_hash));
            }
            _ => {
                let mut object = Map::new();
                object.insert(PASSWORD_HASH_KEY.to_string(), Value::String(new_hash));
                metadata = Value::Object(object);
            }
        }

        if let Err(error) = ctx
            .database
            .update_user(
                &user_id,
                athena_auth_core::utils::password::update_user_metadata(metadata),
            )
            .await
        {
            tracing::warn!(
                user_id = %user_id,
                email = %user_email,
                error = %error,
                "password rehash update failed during sign-in background upgrade"
            );
        }
    });
}

/// `sign_in_core` is the core function for signing in with an email address.
pub(crate) async fn sign_in_core<DB: DatabaseAdapter>(
    body: &SignInRequest,
    config: &EmailPasswordConfig,
    email_verification: Option<&EmailVerificationPlugin>,
    ip_address: Option<String>,
    user_agent: Option<String>,
    ctx: &AuthContext<DB>,
) -> AuthResult<SignInCoreResult<DB::User, DB::Session>> {
    let user: <DB as UserOps>::User = match ctx.database.get_user_by_email(&body.email).await? {
        Some(user) => user,
        None => {
            tracing::warn!(email = %body.email, "sign-in failed: invalid credentials");
            write_sign_in_audit_in_background(
                ctx.clone(),
                "auth.sign_in.email",
                None,
                None,
                Some("email".to_string()),
                Some(body.email.clone()),
                false,
                Some("Invalid credentials".to_string()),
                ip_address.clone(),
                user_agent.clone(),
                None,
            );
            return Err(AuthError::InvalidCredentials);
        }
    };

    let input = SignInCoreInput {
        config,
        email_verification,
        audit_action: "auth.sign_in.email",
        callback_url: body.callback_url.as_deref(),
        ip_address,
        user_agent,
        ctx,
    };

    sign_in_with_user_core(user, &body.password, input).await
}

/// Core sign-in by username.
pub(crate) async fn sign_in_username_core<DB: DatabaseAdapter>(
    body: &SignInUsernameRequest,
    config: &EmailPasswordConfig,
    email_verification: Option<&EmailVerificationPlugin>,
    ip_address: Option<String>,
    user_agent: Option<String>,
    ctx: &AuthContext<DB>,
) -> AuthResult<SignInCoreResult<DB::User, DB::Session>> {
    let user: <DB as UserOps>::User = match ctx
        .database
        .get_user_by_username(&body.username)
        .await?
    {
        Some(user) => user,
        None => {
            tracing::warn!(username = %body.username, "username sign-in failed: invalid credentials");
            write_sign_in_audit_in_background(
                ctx.clone(),
                "auth.sign_in.username",
                None,
                None,
                Some("username".to_string()),
                Some(body.username.clone()),
                false,
                Some("Invalid credentials".to_string()),
                ip_address.clone(),
                user_agent.clone(),
                None,
            );
            return Err(AuthError::InvalidCredentials);
        }
    };

    let input = SignInCoreInput {
        config,
        email_verification,
        audit_action: "auth.sign_in.username",
        callback_url: None,
        ip_address,
        user_agent,
        ctx,
    };

    sign_in_with_user_core(user, &body.password, input).await
}

impl Default for EmailPasswordConfig {
    fn default() -> Self {
        Self {
            enable_signup: true,
            require_email_verification: false,
            password_min_length: 8,
            password_max_length: 128,
            auto_sign_in: true,
            password_hasher: None,
        }
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<DB: DatabaseAdapter> AuthPlugin<DB> for EmailPasswordPlugin {
    fn name(&self) -> &'static str {
        "email-password"
    }

    fn routes(&self) -> Vec<AuthRoute> {
        let mut routes: Vec<AuthRoute> = vec![
            AuthRoute::post("/sign-in/email", "sign_in_email"),
            AuthRoute::post("/sign-in/username", "sign_in_username"),
        ];

        if self.config.enable_signup {
            routes.push(AuthRoute::post("/sign-up/email", "sign_up_email"));
        }

        routes
    }

    async fn on_request(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<Option<AuthResponse>> {
        match (req.method(), req.path()) {
            (HttpMethod::Post, "/sign-up/email") if self.config.enable_signup => {
                Ok(Some(self.handle_sign_up(req, ctx).await?))
            }
            (HttpMethod::Post, "/sign-in/email") => Ok(Some(self.handle_sign_in(req, ctx).await?)),
            (HttpMethod::Post, "/sign-in/username") => {
                Ok(Some(self.handle_sign_in_username(req, ctx).await?))
            }
            _ => Ok(None),
        }
    }

    async fn on_user_created(&self, user: &DB::User, _ctx: &AuthContext<DB>) -> AuthResult<()> {
        if self.config.require_email_verification
            && !user.email_verified()
            && let Some(email) = user.email()
        {
            println!("Email verification required for user: {}", email);
        }
        Ok(())
    }
}

#[cfg(feature = "axum")]
mod axum_impl {
    use super::*;
    use crate::plugins::helpers::extract_session_client_info_from_headers;
    use std::sync::Arc;

    use athena_auth_core::{AuthState, ValidatedJson};
    use axum::Json;
    use axum::extract::{Extension, State};
    use axum::http::HeaderMap;
    use axum::http::header;
    use axum::response::IntoResponse;

    /// Shared plugin state wrapping the full plugin (non-Clone due to
    /// Option<Arc<EmailVerificationPlugin>>).
    type SharedPlugin = Arc<EmailPasswordPlugin>;

    async fn handle_sign_up<DB: DatabaseAdapter>(
        State(state): State<AuthState<DB>>,
        Extension(plugin): Extension<SharedPlugin>,
        headers: HeaderMap,
        ValidatedJson(body): ValidatedJson<SignUpRequest>,
    ) -> Result<axum::response::Response, AuthError> {
        let ctx = state.to_context();
        let (ip_address, user_agent) = extract_session_client_info_from_headers(&ctx, &headers);
        let (response, session_token) =
            sign_up_core(&body, &plugin.config, ip_address, user_agent, &ctx).await?;

        if let Some(token) = session_token {
            let cookie = state.session_cookie(&token);
            Ok(([(header::SET_COOKIE, cookie)], Json(response)).into_response())
        } else {
            Ok(Json(response).into_response())
        }
    }

    /// Helper to convert a `SignInCoreResult` into an axum response.
    fn sign_in_result_to_response<DB: DatabaseAdapter>(
        result: SignInCoreResult<DB::User, DB::Session>,
        state: &AuthState<DB>,
    ) -> axum::response::Response {
        match result {
            SignInCoreResult::Success(response, token) => {
                let cookie = state.session_cookie(&token);
                ([(header::SET_COOKIE, cookie)], Json(response)).into_response()
            }
            SignInCoreResult::TwoFactorRedirect(redirect) => Json(redirect).into_response(),
        }
    }

    async fn handle_sign_in<DB: DatabaseAdapter>(
        State(state): State<AuthState<DB>>,
        Extension(plugin): Extension<SharedPlugin>,
        headers: HeaderMap,
        ValidatedJson(body): ValidatedJson<SignInRequest>,
    ) -> Result<axum::response::Response, AuthError> {
        let ctx = state.to_context();
        let (ip_address, user_agent) = extract_session_client_info_from_headers(&ctx, &headers);
        let result = sign_in_core(
            &body,
            &plugin.config,
            plugin.email_verification.as_deref(),
            ip_address,
            user_agent,
            &ctx,
        )
        .await?;
        Ok(sign_in_result_to_response::<DB>(result, &state))
    }

    async fn handle_sign_in_username<DB: DatabaseAdapter>(
        State(state): State<AuthState<DB>>,
        Extension(plugin): Extension<SharedPlugin>,
        headers: HeaderMap,
        ValidatedJson(body): ValidatedJson<SignInUsernameRequest>,
    ) -> Result<axum::response::Response, AuthError> {
        let ctx = state.to_context();
        let (ip_address, user_agent) = extract_session_client_info_from_headers(&ctx, &headers);
        let result = sign_in_username_core(
            &body,
            &plugin.config,
            plugin.email_verification.as_deref(),
            ip_address,
            user_agent,
            &ctx,
        )
        .await?;
        Ok(sign_in_result_to_response::<DB>(result, &state))
    }

    #[async_trait::async_trait]
    impl<DB: DatabaseAdapter> athena_auth_core::AxumPlugin<DB> for EmailPasswordPlugin {
        fn name(&self) -> &'static str {
            "email-password"
        }

        fn router(&self) -> axum::Router<AuthState<DB>> {
            use axum::routing::post;

            let shared: SharedPlugin = Arc::new(EmailPasswordPlugin {
                config: self.config.clone(),
                email_verification: self.email_verification.clone(),
            });

            axum::Router::new()
                .route("/sign-up/email", post(handle_sign_up::<DB>))
                .route("/sign-in/email", post(handle_sign_in::<DB>))
                .route("/sign-in/username", post(handle_sign_in_username::<DB>))
                .layer(Extension(shared))
        }

        async fn on_user_created(
            &self,
            user: &DB::User,
            _ctx: &athena_auth_core::AuthContext<DB>,
        ) -> athena_auth_core::AuthResult<()> {
            if self.config.require_email_verification
                && !user.email_verified()
                && let Some(email) = user.email()
            {
                tracing::info!(
                    user_id = %user.id(),
                    email = %email,
                    "email verification required for new user"
                );
            }
            Ok(())
        }
    }
}

#[cfg(test)]
#[path = "__tests__/email_password.rs"]
mod tests;
