//! Shared helpers for plugin implementations.
//!
//! Extracted to avoid duplicating common patterns across plugins (DRY).

#[cfg(feature = "axum")]
use athena_auth_core::HttpMethod;
use athena_auth_core::adapters::DatabaseAdapter;
use athena_auth_core::entity::AuthApiKey;
use athena_auth_core::{
    AuthContext, AuthError, AuthRequest, AuthResult, AvatarTemplateContext, CreateAdminAuditLog,
    CreateEmailSendFailure, GrantAssignment, GrantDescriptor, UNTRUSTED_AVATAR_URL_MESSAGE,
    merge_permissions, permissions_map_from_assignments, resolve_avatar_value,
    resolve_grant_assignments,
};
use serde::Serialize;
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::future::Future;
use tracing;

use crate::plugins::grants::{
    NativeRightAssignmentView, ResolvedGrantAssignmentView, ResolvedNativeRightAssignmentView,
    grant_keys_from_assignments, merged_grant_catalog, merged_native_right_catalog,
    native_right_assignment_views, native_right_keys_for_grant_keys,
    native_right_modules_for_grant_keys, native_rights_for_grant_keys,
    resolved_grant_assignment_views, resolved_native_right_assignment_views,
    right_module_inputs_from_native_modules, unknown_native_right_keys_for_grant_keys,
};

/// Server version injected into session payloads so clients can detect drift.
pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Generic session view that appends `serverVersion` to any serialized session object.
#[derive(Debug, Clone, Serialize)]
pub(crate) struct SessionWithServerVersion<S: Serialize> {
    #[serde(flatten)]
    session: S,
    #[serde(rename = "serverVersion")]
    server_version: &'static str,
}

pub(crate) fn session_with_server_version<S: Serialize>(session: S) -> SessionWithServerVersion<S> {
    SessionWithServerVersion {
        session,
        server_version: SERVER_VERSION,
    }
}

