use chrono::Utc;
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;

use crate::SessionUserCache;
use crate::adapters::DatabaseAdapter;
use crate::config::{AuthConfig, GeoIpProvider};
use crate::entity::{AuthSession, AuthUser};
use crate::error::{AuthError, AuthResult};
use crate::types::{
    CreateSession, CreateSessionIpProfileRecord, IpProfileRecord, UpdateUser, UpsertIpProfileRecord,
};

/// Additional knobs used when creating a session row.
#[derive(Debug, Clone)]
pub struct SessionCreateOptions {
    pub ip_address: Option<String>,
    pub user_agent: Option<String>,
    pub impersonated_by: Option<String>,
    pub active_organization_id: Option<String>,
    pub update_last_sign_in_at: bool,
}

impl Default for SessionCreateOptions {
    fn default() -> Self {
        Self {
            ip_address: None,
            user_agent: None,
            impersonated_by: None,
            active_organization_id: None,
            update_last_sign_in_at: true,
        }
    }
}

/// Session manager handles session creation, validation, and cleanup
pub struct SessionManager<DB: DatabaseAdapter> {
    config: Arc<AuthConfig>,
    database: Arc<DB>,
    session_user_cache: SessionUserCache,
}

#[derive(Debug, Clone)]
pub struct SessionCreationResult<U, S> {
    pub session: S,
    pub updated_user: Option<U>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionTokenSource {
    AuthorizationBearer,
    Cookie,
}

impl SessionTokenSource {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::AuthorizationBearer => "authorization_bearer",
            Self::Cookie => "cookie",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractedSessionToken {
    pub token: String,
    pub source: SessionTokenSource,
}

impl<DB: DatabaseAdapter> Clone for SessionManager<DB> {
    fn clone(&self) -> Self {
        Self {
            config: self.config.clone(),
            database: self.database.clone(),
            session_user_cache: self.session_user_cache.clone(),
        }
    }
}

impl<DB: DatabaseAdapter> SessionManager<DB> {
    pub fn new(config: Arc<AuthConfig>, database: Arc<DB>) -> Self {
        Self {
            config,
            database,
            session_user_cache: SessionUserCache::default(),
        }
    }

    pub fn new_with_cache(
        config: Arc<AuthConfig>,
        database: Arc<DB>,
        session_user_cache: SessionUserCache,
    ) -> Self {
        Self {
            config,
            database,
            session_user_cache,
        }
    }

    /// Create a new session for a user
    pub async fn create_session(
        &self,
        user: &impl AuthUser,
        ip_address: Option<String>,
        user_agent: Option<String>,
    ) -> AuthResult<DB::Session> {
        Ok(self
            .create_session_with_user_projection(user, ip_address, user_agent)
            .await?
            .session)
    }

    /// Create a new session and return any updated user projection produced
    /// while refreshing interactive sign-in metadata.
    pub async fn create_session_with_user_projection(
        &self,
        user: &impl AuthUser,
        ip_address: Option<String>,
        user_agent: Option<String>,
    ) -> AuthResult<SessionCreationResult<DB::User, DB::Session>> {
        let options = SessionCreateOptions {
            ip_address,
            user_agent,
            impersonated_by: None,
            active_organization_id: None,
            update_last_sign_in_at: true,
        };
        self.create_session_for_user_id_with_projection(user.id(), options)
            .await
    }

    /// Create a session row for a known user id with explicit creation options.
    pub async fn create_session_for_user_id(
        &self,
        user_id: &str,
        options: SessionCreateOptions,
    ) -> AuthResult<DB::Session> {
        Ok(self
            .create_session_for_user_id_with_projection(user_id, options)
            .await?
            .session)
    }

