//! Default subject and HTML/plain bodies for built-in auth emails.
//!
//! All delivery goes through [`super::EmailProvider`] on [`crate::plugin::AuthContext`].
//! Built-ins can be overridden via `email_templates` table rows (per tenant/schema)
//! keyed by `template_key` + `locale`.

use std::collections::BTreeMap;

use crate::adapters::DatabaseAdapter;
use crate::entity::AuthSession;
use crate::error::AuthError;
use crate::plugin::AuthContext;
use crate::types::EmailTemplate;

const DEFAULT_TEMPLATE_LOCALE: &str = "en";

/// Subject line plus HTML and plain-text bodies ready for [`super::EmailProvider::send`].
#[derive(Debug, Clone)]
pub struct AuthEmailBodies {
    pub subject: String,
    pub html: String,
    pub text: String,
}

/// Render-time context used to resolve explicit template variables plus
/// server-side bindings anchored to auth entities.
#[derive(Debug, Clone, Default)]
pub struct AuthEmailRenderContext {
    pub variables: BTreeMap<String, String>,
    pub user_id: Option<String>,
    pub organization_id: Option<String>,
    pub session_token: Option<String>,
}

impl AuthEmailRenderContext {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn from_variables(variables: BTreeMap<String, String>) -> Self {
        Self {
            variables,
            ..Self::default()
        }
    }

    pub fn with_variable(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.variables.insert(key.into(), value.into());
        self
    }

    pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
        self.user_id = Some(user_id.into());
        self
    }

    pub fn with_organization_id(mut self, organization_id: impl Into<String>) -> Self {
        self.organization_id = Some(organization_id.into());
        self
    }

    pub fn with_session_token(mut self, session_token: impl Into<String>) -> Self {
        self.session_token = Some(session_token.into());
        self
    }
}

/// Canonical email event type metadata used for documentation and runtime mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AuthEmailEventCatalogEntry {
    pub event_type: &'static str,
    pub category: &'static str,
    pub description: &'static str,
    pub default_template_key: Option<&'static str>,
    pub required_variables: &'static [&'static str],
    pub optional_variables: &'static [&'static str],
}

/// Stable identifiers used by DB-backed template overrides (`email_templates.template_key`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthEmailTemplateKey {
    Verification,
    PasswordReset,
    ChangeEmailConfirmation,
    AccountDeletionConfirmation,
    TwoFactorOtp,
    OrganizationInvitation,
}

impl AuthEmailTemplateKey {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Verification => "verification_email",
            Self::PasswordReset => "password_reset_email",
            Self::ChangeEmailConfirmation => "change_email_confirmation_email",
            Self::AccountDeletionConfirmation => "account_deletion_confirmation_email",
            Self::TwoFactorOtp => "two_factor_otp_email",
            Self::OrganizationInvitation => "organization_invitation_email",
        }
    }

    pub const fn event_type(self) -> &'static str {
        match self {
            Self::Verification => "user.email.verify",
            Self::PasswordReset => "user.password.reset",
            Self::ChangeEmailConfirmation => "user.email.change.confirmation",
            Self::AccountDeletionConfirmation => "user.account.delete.confirmation",
            Self::TwoFactorOtp => "user.sign-in.otp",
            Self::OrganizationInvitation => "organization.member.invite",
        }
    }
}