pub(crate) fn sessions_with_server_version<S: Serialize>(
    sessions: Vec<S>,
) -> Vec<SessionWithServerVersion<S>> {
    sessions
        .into_iter()
        .map(session_with_server_version)
        .collect()
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct EffectiveGrantState {
    #[serde(rename = "subjectKind")]
    pub subject_kind: String,
    #[serde(rename = "subjectId")]
    pub subject_id: String,
    #[serde(rename = "grantKeys")]
    pub grant_keys: Vec<String>,
    #[serde(rename = "rightKeys")]
    pub right_keys: Vec<String>,
    #[serde(rename = "rightModules")]
    pub right_modules: Vec<athena_auth_core::NativeRightModuleInput>,
    #[serde(rename = "nativeRightModules")]
    pub native_right_modules: Vec<athena_rights::AthenaRightModule>,
    #[serde(rename = "grantAssignments")]
    pub grant_assignments: Vec<GrantAssignment>,
    #[serde(rename = "rightAssignments")]
    pub right_assignments: Vec<GrantAssignment>,
    #[serde(rename = "resolvedGrants")]
    pub resolved_grants: Vec<ResolvedGrantAssignmentView>,
    #[serde(rename = "resolvedRights")]
    pub resolved_rights: Vec<ResolvedGrantAssignmentView>,
    #[serde(rename = "resolvedNativeRights")]
    pub resolved_native_rights: Vec<ResolvedNativeRightAssignmentView>,
    #[serde(rename = "nativeRights")]
    pub native_rights: Vec<athena_rights::AthenaRightDescriptor>,
    #[serde(rename = "nativeRightAssignments")]
    pub native_right_assignments: Vec<NativeRightAssignmentView>,
    #[serde(rename = "grantedPermissions")]
    pub granted_permissions: HashMap<String, Vec<String>>,
    #[serde(rename = "effectivePermissions")]
    pub effective_permissions: HashMap<String, Vec<String>>,
    #[serde(rename = "unknownGrantKeys")]
    pub unknown_grant_keys: Vec<String>,
    #[serde(rename = "unknownRightKeys")]
    pub unknown_right_keys: Vec<String>,
}

pub(crate) fn permissions_map_from_json_value(
    value: Option<&Value>,
) -> HashMap<String, Vec<String>> {
    let Some(Value::Object(map)) = value else {
        return HashMap::new();
    };

    map.iter()
        .filter_map(|(resource, actions)| {
            let entries = actions
                .as_array()
                .into_iter()
                .flatten()
                .filter_map(|value| value.as_str().map(ToString::to_string))
                .collect::<Vec<_>>();
            if entries.is_empty() {
                None
            } else {
                Some((resource.clone(), entries))
            }
        })
        .collect()
}

pub(crate) async fn load_effective_grant_state<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    grant_catalog: &[GrantDescriptor],
    native_right_catalog: Option<&[athena_rights::AthenaRightDescriptor]>,
    subject_kind: &str,
    subject_id: &str,
    base_permissions: Option<HashMap<String, Vec<String>>>,
) -> AuthResult<EffectiveGrantState> {
    let grant_assignments = ctx
        .database
        .list_grant_assignments_for_subject(subject_kind, subject_id)
        .await?;
    let native_right_catalog_owned;
    let native_right_catalog = match native_right_catalog {
        Some(catalog) => {
            native_right_catalog_owned = merged_native_right_catalog(catalog);
            native_right_catalog_owned.as_slice()
        }
        None => {
            native_right_catalog_owned = athena_rights::native_right_catalog();
            native_right_catalog_owned.as_slice()
        }
    };
    let effective_grant_catalog = merged_grant_catalog(grant_catalog, native_right_catalog);
    let resolved_grants = resolved_grant_assignment_views(
        resolve_grant_assignments(grant_assignments.iter(), &effective_grant_catalog),
        native_right_catalog,
    );
    let grant_keys = grant_keys_from_assignments(grant_assignments.iter());
    let native_rights =
        native_rights_for_grant_keys(grant_keys.iter().map(String::as_str), native_right_catalog);
    let native_right_modules = native_right_modules_for_grant_keys(
        grant_keys.iter().map(String::as_str),
        native_right_catalog,
    );
    let right_modules = right_module_inputs_from_native_modules(&native_right_modules);
    let native_right_assignments =
        native_right_assignment_views(grant_assignments.iter(), native_right_catalog);
    let resolved_native_rights = resolved_native_right_assignment_views(
        grant_assignments.iter(),
        &effective_grant_catalog,
        native_right_catalog,
    );
    let granted_permissions =
        permissions_map_from_assignments(grant_assignments.iter(), &effective_grant_catalog);
    let mut effective_permissions = base_permissions.unwrap_or_default();
    merge_permissions(&mut effective_permissions, granted_permissions.clone());
    let right_keys = native_right_keys_for_grant_keys(
        grant_keys.iter().map(String::as_str),
        native_right_catalog,
    );
    let unknown_grant_keys = grant_assignments
        .iter()
        .filter(|assignment| {
            !resolved_grants
                .iter()
                .any(|resolved| resolved.resolved.grant_key == assignment.grant_key)
        })
        .map(|assignment| assignment.grant_key.clone())
        .collect::<Vec<_>>();
    let unknown_right_keys = unknown_native_right_keys_for_grant_keys(
        grant_keys.iter().map(String::as_str),
        native_right_catalog,
    );

    Ok(EffectiveGrantState {
        subject_kind: subject_kind.to_string(),
        subject_id: subject_id.to_string(),
        grant_keys,
        right_keys,
        right_modules,
        native_right_modules,
        right_assignments: grant_assignments.clone(),
        grant_assignments,
        resolved_rights: resolved_grants.clone(),
        resolved_grants,
        resolved_native_rights,
        native_rights,
        native_right_assignments,
        granted_permissions,
        effective_permissions,
        unknown_right_keys,
        unknown_grant_keys,
    })
}

/// Convert an `expiresIn` value (milliseconds from now) into an RFC 3339
/// `expires_at` timestamp string.
///
/// Returns `None` when `expires_in_ms` is `None`.
pub fn expires_in_to_at(expires_in_ms: Option<i64>) -> AuthResult<Option<String>> {
    match expires_in_ms {
        Some(ms) => {
            let duration = chrono::Duration::try_milliseconds(ms)
                .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
            let dt: chrono::prelude::DateTime<chrono::prelude::Utc> = chrono::Utc::now()
                .checked_add_signed(duration)
                .ok_or_else(|| AuthError::bad_request("expiresIn is out of range"))?;
            Ok(Some(dt.to_rfc3339()))
        }
        None => Ok(None),
    }
}