    /// Create a session row for a known user id and return any user projection
    /// update performed as part of the interactive sign-in flow.
    pub async fn create_session_for_user_id_with_projection(
        &self,
        user_id: &str,
        options: SessionCreateOptions,
    ) -> AuthResult<SessionCreationResult<DB::User, DB::Session>> {
        let now = Utc::now();
        let expires_at: chrono::DateTime<Utc> = now + self.config.session.expires_in;
        let ip_address = self.normalize_ip_address(options.ip_address.as_deref());
        let user_agent = normalize_optional_string(options.user_agent);

        let updated_user = if options.update_last_sign_in_at {
            // Keep last_sign_in_at in sync with successful interactive session creation.
            let updated_user = self
                .database
                .update_user(
                    user_id,
                    UpdateUser {
                        last_sign_in_at: Some(now),
                        ..Default::default()
                    },
                )
                .await?;
            if let Err(error) = self.cache_user_snapshot(updated_user.id(), &updated_user) {
                tracing::warn!(
                    user_id = %updated_user.id(),
                    error = %error,
                    "failed to refresh session user cache after session creation"
                );
            }
            Some(updated_user)
        } else {
            None
        };

        let create_session: CreateSession = CreateSession {
            user_id: user_id.to_string(),
            expires_at,
            ip_address: ip_address.clone(),
            user_agent,
            impersonated_by: options.impersonated_by,
            active_organization_id: options.active_organization_id,
        };

        let session = self.database.create_session(create_session).await?;
        self.spawn_session_ip_enrichment(session.clone(), ip_address);

        Ok(SessionCreationResult {
            session,
            updated_user,
        })
    }

    /// Get session by token
    pub async fn get_session(&self, token: &str) -> AuthResult<Option<DB::Session>> {
        let session = self.database.get_session(token).await?;

        // Guard against custom adapters that may still return stale rows.
        if let Some(ref session) = session {
            if !session.is_active_now() {
                return Ok(None);
            }

            // Update session if configured to do so
            if !self.config.session.disable_session_refresh {
                let should_refresh: bool = match self.config.session.update_age {
                    Some(age) => {
                        // Only refresh if the session was last updated more than
                        // `update_age` ago.
                        let updated = session.updated_at();
                        Utc::now() - updated >= age
                    }
                    // No update_age set → refresh on every access.
                    None => true,
                };

                if should_refresh {
                    let new_expires_at: chrono::DateTime<Utc> =
                        Utc::now() + self.config.session.expires_in;
                    let _ = self
                        .database
                        .update_session_expiry(token, new_expires_at)
                        .await;
                }
            }
        }

        Ok(session)
    }

    /// Delete a session
    pub async fn delete_session(&self, token: &str) -> AuthResult<()> {
        self.database.delete_session(token).await?;
        Ok(())
    }

    /// Delete all sessions for a user
    pub async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()> {
        self.database.delete_user_sessions(user_id).await?;
        Ok(())
    }

    /// Get all active sessions for a user
    pub async fn list_user_sessions(&self, user_id: &str) -> AuthResult<Vec<DB::Session>> {
        let sessions = self.database.get_user_sessions(user_id).await?;

        let active_sessions: Vec<DB::Session> = sessions
            .into_iter()
            .filter(|session| session.is_active_now())
            .collect();

        Ok(active_sessions)
    }