const VARS_VERIFY_REQUIRED: &[&str] = &["verification_url"];
const VARS_VERIFY_OPTIONAL: &[&str] = &["app_name"];
const VARS_RESET_REQUIRED: &[&str] = &["reset_url"];
const VARS_RESET_OPTIONAL: &[&str] = &["app_name"];
const VARS_CHANGE_EMAIL_REQUIRED: &[&str] = &["verification_url"];
const VARS_CHANGE_EMAIL_OPTIONAL: &[&str] = &["app_name"];
const VARS_DELETE_CONFIRM_REQUIRED: &[&str] = &["verification_url"];
const VARS_DELETE_CONFIRM_OPTIONAL: &[&str] = &["app_name"];
const VARS_SIGN_IN_OTP_REQUIRED: &[&str] = &["otp_code"];
const VARS_SIGN_IN_OTP_OPTIONAL: &[&str] = &["app_name"];
const VARS_ORG_INVITE_REQUIRED: &[&str] = &[
    "organization_name",
    "role",
    "inviter_identity",
    "invitation_url",
];
const VARS_ORG_INVITE_OPTIONAL: &[&str] = &["app_name"];
const VARS_SIGN_IN_EMAIL_REQUIRED: &[&str] = &["sign_in_url"];
const VARS_SIGN_IN_EMAIL_OPTIONAL: &[&str] = &["app_name"];
const VARS_WELCOME_REQUIRED: &[&str] = &["user_name"];
const VARS_WELCOME_OPTIONAL: &[&str] = &["app_name", "dashboard_url"];
const VARS_PASSWORD_CHANGED_REQUIRED: &[&str] = &[];
const VARS_PASSWORD_CHANGED_OPTIONAL: &[&str] = &["app_name", "support_url"];
const VARS_SECURITY_ALERT_REQUIRED: &[&str] = &["alert_title", "alert_details"];
const VARS_SECURITY_ALERT_OPTIONAL: &[&str] = &["app_name", "support_url"];
const VARS_ORG_CREATE_REQUIRED: &[&str] = &["organization_name"];
const VARS_ORG_CREATE_OPTIONAL: &[&str] = &["app_name", "organization_url"];
const VARS_ORG_MEMBER_ADDED_REQUIRED: &[&str] = &["organization_name", "member_identity"];
const VARS_ORG_MEMBER_ADDED_OPTIONAL: &[&str] = &["app_name", "actor_identity"];
const VARS_ORG_MEMBER_REMOVED_REQUIRED: &[&str] = &["organization_name", "member_identity"];
const VARS_ORG_MEMBER_REMOVED_OPTIONAL: &[&str] = &["app_name", "actor_identity"];
const VARS_ORG_MEMBER_ROLE_UPDATED_REQUIRED: &[&str] =
    &["organization_name", "member_identity", "new_role"];
const VARS_ORG_MEMBER_ROLE_UPDATED_OPTIONAL: &[&str] =
    &["app_name", "previous_role", "actor_identity"];
const VARS_ORG_INVITE_REMINDER_REQUIRED: &[&str] = &["organization_name", "invitation_url"];
const VARS_ORG_INVITE_REMINDER_OPTIONAL: &[&str] = &["app_name", "role", "inviter_identity"];
const VARS_ORG_INVITE_REVOKED_REQUIRED: &[&str] = &["organization_name", "invited_email"];
const VARS_ORG_INVITE_REVOKED_OPTIONAL: &[&str] = &["app_name", "inviter_identity"];