/// Fetch an API key by ID and verify that it belongs to the given user.
///
/// Returns `AuthError::not_found` if the key does not exist or belongs to
/// another user.  This pattern was duplicated in `handle_get`, `handle_update`,
/// and `handle_delete`.
pub async fn get_owned_api_key<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    key_id: &str,
    user_id: &str,
) -> AuthResult<DB::ApiKey> {
    let api_key = ctx
        .database
        .get_api_key_by_id(key_id)
        .await?
        .ok_or_else(|| AuthError::not_found("API key not found"))?;

    if api_key.user_id() != user_id {
        return Err(AuthError::not_found("API key not found"));
    }

    Ok(api_key)
}

/// Extract `(ip_address, user_agent)` for session creation from a framework-agnostic request.
pub fn extract_session_client_info<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    req: &AuthRequest,
) -> (Option<String>, Option<String>) {
    ctx.extract_session_client_info(req)
}

pub(crate) fn normalize_auth_base_path(path: &str) -> String {
    let trimmed: &str = path.trim();
    if trimmed.is_empty() || trimmed == "/" {
        return String::new();
    }
    if trimmed.starts_with('/') {
        trimmed.trim_end_matches('/').to_string()
    } else {
        format!("/{}", trimmed.trim_end_matches('/'))
    }
}

pub(crate) fn build_public_auth_url_on_origin(
    origin: &str,
    auth_base_path: &str,
    route_path: &str,
) -> String {
    let origin: &str = origin.trim_end_matches('/');
    let auth_base_path: String = normalize_auth_base_path(auth_base_path);
    let route_path: &str = route_path.trim_start_matches('/');

    if auth_base_path.is_empty() {
        format!("{origin}/{route_path}")
    } else {
        format!("{origin}{auth_base_path}/{route_path}")
    }
}

pub(crate) fn trusted_public_callback_url<'a, DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    callback_url: Option<&'a str>,
) -> Option<&'a str> {
    let callback_url = callback_url?;
    if callback_url.starts_with('/') || ctx.config.is_origin_trusted(callback_url) {
        return Some(callback_url);
    }

    tracing::warn!(callback_url = %callback_url, "Ignoring untrusted callback URL");
    None
}

pub(crate) fn resolve_user_name_from_email(
    email: &str,
    explicit_name: Option<&str>,
) -> AuthResult<String> {
    if let Some(name) = explicit_name
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        return Ok(name.to_string());
    }

    let trimmed_email = email.trim();
    let local_part = trimmed_email
        .split_once('@')
        .map(|(local_part, _)| local_part.trim())
        .filter(|value| !value.is_empty())
        .unwrap_or(trimmed_email);

    if local_part.is_empty() {
        return Err(AuthError::bad_request(
            "Unable to derive name from email address",
        ));
    }

    Ok(local_part.to_string())
}

pub(crate) async fn record_email_send_failure<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    user_id: Option<&str>,
    recipient_email: &str,
    flow: &str,
    provider: Option<&str>,
    error_message: &str,
    metadata: Option<Value>,
) {
    let log_row = CreateEmailSendFailure {
        user_id: user_id.map(str::to_string),
        recipient_email: recipient_email.to_string(),
        flow: flow.to_string(),
        provider: provider.map(str::to_string),
        error_message: error_message.to_string(),
        metadata,
    };

    if let Err(log_err) = ctx.database.create_email_send_failure(log_row).await {
        tracing::error!(
            recipient_email = %recipient_email,
            flow = %flow,
            provider = ?provider,
            failure_log_error = %log_err,
            "Failed to persist email send failure"
        );
    }
}

pub(crate) async fn resolve_optional_avatar_value<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    raw_value: Option<&str>,
    template_context: AvatarTemplateContext<'_>,
) -> AuthResult<Option<String>> {
    let Some(raw_value) = raw_value else {
        return Ok(None);
    };

    resolve_avatar_value(&ctx.config, raw_value, template_context)
        .await
        .map(Some)
}

