use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use tracing;
use uuid::Uuid;

use athena_auth_core::{AuthContext, AuthError, AuthResult, PASSWORD_HASH_KEY, extract_origin};
use athena_auth_core::{
    AuthEmailRenderContext, build_auth_email_message, password_reset_email_for_context,
};
use athena_auth_core::{AuthSession, AuthUser, AuthVerification, DatabaseAdapter};

use athena_auth_core::utils::password::{self as password_utils};

use crate::plugins::helpers::{
    build_public_auth_url_on_origin, record_email_send_failure, trusted_public_callback_url,
};

use super::types::*;
use super::{PasswordManagementConfig, StatusResponse};

// ---------------------------------------------------------------------------
// Core functions (framework-agnostic business logic)
// ---------------------------------------------------------------------------

const RESET_TOKEN_CONTEXT_PREFIX: &str = "reset_ctx:";

#[derive(Debug, Clone, Serialize, Deserialize)]
struct StoredResetTokenContext {
    email: String,
    #[serde(rename = "callbackURL", skip_serializing_if = "Option::is_none")]
    callback_url: Option<String>,
}

pub(super) fn build_reset_password_url<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    token: &str,
    callback_url: Option<&str>,
) -> String {
    let callback_url = trusted_public_callback_url(ctx, callback_url);
    let mut reset_url = ctx
        .config
        .public_auth_url(&format!("reset-password/{token}"));

    if let Some(callback) = callback_url
        && let Ok(parsed) = url::Url::parse(callback)
        && parsed.origin().is_tuple()
    {
        let origin = parsed.origin().ascii_serialization();
        reset_url = build_public_auth_url_on_origin(
            &origin,
            ctx.config.effective_public_base_path(),
            &format!("reset-password/{token}"),
        );
    }

    if let Some(callback) = callback_url {
        reset_url.push_str("?callbackURL=");
        reset_url.push_str(&urlencoding::encode(callback));
    }

    reset_url
}

pub(super) fn resolve_reset_password_callback_url<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    requested_callback_url: Option<&str>,
    request_origin: Option<&str>,
    request_referer: Option<&str>,
) -> Option<String> {
    if let Some(callback_url) = trusted_public_callback_url(ctx, requested_callback_url) {
        return Some(callback_url.to_string());
    }

    infer_reset_password_callback_url_from_request(ctx, request_origin, request_referer)
}

fn infer_reset_password_callback_url_from_request<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    request_origin: Option<&str>,
    request_referer: Option<&str>,
) -> Option<String> {
    for source in [request_origin, request_referer] {
        let Some(raw_value) = source else {
            continue;
        };
        let Some(origin) = extract_origin(raw_value) else {
            continue;
        };
        if ctx.config.is_origin_trusted(&origin) {
            tracing::debug!(
                request_origin = %origin,
                "Inferred password reset callback from trusted request origin"
            );
            return Some(format!("{}/reset-password", origin.trim_end_matches('/')));
        }
    }

    None
}

fn store_reset_token_context(email: &str, callback_url: Option<&str>) -> String {
    let Some(callback_url) = callback_url else {
        return email.to_string();
    };

    let payload = StoredResetTokenContext {
        email: email.to_string(),
        callback_url: Some(callback_url.to_string()),
    };

    match serde_json::to_string(&payload) {
        Ok(encoded) => format!("{RESET_TOKEN_CONTEXT_PREFIX}{encoded}"),
        Err(error) => {
            tracing::warn!(
                error = %error,
                email = %email,
                "Failed to serialize password reset verification context"
            );
            email.to_string()
        }
    }
}

fn parse_reset_token_context(identifier: &str) -> StoredResetTokenContext {
    let Some(encoded) = identifier.strip_prefix(RESET_TOKEN_CONTEXT_PREFIX) else {
        return StoredResetTokenContext {
            email: identifier.to_string(),
            callback_url: None,
        };
    };

    match serde_json::from_str::<StoredResetTokenContext>(encoded) {
        Ok(context) => context,
        Err(error) => {
            tracing::warn!(
                error = %error,
                "Failed to parse stored password reset verification context"
            );
            StoredResetTokenContext {
                email: identifier.to_string(),
                callback_url: None,
            }
        }
    }
}