const AUTH_EMAIL_EVENT_CATALOG: &[AuthEmailEventCatalogEntry] = &[
    AuthEmailEventCatalogEntry {
        event_type: "user.email.verify",
        category: "user_lifecycle",
        description: "Verify a user email address after signup or manual verification resend.",
        default_template_key: Some("verification_email"),
        required_variables: VARS_VERIFY_REQUIRED,
        optional_variables: VARS_VERIFY_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.password.reset",
        category: "user_security",
        description: "Send a password reset link.",
        default_template_key: Some("password_reset_email"),
        required_variables: VARS_RESET_REQUIRED,
        optional_variables: VARS_RESET_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.email.change.confirmation",
        category: "user_security",
        description: "Confirm a pending user email address change.",
        default_template_key: Some("change_email_confirmation_email"),
        required_variables: VARS_CHANGE_EMAIL_REQUIRED,
        optional_variables: VARS_CHANGE_EMAIL_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.account.delete.confirmation",
        category: "user_security",
        description: "Confirm account deletion before destructive delete.",
        default_template_key: Some("account_deletion_confirmation_email"),
        required_variables: VARS_DELETE_CONFIRM_REQUIRED,
        optional_variables: VARS_DELETE_CONFIRM_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.sign-in.otp",
        category: "user_security",
        description: "Deliver one-time passcode for two-factor authentication.",
        default_template_key: Some("two_factor_otp_email"),
        required_variables: VARS_SIGN_IN_OTP_REQUIRED,
        optional_variables: VARS_SIGN_IN_OTP_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.invite",
        category: "organization_lifecycle",
        description: "Invite a member to join an organization.",
        default_template_key: Some("organization_invitation_email"),
        required_variables: VARS_ORG_INVITE_REQUIRED,
        optional_variables: VARS_ORG_INVITE_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.sign-in.email",
        category: "user_security",
        description: "Email-based sign-in link or magic-link authentication flow.",
        default_template_key: None,
        required_variables: VARS_SIGN_IN_EMAIL_REQUIRED,
        optional_variables: VARS_SIGN_IN_EMAIL_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.sign-up.welcome",
        category: "user_lifecycle",
        description: "Welcome message after account creation.",
        default_template_key: None,
        required_variables: VARS_WELCOME_REQUIRED,
        optional_variables: VARS_WELCOME_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.password.changed",
        category: "user_security",
        description: "Security notification after password change completes.",
        default_template_key: None,
        required_variables: VARS_PASSWORD_CHANGED_REQUIRED,
        optional_variables: VARS_PASSWORD_CHANGED_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "user.security.alert",
        category: "user_security",
        description: "General security alert notification (suspicious sign-in, new device, policy event).",
        default_template_key: None,
        required_variables: VARS_SECURITY_ALERT_REQUIRED,
        optional_variables: VARS_SECURITY_ALERT_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.create",
        category: "organization_lifecycle",
        description: "Organization creation confirmation and onboarding.",
        default_template_key: None,
        required_variables: VARS_ORG_CREATE_REQUIRED,
        optional_variables: VARS_ORG_CREATE_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.added",
        category: "organization_lifecycle",
        description: "Notify when a member is added without invite flow.",
        default_template_key: None,
        required_variables: VARS_ORG_MEMBER_ADDED_REQUIRED,
        optional_variables: VARS_ORG_MEMBER_ADDED_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.removed",
        category: "organization_lifecycle",
        description: "Notify when a member is removed from an organization.",
        default_template_key: None,
        required_variables: VARS_ORG_MEMBER_REMOVED_REQUIRED,
        optional_variables: VARS_ORG_MEMBER_REMOVED_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.role.updated",
        category: "organization_lifecycle",
        description: "Notify when organization member role is changed.",
        default_template_key: None,
        required_variables: VARS_ORG_MEMBER_ROLE_UPDATED_REQUIRED,
        optional_variables: VARS_ORG_MEMBER_ROLE_UPDATED_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.invite.reminder",
        category: "organization_lifecycle",
        description: "Reminder message for pending organization invitations.",
        default_template_key: None,
        required_variables: VARS_ORG_INVITE_REMINDER_REQUIRED,
        optional_variables: VARS_ORG_INVITE_REMINDER_OPTIONAL,
    },
    AuthEmailEventCatalogEntry {
        event_type: "organization.member.invite.revoked",
        category: "organization_lifecycle",
        description: "Notification that an invitation has been revoked.",
        default_template_key: None,
        required_variables: VARS_ORG_INVITE_REVOKED_REQUIRED,
        optional_variables: VARS_ORG_INVITE_REVOKED_OPTIONAL,
    },
];

/// Returns the canonical email event catalog used for DB seeding and documentation.
pub const fn auth_email_event_catalog() -> &'static [AuthEmailEventCatalogEntry] {
    AUTH_EMAIL_EVENT_CATALOG
}