/// Resolve an avatar during sign-in flows without failing authentication.
///
/// Untrusted external avatar URLs are skipped with a warning, matching the
/// behavior of [`trusted_public_callback_url`].
pub(crate) async fn resolve_optional_avatar_value_for_sign_in<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    raw_value: Option<&str>,
    template_context: AvatarTemplateContext<'_>,
) -> AuthResult<Option<String>> {
    let Some(raw_value) = raw_value else {
        return Ok(None);
    };

    match resolve_avatar_value(&ctx.config, raw_value, template_context).await {
        Ok(resolved) => Ok(Some(resolved)),
        Err(AuthError::BadRequest(message)) if message == UNTRUSTED_AVATAR_URL_MESSAGE => {
            tracing::warn!(
                avatar_url = %raw_value,
                "Ignoring untrusted avatar URL during sign-in"
            );
            Ok(None)
        }
        Err(error) => Err(error),
    }
}

/// Persist a login/sign-in event via the existing audit log table.
///
/// This uses the same durability path as admin audit events so operators can
/// inspect authentication activity without adding a separate storage surface.
#[allow(clippy::too_many_arguments)]
pub async fn write_sign_in_audit<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    action: &str,
    actor_user_id: Option<&str>,
    actor_session_token: Option<&str>,
    target_type: Option<&str>,
    target_id: Option<&str>,
    success: bool,
    error_message: Option<String>,
    ip_address: Option<&str>,
    user_agent: Option<&str>,
    details: Option<Value>,
) {
    let mut metadata = Map::new();
    metadata.insert(
        "source".to_string(),
        Value::String("auth.sign_in".to_string()),
    );
    if let Some(ip) = ip_address {
        metadata.insert("ipAddress".to_string(), Value::String(ip.to_string()));
    }
    if let Some(agent) = user_agent {
        metadata.insert("userAgent".to_string(), Value::String(agent.to_string()));
    }
    if let Some(details) = details {
        metadata.insert("details".to_string(), details);
    }

    let event: CreateAdminAuditLog = CreateAdminAuditLog {
        actor_user_id: actor_user_id.map(str::to_string),
        actor_session_token: actor_session_token.map(str::to_string),
        trace_id: None,
        api_key: None,
        action: action.to_string(),
        target_type: target_type.map(str::to_string),
        target_id: target_id.map(str::to_string),
        success,
        error_message,
        granted_rights: Vec::new(),
        requested_rights: Vec::new(),
        metadata: Some(Value::Object(metadata)),
    };

    if let Err(err) = ctx.database.create_admin_audit_log(event).await {
        tracing::error!(
            action = %action,
            error = %err,
            "failed to persist sign-in audit log"
        );
    }
}

pub(crate) fn spawn_background_task(
    task_name: &'static str,
    task: impl Future<Output = ()> + Send + 'static,
) {
    tokio::spawn(async move {
        tracing::trace!(task = task_name, "spawned background auth task");
        task.await;
    });
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn write_sign_in_audit_in_background<DB: DatabaseAdapter>(
    ctx: AuthContext<DB>,
    action: impl Into<String>,
    actor_user_id: Option<String>,
    actor_session_token: Option<String>,
    target_type: Option<String>,
    target_id: Option<String>,
    success: bool,
    error_message: Option<String>,
    ip_address: Option<String>,
    user_agent: Option<String>,
    details: Option<Value>,
) {
    let action: String = action.into();
    spawn_background_task("sign_in_audit", async move {
        write_sign_in_audit(
            &ctx,
            &action,
            actor_user_id.as_deref(),
            actor_session_token.as_deref(),
            target_type.as_deref(),
            target_id.as_deref(),
            success,
            error_message,
            ip_address.as_deref(),
            user_agent.as_deref(),
            details,
        )
        .await;
    });
}

#[cfg(feature = "axum")]
pub fn extract_session_client_info_from_headers<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    headers: &axum::http::HeaderMap,
) -> (Option<String>, Option<String>) {
    let mut req = AuthRequest::new(HttpMethod::Get, "/");
    for (name, value) in headers {
        if let Ok(value) = value.to_str() {
            req.headers
                .insert(name.as_str().to_string(), value.to_string());
        }
    }
    ctx.extract_session_client_info(&req)
}