    /// Revoke a specific session by token
    pub async fn revoke_session(&self, token: &str) -> AuthResult<bool> {
        // Check if session exists before trying to delete
        let session_exists = self.get_session(token).await?.is_some();

        if session_exists {
            self.delete_session(token).await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }

    /// Revoke all sessions for a user
    pub async fn revoke_all_user_sessions(&self, user_id: &str) -> AuthResult<usize> {
        // Get count of sessions before deletion for return value
        let sessions = self.list_user_sessions(user_id).await?;
        let count = sessions.len();

        self.delete_user_sessions(user_id).await?;
        Ok(count)
    }

    /// Revoke all sessions for a user except the current one
    pub async fn revoke_other_user_sessions(
        &self,
        user_id: &str,
        current_token: &str,
    ) -> AuthResult<usize> {
        let sessions = self.list_user_sessions(user_id).await?;
        let mut count = 0;

        for session in sessions {
            if session.token() != current_token {
                self.delete_session(session.token()).await?;
                count += 1;
            }
        }

        Ok(count)
    }

    /// Cleanup expired sessions
    pub async fn cleanup_expired_sessions(&self) -> AuthResult<usize> {
        let count = self.database.delete_expired_sessions().await?;
        Ok(count)
    }

    /// Check whether a session is "fresh" (created recently enough for
    /// sensitive operations like password change or account deletion).
    ///
    /// Returns `true` when `fresh_age` is set and
    /// `session.created_at() + fresh_age > now`.
    /// If `fresh_age` is `None`, the session is never considered fresh.
    pub fn is_session_fresh(&self, session: &impl AuthSession) -> bool {
        match self.config.session.fresh_age {
            Some(fresh_age) => session.created_at() + fresh_age > Utc::now(),
            None => false,
        }
    }

    /// Validate session token format
    pub fn validate_token_format(&self, token: &str) -> bool {
        token.starts_with("session_") && token.len() > 40
    }

    /// Extract session token from a request.
    ///
    /// Tries Bearer token from Authorization header first, then falls back
    /// to parsing the configured cookie from the Cookie header.
    pub fn extract_session_token(&self, req: &crate::types::AuthRequest) -> Option<String> {
        self.extract_session_token_with_source(req)
            .map(|session_token| session_token.token)
    }

    /// Extract the session token and record which request credential supplied it.
    pub fn extract_session_token_with_source(
        &self,
        req: &crate::types::AuthRequest,
    ) -> Option<ExtractedSessionToken> {
        // Try Bearer token first
        if let Some(auth_header) = get_header_case_insensitive(req, "authorization")
            && let Some(token) = auth_header.strip_prefix("Bearer ")
        {
            return Some(ExtractedSessionToken {
                token: token.to_string(),
                source: SessionTokenSource::AuthorizationBearer,
            });
        }

        // Fall back to cookie (using the `cookie` crate for correct parsing)
        if let Some(cookie_header) = get_header_case_insensitive(req, "cookie") {
            let cookie_name: &String = &self.config.session.cookie_name;
            for c in cookie::Cookie::split_parse(cookie_header).flatten() {
                if c.name() == cookie_name && !c.value().is_empty() {
                    return Some(ExtractedSessionToken {
                        token: c.value().to_string(),
                        source: SessionTokenSource::Cookie,
                    });
                }
            }
        }

        None
    }

    fn normalize_ip_address(&self, raw: Option<&str>) -> Option<String> {
        if self.config.advanced.ip_address.disable_ip_tracking {
            return None;
        }
        raw.and_then(normalize_ip_address_value)
    }

    fn spawn_session_ip_enrichment(&self, session: DB::Session, ip_address: Option<String>) {
        let intelligence = &self.config.advanced.session_intelligence;
        if !intelligence.enabled || !intelligence.geoip.enabled {
            return;
        }

        let manager = self.clone();
        tokio::spawn(async move {
            if let Err(error) = manager
                .enrich_session_ip_profile(&session, ip_address.as_deref())
                .await
            {
                tracing::warn!(
                    session_id = %session.id(),
                    user_id = %session.user_id(),
                    error = %error,
                    "session intelligence enrichment failed; continuing with fail-open behavior"
                );
            }
        });
    }

    async fn enrich_session_ip_profile(
        &self,
        session: &DB::Session,
        ip_address: Option<&str>,
    ) -> AuthResult<()> {
        let intelligence = &self.config.advanced.session_intelligence;
        if !intelligence.enabled || !intelligence.geoip.enabled {
            return Ok(());
        }
        let Some(ip_address) = ip_address.map(str::trim).filter(|v| !v.is_empty()) else {
            return Ok(());
        };

        // Ensure idempotency at the session row level as well.
        if self
            .database
            .get_session_ip_profile_by_session_id(session.id())
            .await?
            .is_some()
        {
            return Ok(());
        }

        let profile = self.resolve_ip_profile(ip_address).await?;
        let country_code = profile.as_ref().and_then(|p| p.country_code.clone());
        let country_name = profile.as_ref().and_then(|p| p.country_name.clone());
        let previous_country_code = if intelligence.detect_country_change {
            self.database
                .get_latest_session_country_code(session.user_id(), Some(session.id()))
                .await?
        } else {
            None
        };
        let country_changed = intelligence.detect_country_change
            && previous_country_code.is_some()
            && country_code.is_some()
            && previous_country_code != country_code;

        let binding = CreateSessionIpProfileRecord {
            session_id: session.id().to_string(),
            session_token: session.token().to_string(),
            user_id: session.user_id().to_string(),
            ip_address: ip_address.to_string(),
            ip_profile_id: profile.as_ref().map(|p| p.id.clone()),
            country_code,
            country_name,
            is_vpn: profile.as_ref().and_then(|p| p.is_vpn),
            is_proxy: profile.as_ref().and_then(|p| p.is_proxy),
            is_tor: profile.as_ref().and_then(|p| p.is_tor),
            is_hosting: profile.as_ref().and_then(|p| p.is_hosting),
            country_changed,
            previous_country_code,
            provider: profile.as_ref().map(|p| p.provider.clone()),
        };
        let _ = self.database.create_session_ip_profile(binding).await?;
        Ok(())
    }

    async fn resolve_ip_profile(&self, ip_address: &str) -> AuthResult<Option<IpProfileRecord>> {
        if should_skip_geo_lookup(ip_address) {
            return Ok(None);
        }

        let current_profile = self.database.get_ip_profile_by_ip(ip_address).await?;
        let now = Utc::now();
        if let Some(profile) = current_profile.as_ref() {
            let is_fresh = profile
                .lookup_expires_at
                .map(|expires_at| expires_at > now)
                .unwrap_or(true);
            if is_fresh {
                return Ok(current_profile);
            }
        }

        match self.lookup_ip_profile_from_provider(ip_address).await {
            Ok(Some(upsert)) => {
                let persisted = self.database.upsert_ip_profile(upsert).await?;
                Ok(persisted.or(current_profile))
            }
            Ok(None) => Ok(current_profile),
            Err(error) => {
                tracing::warn!(
                    ip_address = %ip_address,
                    error = %error,
                    "geoip lookup failed; falling back to existing cached profile if present"
                );
                Ok(current_profile)
            }
        }
    }

    async fn lookup_ip_profile_from_provider(
        &self,
        ip_address: &str,
    ) -> AuthResult<Option<UpsertIpProfileRecord>> {
        let geo = &self.config.advanced.session_intelligence.geoip;
        match geo.provider {
            GeoIpProvider::IpWhoIs => self.lookup_ipwhois_profile(ip_address).await,
            GeoIpProvider::IpApiCo => self.lookup_ipapi_profile(ip_address).await,
        }
    }

    async fn lookup_ipwhois_profile(
        &self,
        ip_address: &str,
    ) -> AuthResult<Option<UpsertIpProfileRecord>> {
        let geo = &self.config.advanced.session_intelligence.geoip;
        let base_url = geo.base_url.trim().trim_end_matches('/');
        if base_url.is_empty() {
            return Ok(None);
        }

        let client = build_geoip_http_client(geo.timeout_ms.max(100))?;

        let mut request = client
            .get(format!("{base_url}/{ip_address}"))
            .header("accept", "application/json");
        if let Some(api_key) = geo
            .api_key
            .as_deref()
            .map(str::trim)
            .filter(|v| !v.is_empty())
        {
            let header_name = geo
                .api_key_header
                .as_deref()
                .map(str::trim)
                .filter(|v| !v.is_empty())
                .unwrap_or("x-api-key");
            request = request.header(header_name, api_key);
        }

        let response = request
            .send()
            .await
            .map_err(|error| AuthError::internal(format!("geoip request failed: {error}")))?;
        if !response.status().is_success() {
            return Err(AuthError::internal(format!(
                "geoip request returned status {}",
                response.status()
            )));
        }

        let payload: serde_json::Value = response.json().await.map_err(|error| {
            AuthError::internal(format!("invalid geoip response payload: {error}"))
        })?;
        if payload
            .get("success")
            .and_then(|value| value.as_bool())
            .is_some_and(|success| !success)
        {
            return Ok(None);
        }

        let connection = payload.get("connection");
        let security = payload.get("security");
        let cache_ttl = geo.cache_ttl.max(chrono::Duration::seconds(1));
        let lookup_expires_at = Some(Utc::now() + cache_ttl);

        Ok(Some(UpsertIpProfileRecord {
            ip_address: ip_address.to_string(),
            provider: geo.provider.to_string(),
            country_code: json_string(&payload, "country_code"),
            country_name: json_string(&payload, "country"),
            region_name: json_string(&payload, "region"),
            city: json_string(&payload, "city"),
            latitude: json_f64(&payload, "latitude"),
            longitude: json_f64(&payload, "longitude"),
            timezone: json_string(connection.unwrap_or(&payload), "timezone").or_else(|| {
                payload
                    .get("timezone")
                    .and_then(|v| v.get("id"))
                    .and_then(|v| v.as_str())
                    .map(str::to_string)
            }),
            isp: connection
                .and_then(|value| value.get("isp"))
                .and_then(|value| value.as_str())
                .map(str::to_string),
            organization: connection
                .and_then(|value| value.get("org"))
                .and_then(|value| value.as_str())
                .map(str::to_string),
            asn: connection
                .and_then(|value| value.get("asn"))
                .and_then(|value| {
                    value
                        .as_str()
                        .map(str::to_string)
                        .or_else(|| value.as_i64().map(|n| n.to_string()))
                }),
            is_vpn: security
                .and_then(|value| value.get("vpn"))
                .and_then(|v| v.as_bool()),
            is_proxy: security
                .and_then(|value| value.get("proxy"))
                .and_then(|v| v.as_bool())
                .or_else(|| {
                    security
                        .and_then(|value| value.get("anonymous"))
                        .and_then(|v| v.as_bool())
                }),
            is_tor: security
                .and_then(|value| value.get("tor"))
                .and_then(|v| v.as_bool()),
            is_hosting: security
                .and_then(|value| value.get("hosting"))
                .and_then(|v| v.as_bool()),
            raw_payload: payload,
            lookup_expires_at,
        }))
    }

    async fn lookup_ipapi_profile(
        &self,
        ip_address: &str,
    ) -> AuthResult<Option<UpsertIpProfileRecord>> {
        let geo = &self.config.advanced.session_intelligence.geoip;
        let base_url = geo.base_url.trim().trim_end_matches('/');
        if base_url.is_empty() {
            return Ok(None);
        }

        let client = build_geoip_http_client(geo.timeout_ms.max(100))?;

        let mut request = client
            .get(format!("{base_url}/{ip_address}/json/"))
            .header("accept", "application/json");
        if let Some(api_key) = geo
            .api_key
            .as_deref()
            .map(str::trim)
            .filter(|v| !v.is_empty())
        {
            let header_name = geo
                .api_key_header
                .as_deref()
                .map(str::trim)
                .filter(|v| !v.is_empty())
                .unwrap_or("x-api-key");
            request = request.header(header_name, api_key);
        }

        let response = request
            .send()
            .await
            .map_err(|error| AuthError::internal(format!("geoip request failed: {error}")))?;
        if !response.status().is_success() {
            return Err(AuthError::internal(format!(
                "geoip request returned status {}",
                response.status()
            )));
        }

        let payload: serde_json::Value = response.json().await.map_err(|error| {
            AuthError::internal(format!("invalid geoip response payload: {error}"))
        })?;
        if payload
            .get("error")
            .and_then(serde_json::Value::as_bool)
            .unwrap_or(false)
        {
            return Ok(None);
        }

        let cache_ttl = geo.cache_ttl.max(chrono::Duration::seconds(1));
        let lookup_expires_at = Some(Utc::now() + cache_ttl);

        Ok(Some(UpsertIpProfileRecord {
            ip_address: ip_address.to_string(),
            provider: geo.provider.to_string(),
            country_code: json_string(&payload, "country_code"),
            country_name: json_string(&payload, "country_name"),
            region_name: json_string(&payload, "region"),
            city: json_string(&payload, "city"),
            latitude: json_f64(&payload, "latitude"),
            longitude: json_f64(&payload, "longitude"),
            timezone: json_string(&payload, "timezone"),
            isp: json_string(&payload, "org"),
            organization: json_string(&payload, "org"),
            asn: json_string(&payload, "asn"),
            is_vpn: None,
            is_proxy: None,
            is_tor: None,
            is_hosting: None,
            raw_payload: payload,
            lookup_expires_at,
        }))
    }

    fn cache_user_snapshot(
        &self,
        user_id: &str,
        user: &impl AuthUser,
    ) -> Result<(), serde_json::Error> {
        let user_json = serde_json::to_value(user)?;
        self.session_user_cache
            .insert(user_id.to_string(), user_json);
        Ok(())
    }
}

fn get_header_case_insensitive<'a>(
    req: &'a crate::types::AuthRequest,
    name: &str,
) -> Option<&'a str> {
    req.headers
        .iter()
        .find(|(header_name, _)| header_name.eq_ignore_ascii_case(name))
        .map(|(_, header_value)| header_value.as_str())
}

fn normalize_optional_string(value: Option<String>) -> Option<String> {
    value
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn json_string(value: &serde_json::Value, key: &str) -> Option<String> {
    value.get(key).and_then(|v| v.as_str()).map(str::to_string)
}

fn json_f64(value: &serde_json::Value, key: &str) -> Option<f64> {
    value.get(key).and_then(|v| v.as_f64())
}

fn build_geoip_http_client(timeout_ms: u64) -> AuthResult<reqwest::Client> {
    let builder = reqwest::Client::builder();
    #[cfg(not(target_arch = "wasm32"))]
    let builder = builder.timeout(std::time::Duration::from_millis(timeout_ms));
    #[cfg(target_arch = "wasm32")]
    let _ = timeout_ms;

    builder
        .build()
        .map_err(|error| AuthError::internal(format!("failed to build geoip HTTP client: {error}")))
}

fn should_skip_geo_lookup(ip_address: &str) -> bool {
    let Ok(ip) = IpAddr::from_str(ip_address) else {
        return false;
    };

    match ip {
        IpAddr::V4(v4) => {
            v4.is_private()
                || v4.is_loopback()
                || v4.is_link_local()
                || v4.is_multicast()
                || v4.is_unspecified()
                || v4.is_documentation()
                || v4.is_broadcast()
        }
        IpAddr::V6(v6) => {
            v6.is_loopback() || v6.is_unspecified() || v6.is_multicast() || v6.is_unique_local()
        }
    }
}

pub fn resolve_client_ip_from_headers(
    headers: &HashMap<String, String>,
    configured_ip_headers: &[String],
    trusted_proxy_headers: &[String],
) -> Option<String> {
    const INTERNAL_AXUM_REMOTE_IP_HEADER: &str = "x-athena-client-ip";
    const COMMON_PROXY_IP_HEADERS: [&str; 7] = [
        "x-forwarded-for",
        "x-real-ip",
        "cf-connecting-ip",
        "true-client-ip",
        "x-client-ip",
        "x-cluster-client-ip",
        "forwarded",
    ];

    let mut ordered_headers: Vec<&str> = configured_ip_headers.iter().map(String::as_str).collect();
    for header in trusted_proxy_headers {
        let header = header.as_str();
        if !ordered_headers
            .iter()
            .any(|candidate| candidate.eq_ignore_ascii_case(header))
        {
            ordered_headers.push(header);
        }
    }

    if ordered_headers.is_empty() {
        for header in COMMON_PROXY_IP_HEADERS {
            if !ordered_headers
                .iter()
                .any(|candidate| candidate.eq_ignore_ascii_case(header))
            {
                ordered_headers.push(header);
            }
        }
    }

    if !ordered_headers
        .iter()
        .any(|candidate| candidate.eq_ignore_ascii_case(INTERNAL_AXUM_REMOTE_IP_HEADER))
    {
        ordered_headers.push(INTERNAL_AXUM_REMOTE_IP_HEADER);
    }

    ordered_headers.iter().find_map(|header| {
        headers
            .iter()
            .find(|(name, _)| name.eq_ignore_ascii_case(header))
            .map(|(_, value)| value.as_str())
            .and_then(normalize_ip_address_value)
    })
}

pub fn normalize_ip_address_value(raw: &str) -> Option<String> {
    let candidate = raw
        .split(',')
        .next()
        .map(str::trim)
        .unwrap_or_default()
        .trim_matches('"')
        .trim();
    if candidate.is_empty() || candidate.eq_ignore_ascii_case("unknown") {
        return None;
    }

    let candidate = candidate
        .strip_prefix("for=")
        .unwrap_or(candidate)
        .split(';')
        .next()
        .map(str::trim)
        .unwrap_or_default();
    if candidate.is_empty() {
        return None;
    }
    if let Ok(ip) = IpAddr::from_str(candidate) {
        return Some(ip.to_string());
    }
    if let Ok(socket_addr) = SocketAddr::from_str(candidate) {
        return Some(socket_addr.ip().to_string());
    }
    let candidate = candidate
        .trim_start_matches('[')
        .trim_end_matches(']')
        .trim();
    if let Ok(ip) = IpAddr::from_str(candidate) {
        return Some(ip.to_string());
    }
    Some(candidate.to_string())
}

#[cfg(test)]
mod tests {
    use super::{normalize_ip_address_value, resolve_client_ip_from_headers};
    use std::collections::HashMap;

    #[test]
    fn normalize_forwarded_token_with_parameters() {
        assert_eq!(
            normalize_ip_address_value("for=203.0.113.7;proto=https;by=203.0.113.1"),
            Some("203.0.113.7".to_string())
        );
    }

    #[test]
    fn normalize_forwarded_token_with_quotes() {
        assert_eq!(
            normalize_ip_address_value("\"for=203.0.113.8;proto=https\""),
            Some("203.0.113.8".to_string())
        );
    }

    #[test]
    fn resolve_client_ip_from_headers_uses_internal_fallback_after_trusted_headers() {
        let headers = HashMap::from([
            (
                "x-athena-client-ip".to_string(),
                "198.51.100.40".to_string(),
            ),
            ("x-real-ip".to_string(), "198.51.100.41".to_string()),
        ]);

        assert_eq!(
            resolve_client_ip_from_headers(
                &headers,
                &["x-real-ip".to_string()],
                &["cf-connecting-ip".to_string()],
            ),
            Some("198.51.100.41".to_string())
        );
    }
}