/// Resolves the canonical event type for a built-in template key.
pub fn default_event_type_for_template_key(template_key: &str) -> Option<&'static str> {
    auth_email_event_catalog()
        .iter()
        .find(|entry| entry.default_template_key == Some(template_key))
        .map(|entry| entry.event_type)
}

/// Resolves the default template key for a canonical event type.
pub fn default_template_key_for_event_type(event_type: &str) -> Option<&'static str> {
    auth_email_event_catalog()
        .iter()
        .find(|entry| entry.event_type == event_type)
        .and_then(|entry| entry.default_template_key)
}

#[derive(Debug, Clone, Copy)]
struct DefaultTemplateDefinition {
    subject: &'static str,
    html: &'static str,
    text: &'static str,
}

fn default_template_definition(key: AuthEmailTemplateKey) -> DefaultTemplateDefinition {
    match key {
        AuthEmailTemplateKey::Verification => DefaultTemplateDefinition {
            subject: "Verify your email address",
            html: "<p>Click the link below to verify your email address:</p>\
                   <p><a href=\"{{verification_url}}\">Verify Email</a></p>",
            text: "Verify your email address: {{verification_url}}",
        },
        AuthEmailTemplateKey::PasswordReset => DefaultTemplateDefinition {
            subject: "Reset your password",
            html: "<p>Click the link below to reset your password:</p>\
                   <p><a href=\"{{reset_url}}\">Reset Password</a></p>",
            text: "Reset your password: {{reset_url}}",
        },
        AuthEmailTemplateKey::ChangeEmailConfirmation => DefaultTemplateDefinition {
            subject: "Confirm your email change",
            html: "<p>Click the link below to confirm your new email address:</p>\
                   <p><a href=\"{{verification_url}}\">Confirm Email Change</a></p>",
            text: "Confirm your email change: {{verification_url}}",
        },
        AuthEmailTemplateKey::AccountDeletionConfirmation => DefaultTemplateDefinition {
            subject: "Confirm account deletion",
            html: "<p>Click the link below to confirm the deletion of your account:</p>\
                   <p><a href=\"{{verification_url}}\">Confirm Account Deletion</a></p>\
                   <p>If you did not request this, please ignore this email.</p>",
            text: "Confirm account deletion: {{verification_url}}",
        },
        AuthEmailTemplateKey::TwoFactorOtp => DefaultTemplateDefinition {
            subject: "Your verification code",
            html: "<p>Your 2FA verification code is: <strong>{{otp_code}}</strong></p>",
            text: "Your 2FA verification code is: {{otp_code}}",
        },
        AuthEmailTemplateKey::OrganizationInvitation => DefaultTemplateDefinition {
            subject: "Invitation to join {{organization_name}}",
            html: "<p>You were invited to join <strong>{{organization_name}}</strong>.</p>\
                   <p>Role: <strong>{{role}}</strong></p>\
                   <p>Invited by: <strong>{{inviter_identity}}</strong></p>\
                   <p><a href=\"{{invitation_url}}\">Open invitation</a></p>\
                   <p>If you are not signed in yet, create or verify your account first, then open the invitation link again.</p>",
            text: "You were invited to join {{organization_name}}.\nRole: {{role}}\nInvited by: {{inviter_identity}}\nInvitation link: {{invitation_url}}",
        },
    }
}

fn render_template_fragment(fragment: &str, variables: &BTreeMap<String, String>) -> String {
    let mut rendered = fragment.to_string();
    for (key, value) in variables {
        let compact = format!("{{{{{key}}}}}");
        let spaced = format!("{{{{ {key} }}}}");
        rendered = rendered.replace(&compact, value);
        rendered = rendered.replace(&spaced, value);
    }
    rendered
}

fn with_default_variables(
    mut variables: BTreeMap<String, String>,
    app_name: &str,
) -> BTreeMap<String, String> {
    variables
        .entry("app_name".to_string())
        .or_insert_with(|| app_name.to_string());
    variables
}