pub(crate) async fn forget_password_core<DB: DatabaseAdapter>(
    body: &ForgetPasswordRequest,
    callback_url: Option<&str>,
    config: &PasswordManagementConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<StatusResponse> {
    // Check if user exists
    let user = match ctx.database.get_user_by_email(&body.email).await? {
        Some(user) => user,
        None => {
            tracing::debug!(
                email = %body.email,
                "forgot-password: no user for email (generic ok response)"
            );
            // Don't reveal whether email exists or not for security
            return Ok(StatusResponse { status: true });
        }
    };

    // Generate password reset token
    let reset_token = format!("reset_{}", Uuid::new_v4());
    let expires_at = Utc::now() + Duration::hours(config.reset_token_expiry_hours);

    // Create verification token
    let create_verification = athena_auth_core::CreateVerification {
        identifier: store_reset_token_context(user.email().unwrap_or_default(), callback_url),
        value: reset_token.clone(),
        expires_at,
    };

    ctx.database
        .create_verification(create_verification)
        .await?;

    let reset_url = build_reset_password_url(ctx, &reset_token, callback_url);

    if config.send_email_notifications {
        if let Some(sender) = &config.send_reset_password {
            let user_value = password_utils::serialize_to_value(&user)?;
            match sender.send(&user_value, &reset_url, &reset_token).await {
                Ok(()) => {
                    tracing::info!(
                        email = %body.email,
                        user_id = %user.id(),
                        "password reset email sent (custom sender)"
                    );
                }
                Err(e) => {
                    tracing::error!(
                        email = %body.email,
                        user_id = %user.id(),
                        error = %e,
                        "custom send_reset_password failed"
                    );
                }
            }
        } else if let Ok(provider) = ctx.email_provider() {
            let bodies = password_reset_email_for_context(
                ctx,
                &reset_url,
                AuthEmailRenderContext::new().with_user_id(user.id()),
            )
            .await;
            match build_auth_email_message(ctx, &body.email, &bodies).await {
                Ok(message) => match provider.send_message(&message).await {
                    Ok(()) => {
                        tracing::info!(
                            email = %body.email,
                            user_id = %user.id(),
                            "password reset email sent"
                        );
                    }
                    Err(e) => {
                        record_email_send_failure(
                            ctx,
                            Some(user.id()),
                            &body.email,
                            "password_reset",
                            Some("email_provider"),
                            &e.to_string(),
                            message.metadata.clone(),
                        )
                        .await;
                        tracing::error!(
                            email = %body.email,
                            user_id = %user.id(),
                            error = %e,
                            metadata = ?message.metadata,
                            "password reset email send failed"
                        );
                    }
                },
                Err(e) => {
                    record_email_send_failure(
                        ctx,
                        Some(user.id()),
                        &body.email,
                        "password_reset",
                        Some("email_provider"),
                        &e.to_string(),
                        e.metadata().cloned(),
                    )
                    .await;
                    tracing::error!(
                        email = %body.email,
                        user_id = %user.id(),
                        error = %e,
                        metadata = ?e.metadata(),
                        "password reset email preparation failed"
                    );
                }
            }
        } else {
            tracing::warn!(
                email = %body.email,
                user_id = %user.id(),
                "no email provider configured, password reset email not sent"
            );
        }
    } else {
        tracing::debug!(
            email = %body.email,
            user_id = %user.id(),
            "forgot-password: send_email_notifications disabled"
        );
    }

    Ok(StatusResponse { status: true })
}

pub(crate) async fn reset_password_core<DB: DatabaseAdapter>(
    body: &ResetPasswordRequest,
    config: &PasswordManagementConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<StatusResponse> {
    // Validate password
    password_utils::validate_password(
        &body.new_password,
        ctx.config.password.min_length,
        usize::MAX,
        ctx,
    )?;

    // Find user by reset token
    let token = body.token.as_deref().unwrap_or("");
    if token.is_empty() {
        return Err(AuthError::bad_request("Reset token is required"));
    }

    let (user, verification) = find_user_by_reset_token(token, ctx)
        .await?
        .ok_or_else(|| AuthError::bad_request("Invalid or expired reset token"))?;

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

    // Update user password
    let mut metadata = user.metadata().clone();
    metadata[PASSWORD_HASH_KEY] = serde_json::Value::String(password_hash);

    let updated_user = ctx
        .database
        .update_user(user.id(), password_utils::update_user_metadata(metadata))
        .await?;
    if let Err(cache_error) = ctx.cache_session_user(updated_user.id(), &updated_user) {
        tracing::warn!(
            user_id = %updated_user.id(),
            error = %cache_error,
            "failed to refresh session user cache after password reset"
        );
    }

    // Delete the used verification token
    ctx.database.delete_verification(verification.id()).await?;

    // Revoke all existing sessions for security (when configured)
    if config.revoke_sessions_on_password_reset {
        ctx.database.delete_user_sessions(user.id()).await?;
    }

    // Call on_password_reset callback if configured.
    if let Some(callback) = &config.on_password_reset {
        match password_utils::serialize_to_value(&updated_user) {
            Ok(user_value) => {
                if let Err(e) = callback(user_value).await {
                    tracing::warn!(
                        error = %e,
                        "on_password_reset callback failed"
                    );
                }
            }
            Err(e) => {
                tracing::warn!(
                    error = %e,
                    "Failed to serialize user for on_password_reset callback"
                );
            }
        }
    }

    Ok(StatusResponse { status: true })
}

pub(crate) async fn reset_password_token_core<DB: DatabaseAdapter>(
    token: &str,
    query: &ResetPasswordTokenQuery,
    ctx: &AuthContext<DB>,
) -> AuthResult<ResetPasswordTokenResult> {
    // Validate the reset token exists and is not expired
    let stored_callback_url = match find_user_by_reset_token(token, ctx).await? {
        Some((_user, verification)) => {
            parse_reset_token_context(verification.identifier()).callback_url
        }
        None => {
            // Redirect to callback URL with error if provided
            if let Some(callback_url) = &query.callback_url {
                let redirect_url = format!("{}?error=INVALID_TOKEN", callback_url);
                return Ok(ResetPasswordTokenResult::Redirect(redirect_url));
            }
            return Err(AuthError::bad_request("Invalid or expired reset token"));
        }
    };

    // If callback URL is provided, redirect with valid token
    if let Some(callback_url) = query.callback_url.as_ref().or(stored_callback_url.as_ref()) {
        let redirect_url = format!("{}?token={}", callback_url, token);
        return Ok(ResetPasswordTokenResult::Redirect(redirect_url));
    }

    // Otherwise return the token directly
    Ok(ResetPasswordTokenResult::Json(ResetPasswordTokenResponse {
        token: token.to_string(),
    }))
}

/// Change the user's password. Returns the response and an optional new session token.
pub(crate) async fn change_password_core<DB: DatabaseAdapter>(
    body: &ChangePasswordRequest,
    user: &DB::User,
    config: &PasswordManagementConfig,
    ip_address: Option<String>,
    user_agent: Option<String>,
    ctx: &AuthContext<DB>,
) -> AuthResult<(ChangePasswordResponse<DB::User>, Option<String>)> {
    // Verify current password
    if config.require_current_password {
        let stored_hash = user
            .password_hash()
            .ok_or_else(|| AuthError::bad_request("No password set for this user"))?;

        password_utils::verify_password_with_config(
            config.password_hasher.as_ref(),
            Some(&ctx.config.password.argon2_config),
            &body.current_password,
            stored_hash,
        )
        .await
        .map_err(|_| AuthError::InvalidCredentials)?;
    }

    // Validate new password
    password_utils::validate_password(
        &body.new_password,
        ctx.config.password.min_length,
        usize::MAX,
        ctx,
    )?;

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

    // Update user password
    let mut metadata = user.metadata().clone();
    metadata[PASSWORD_HASH_KEY] = serde_json::Value::String(password_hash);

    let updated_user = ctx
        .database
        .update_user(user.id(), password_utils::update_user_metadata(metadata))
        .await?;
    if let Err(cache_error) = ctx.cache_session_user(updated_user.id(), &updated_user) {
        tracing::warn!(
            user_id = %updated_user.id(),
            error = %cache_error,
            "failed to refresh session user cache after change-password"
        );
    }

    // Handle session revocation
    let new_token = if body.revoke_other_sessions == Some(true) {
        // Revoke all sessions except current one
        ctx.database.delete_user_sessions(user.id()).await?;

        // Create new session
        let session = ctx
            .session_manager()
            .create_session(&updated_user, ip_address, user_agent)
            .await?;
        Some(session.token().to_string())
    } else {
        None
    };

    let response = ChangePasswordResponse {
        token: new_token.clone(),
        user: updated_user,
    };

    Ok((response, new_token))
}

pub(crate) async fn set_password_core<DB: DatabaseAdapter>(
    body: &SetPasswordRequest,
    user: &DB::User,
    config: &PasswordManagementConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<StatusResponse> {
    // Verify the user does NOT already have a password
    if user.password_hash().is_some() {
        return Err(AuthError::bad_request(
            "User already has a password. Use /change-password instead.",
        ));
    }

    // Validate new password
    password_utils::validate_password(
        &body.new_password,
        ctx.config.password.min_length,
        usize::MAX,
        ctx,
    )?;

    // Hash and store the new password
    let password_hash = password_utils::hash_password_with_config(
        config.password_hasher.as_ref(),
        Some(&ctx.config.password.argon2_config),
        &body.new_password,
    )
    .await?;

    let mut metadata = user.metadata().clone();
    metadata[PASSWORD_HASH_KEY] = serde_json::Value::String(password_hash);

    let updated_user = ctx
        .database
        .update_user(user.id(), password_utils::update_user_metadata(metadata))
        .await?;
    if let Err(cache_error) = ctx.cache_session_user(updated_user.id(), &updated_user) {
        tracing::warn!(
            user_id = %updated_user.id(),
            error = %cache_error,
            "failed to refresh session user cache after setting password"
        );
    }

    Ok(StatusResponse { status: true })
}

/// Shared helper: find a user by reset token value.
pub(super) async fn find_user_by_reset_token<DB: DatabaseAdapter>(
    token: &str,
    ctx: &AuthContext<DB>,
) -> AuthResult<Option<(DB::User, DB::Verification)>> {
    let verification = match ctx.database.get_verification_by_value(token).await? {
        Some(verification) => verification,
        None => return Ok(None),
    };
    let stored_context = parse_reset_token_context(verification.identifier());

    let user = match ctx
        .database
        .get_user_by_email(&stored_context.email)
        .await?
    {
        Some(user) => user,
        None => return Ok(None),
    };

    Ok(Some((user, verification)))
}