fn normalize_locale(locale: &str) -> Option<String> {
    let normalized = locale.trim().to_ascii_lowercase();
    if normalized.is_empty() {
        None
    } else {
        Some(normalized)
    }
}

#[derive(Debug, Clone)]
struct ParsedVariableBinding {
    name: String,
    source: String,
    required: bool,
}

#[derive(Debug, Clone, Default)]
struct ResolvedBindingAnchors {
    user_id: Option<String>,
    organization_id: Option<String>,
}

fn json_value_to_string(value: &serde_json::Value) -> Option<String> {
    match value {
        serde_json::Value::Null => None,
        serde_json::Value::String(text) => Some(text.clone()),
        serde_json::Value::Bool(value) => Some(value.to_string()),
        serde_json::Value::Number(value) => Some(value.to_string()),
        serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
            serde_json::to_string(value).ok()
        }
    }
}

fn entity_column_value_as_string<T: serde::Serialize>(entity: &T, column: &str) -> Option<String> {
    let value = serde_json::to_value(entity).ok()?;
    let object = value.as_object()?;
    let value = object.get(column)?;
    json_value_to_string(value)
}

fn parse_variable_bindings(
    template: &EmailTemplate,
) -> Result<Vec<ParsedVariableBinding>, AuthError> {
    let bindings = template.variable_bindings.as_array().ok_or_else(|| {
        AuthError::bad_request("email template variableBindings must be an array")
    })?;

    bindings
        .iter()
        .map(|binding| {
            let object = binding.as_object().ok_or_else(|| {
                AuthError::bad_request("email template variableBindings entries must be objects")
            })?;
            let name = object
                .get("name")
                .and_then(serde_json::Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .ok_or_else(|| {
                    AuthError::bad_request(
                        "email template variableBindings entries must include a non-empty name",
                    )
                })?;
            let source = object
                .get("source")
                .and_then(serde_json::Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .ok_or_else(|| {
                    AuthError::bad_request(
                        "email template variableBindings entries must include a non-empty source",
                    )
                })?;
            let required = object
                .get("required")
                .and_then(serde_json::Value::as_bool)
                .unwrap_or(false);

            Ok(ParsedVariableBinding {
                name: name.to_string(),
                source: source.to_string(),
                required,
            })
        })
        .collect()
}

async fn resolve_binding_anchors<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    render_context: &AuthEmailRenderContext,
) -> Result<ResolvedBindingAnchors, AuthError> {
    let mut anchors = ResolvedBindingAnchors {
        user_id: render_context.user_id.clone(),
        organization_id: render_context.organization_id.clone(),
    };

    if (anchors.user_id.is_none() || anchors.organization_id.is_none())
        && let Some(session_token) = render_context.session_token.as_deref()
        && let Some(session) = ctx.database.get_session(session_token).await?
    {
        if anchors.user_id.is_none() {
            anchors.user_id = Some(session.user_id().to_string());
        }
        if anchors.organization_id.is_none() {
            anchors.organization_id = session.active_organization_id().map(str::to_string);
        }
    }

    Ok(anchors)
}

fn parse_source_address(source: &str) -> Result<(&str, &str, &str), AuthError> {
    let parts: Vec<&str> = source.split('.').map(str::trim).collect();
    if parts.len() != 3 || parts.iter().any(|segment| segment.is_empty()) {
        return Err(AuthError::bad_request(format!(
            "variable binding source '{source}' must use schema.table.column format"
        )));
    }

    Ok((parts[0], parts[1], parts[2]))
}

async fn resolve_binding_value<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    render_context: &AuthEmailRenderContext,
    anchors: &ResolvedBindingAnchors,
    binding: &ParsedVariableBinding,
) -> Result<Option<String>, AuthError> {
    let (_schema, table, column) = parse_source_address(&binding.source)?;
    let normalized_table = table.to_ascii_lowercase();

    match normalized_table.as_str() {
        "users" | "user" => {
            let Some(user_id) = anchors.user_id.as_deref() else {
                if binding.required {
                    return Err(AuthError::bad_request(format!(
                        "variable binding '{}' requires a user_id render anchor",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            let Some(user) = ctx.database.get_user_by_id(user_id).await? else {
                if binding.required {
                    return Err(AuthError::not_found(format!(
                        "user anchor '{user_id}' was not found for binding '{}'",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            Ok(entity_column_value_as_string(&user, column))
        }
        "organizations" | "organization" => {
            let Some(organization_id) = anchors.organization_id.as_deref() else {
                if binding.required {
                    return Err(AuthError::bad_request(format!(
                        "variable binding '{}' requires an organization_id render anchor",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            let Some(organization) = ctx.database.get_organization_by_id(organization_id).await?
            else {
                if binding.required {
                    return Err(AuthError::not_found(format!(
                        "organization anchor '{organization_id}' was not found for binding '{}'",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            Ok(entity_column_value_as_string(&organization, column))
        }
        "sessions" | "session" => {
            if column.eq_ignore_ascii_case("token") {
                return Err(AuthError::bad_request(format!(
                    "variable binding '{}' may not expose session token values",
                    binding.name
                )));
            }
            let Some(session_token) = render_context.session_token.as_deref() else {
                if binding.required {
                    return Err(AuthError::bad_request(format!(
                        "variable binding '{}' requires a session_token render anchor",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            let Some(session) = ctx.database.get_session(session_token).await? else {
                if binding.required {
                    return Err(AuthError::not_found(format!(
                        "session anchor for binding '{}' was not found",
                        binding.name
                    )));
                }
                return Ok(None);
            };
            Ok(entity_column_value_as_string(&session, column))
        }
        _ => Err(AuthError::bad_request(format!(
            "variable binding source '{}' is not supported by athena-auth email rendering",
            binding.source
        ))),
    }
}

async fn resolve_template_variables<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    template: &EmailTemplate,
    render_context: &AuthEmailRenderContext,
) -> Result<BTreeMap<String, String>, AuthError> {
    let mut variables = render_context.variables.clone();
    let anchors = resolve_binding_anchors(ctx, render_context).await?;
    let bindings = parse_variable_bindings(template)?;

    for binding in bindings {
        match resolve_binding_value(ctx, render_context, &anchors, &binding).await? {
            Some(value) => {
                variables.entry(binding.name).or_insert(value);
            }
            None if binding.required => {
                return Err(AuthError::bad_request(format!(
                    "required variable binding '{}' did not resolve a value",
                    binding.name
                )));
            }
            None => {}
        }
    }

    Ok(variables)
}

fn build_email_bodies(
    key: AuthEmailTemplateKey,
    variables: &BTreeMap<String, String>,
    custom_template: Option<&EmailTemplate>,
) -> AuthEmailBodies {
    let defaults = default_template_definition(key);
    let (subject_template, html_template, text_template) = match custom_template {
        Some(template) => (
            template.subject_template.as_str(),
            template.html_template.as_deref().unwrap_or(defaults.html),
            template.text_template.as_deref().unwrap_or(defaults.text),
        ),
        None => (defaults.subject, defaults.html, defaults.text),
    };

    AuthEmailBodies {
        subject: render_template_fragment(subject_template, variables),
        html: render_template_fragment(html_template, variables),
        text: render_template_fragment(text_template, variables),
    }
}

async fn render_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    key: AuthEmailTemplateKey,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    let mut variables =
        with_default_variables(render_context.variables.clone(), &ctx.config.app_name);
    let preferred_locale = variables
        .get("locale")
        .and_then(|value| normalize_locale(value))
        .unwrap_or_else(|| DEFAULT_TEMPLATE_LOCALE.to_string());

    let custom_template = match ctx
        .database
        .get_active_email_template_for_event(
            key.event_type(),
            key.as_str(),
            Some(&preferred_locale),
        )
        .await
    {
        Ok(template) => template,
        Err(error) => {
            tracing::warn!(
                template_key = key.as_str(),
                event_type = key.event_type(),
                locale = %preferred_locale,
                error = %error,
                "failed to load email template override; using built-in default"
            );
            None
        }
    };

    if let Some(template) = custom_template.as_ref() {
        match resolve_template_variables(
            ctx,
            template,
            &AuthEmailRenderContext {
                variables: variables.clone(),
                ..render_context
            },
        )
        .await
        {
            Ok(resolved_variables) => {
                variables = with_default_variables(resolved_variables, &ctx.config.app_name);
                build_email_bodies(key, &variables, Some(template))
            }
            Err(error) => {
                tracing::warn!(
                    template_key = key.as_str(),
                    event_type = key.event_type(),
                    locale = %preferred_locale,
                    error = %error,
                    "failed to resolve email template variable bindings; using built-in default"
                );
                build_email_bodies(key, &variables, None)
            }
        }
    } else {
        build_email_bodies(key, &variables, None)
    }
}

/// Build an auth email body using DB-backed override lookup (with safe fallback).
pub async fn render_auth_email_template<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    key: AuthEmailTemplateKey,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(ctx, key, render_context).await
}

fn single_var_template(key: &str, value: &str) -> BTreeMap<String, String> {
    BTreeMap::from([(key.to_string(), value.to_string())])
}

/// Email verification (sign-up / resend / sign-in nudge).
pub fn verification_email(verification_url: &str) -> AuthEmailBodies {
    let vars = single_var_template("verification_url", verification_url);
    build_email_bodies(AuthEmailTemplateKey::Verification, &vars, None)
}

/// Context-aware verification email using tenant/template overrides.
pub async fn verification_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    verification_url: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(
        ctx,
        AuthEmailTemplateKey::Verification,
        render_context.with_variable("verification_url", verification_url),
    )
    .await
}

/// Password reset link.
pub fn password_reset_email(reset_url: &str) -> AuthEmailBodies {
    let vars = single_var_template("reset_url", reset_url);
    build_email_bodies(AuthEmailTemplateKey::PasswordReset, &vars, None)
}

/// Context-aware password reset email using tenant/template overrides.
pub async fn password_reset_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    reset_url: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(
        ctx,
        AuthEmailTemplateKey::PasswordReset,
        render_context.with_variable("reset_url", reset_url),
    )
    .await
}

/// Confirm a pending email address change.
pub fn change_email_confirmation_email(verification_url: &str) -> AuthEmailBodies {
    let vars = single_var_template("verification_url", verification_url);
    build_email_bodies(AuthEmailTemplateKey::ChangeEmailConfirmation, &vars, None)
}

/// Context-aware change-email confirmation email using tenant/template overrides.
pub async fn change_email_confirmation_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    verification_url: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(
        ctx,
        AuthEmailTemplateKey::ChangeEmailConfirmation,
        render_context.with_variable("verification_url", verification_url),
    )
    .await
}

/// Confirm account deletion.
pub fn account_deletion_confirmation_email(verification_url: &str) -> AuthEmailBodies {
    let vars = single_var_template("verification_url", verification_url);
    build_email_bodies(
        AuthEmailTemplateKey::AccountDeletionConfirmation,
        &vars,
        None,
    )
}

/// Context-aware account deletion confirmation email using tenant/template overrides.
pub async fn account_deletion_confirmation_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    verification_url: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(
        ctx,
        AuthEmailTemplateKey::AccountDeletionConfirmation,
        render_context.with_variable("verification_url", verification_url),
    )
    .await
}

/// Email OTP for two-factor authentication.
pub fn two_factor_otp_email(code: &str) -> AuthEmailBodies {
    let vars = single_var_template("otp_code", code);
    build_email_bodies(AuthEmailTemplateKey::TwoFactorOtp, &vars, None)
}

/// Context-aware 2FA OTP email using tenant/template overrides.
pub async fn two_factor_otp_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    code: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    render_for_context(
        ctx,
        AuthEmailTemplateKey::TwoFactorOtp,
        render_context.with_variable("otp_code", code),
    )
    .await
}

/// Organization invitation email.
pub fn organization_invitation_email(
    organization_name: &str,
    role: &str,
    inviter_identity: &str,
    invitation_url: &str,
) -> AuthEmailBodies {
    let vars = BTreeMap::from([
        (
            "organization_name".to_string(),
            organization_name.to_string(),
        ),
        ("role".to_string(), role.to_string()),
        ("inviter_identity".to_string(), inviter_identity.to_string()),
        ("invitation_url".to_string(), invitation_url.to_string()),
    ]);
    build_email_bodies(AuthEmailTemplateKey::OrganizationInvitation, &vars, None)
}

/// Context-aware organization invitation email using tenant/template overrides.
pub async fn organization_invitation_email_for_context<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    organization_name: &str,
    role: &str,
    inviter_identity: &str,
    invitation_url: &str,
    render_context: AuthEmailRenderContext,
) -> AuthEmailBodies {
    let render_context = render_context
        .with_variable("organization_name", organization_name)
        .with_variable("role", role)
        .with_variable("inviter_identity", inviter_identity)
        .with_variable("invitation_url", invitation_url);
    render_for_context(
        ctx,
        AuthEmailTemplateKey::OrganizationInvitation,
        render_context,
    )
    .await
}

#[cfg(test)]
mod tests {
    use chrono::Utc;
    use serde_json::json;

    use super::*;

    #[test]
    fn verification_contains_url() {
        let b = verification_email("https://app.example/verify?token=1");
        assert!(b.html.contains("https://app.example/verify?token=1"));
        assert!(b.text.contains("https://app.example/verify?token=1"));
        assert_eq!(b.subject, "Verify your email address");
    }

    #[test]
    fn otp_email_contains_code() {
        let b = two_factor_otp_email("123456");
        assert!(b.html.contains("123456"));
        assert!(b.text.contains("123456"));
    }

    #[test]
    fn custom_template_overrides_subject_and_keeps_default_html_when_missing() {
        let vars = BTreeMap::from([
            (
                "reset_url".to_string(),
                "https://app/reset?token=1".to_string(),
            ),
            ("app_name".to_string(), "Acme Auth".to_string()),
        ]);

        let custom = EmailTemplate {
            id: "tmpl_1".to_string(),
            template_key: "password_reset_email".to_string(),
            event_type: Some("user.password.reset".to_string()),
            locale: "en".to_string(),
            subject_template: "[{{app_name}}] Password reset".to_string(),
            text_template: Some("Use this link: {{reset_url}}".to_string()),
            html_template: None,
            variables: json!(["app_name", "reset_url"]),
            variable_bindings: json!([]),
            is_active: true,
            metadata: json!({}),
            created_at: Utc::now(),
            updated_at: Utc::now(),
        };

        let rendered =
            build_email_bodies(AuthEmailTemplateKey::PasswordReset, &vars, Some(&custom));

        assert_eq!(rendered.subject, "[Acme Auth] Password reset");
        assert_eq!(rendered.text, "Use this link: https://app/reset?token=1");
        assert!(rendered.html.contains("Reset Password"));
        assert!(rendered.html.contains("https://app/reset?token=1"));
    }

    #[test]
    fn built_in_template_key_has_canonical_event_type() {
        let event_type = default_event_type_for_template_key("verification_email");
        assert_eq!(event_type, Some("user.email.verify"));
    }

    #[test]
    fn catalog_contains_organization_invite_event() {
        assert!(
            auth_email_event_catalog()
                .iter()
                .any(|entry| entry.event_type == "organization.member.invite")
        );
    }
}
