use crate::config::{AuthConfig, extract_origin, normalize_configured_origin_pattern};
use crate::types::{AuthRequest, HttpMethod};
use serde::Serialize;
use std::collections::VecDeque;
use std::sync::{Mutex, OnceLock};

const MAX_STORED_REJECTED_ATTEMPTS: usize = 50;
const ORIGIN_DEBUG_NOTE: &str = "Recent rejected origin attempts are stored in-memory in the current Athena Auth process and reset on restart.";

static ORIGIN_TRUST_RECORDER: OnceLock<Mutex<VecDeque<OriginTrustViolationAttempt>>> =
    OnceLock::new();

fn recorder() -> &'static Mutex<VecDeque<OriginTrustViolationAttempt>> {
    ORIGIN_TRUST_RECORDER.get_or_init(|| Mutex::new(VecDeque::new()))
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OriginTrustViolationPhase {
    CorsPreflightRejected,
    CorsHeadersSuppressed,
    CsrfRejected,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OriginTrustSourceHeader {
    Origin,
    Referer,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OriginTrustCurrentRequest {
    pub origin_present: bool,
    pub source_header: Option<OriginTrustSourceHeader>,
    pub delivered_origin: Option<String>,
    pub normalized_delivered_origin: Option<String>,
    pub delivered_referer: Option<String>,
    pub trusted_origins_configured: bool,
    pub expected_base_origin: Option<String>,
    pub expected_trusted_origins: Vec<String>,
    pub expected_trusted_origins_raw: String,
    pub allowed: bool,
    pub suggested_trusted_origin: Option<String>,
    pub suggested_trusted_origins_raw: String,
    pub suggested_trusted_origins_env_line: Option<String>,
    pub suggestion_reason: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OriginTrustRuntimeSnapshot {
    pub base_origin: Option<String>,
    pub trusted_origins_raw: String,
    pub trusted_origins: Vec<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OriginTrustViolationAttempt {
    pub id: String,
    pub occurred_at: String,
    pub phase: OriginTrustViolationPhase,
    pub method: String,
    pub request_path: String,
    pub source_header: Option<OriginTrustSourceHeader>,
    pub delivered_origin: Option<String>,
    pub normalized_delivered_origin: Option<String>,
    pub delivered_referer: Option<String>,
    pub expected_base_origin: Option<String>,
    pub expected_trusted_origins: Vec<String>,
    pub expected_trusted_origins_raw: String,
    pub suggested_trusted_origin: Option<String>,
    pub suggested_trusted_origins_raw: String,
    pub suggested_trusted_origins_env_line: Option<String>,
    pub suggestion_reason: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OriginTrustDebugSnapshot {
    pub current_request: Option<OriginTrustCurrentRequest>,
    pub runtime: OriginTrustRuntimeSnapshot,
    pub max_stored_rejected_attempts: usize,
    pub rejected_attempt_count: usize,
    pub recent_rejected_attempts: Vec<OriginTrustViolationAttempt>,
    pub note: &'static str,
}

fn raw_trusted_origins(config: &AuthConfig) -> String {
    config.trusted_origins.join(",")
}

fn normalized_base_origin(config: &AuthConfig) -> Option<String> {
    normalize_configured_origin_pattern(&config.base_url)
}

fn normalized_trusted_origins(config: &AuthConfig) -> Vec<String> {
    config
        .trusted_origins
        .iter()
        .filter_map(|value| normalize_configured_origin_pattern(value))
        .collect()
}

fn resolved_request_origin(
    origin: Option<&str>,
    referer: Option<&str>,
) -> (
    Option<OriginTrustSourceHeader>,
    Option<String>,
    Option<String>,
) {
    if let Some(origin) = origin.and_then(extract_origin) {
        return (
            Some(OriginTrustSourceHeader::Origin),
            Some(origin),
            referer.map(str::to_string),
        );
    }

    if let Some(referer_value) = referer {
        return (
            Some(OriginTrustSourceHeader::Referer),
            extract_origin(referer_value),
            Some(referer_value.to_string()),
        );
    }

    (None, None, None)
}

fn build_suggested_trusted_origins_raw(
    config: &AuthConfig,
    suggested_trusted_origin: Option<&str>,
) -> String {
    let mut entries: Vec<String> = config.trusted_origins.clone();
    let normalized_existing: Vec<String> = normalized_trusted_origins(config);

    if let Some(suggested_trusted_origin) = suggested_trusted_origin
        && !normalized_existing
            .iter()
            .any(|value| value == suggested_trusted_origin)
    {
        entries.push(suggested_trusted_origin.to_string());
    }

    entries.join(",")
}

fn build_suggestion_reason(suggested_trusted_origin: Option<&str>) -> String {
    suggested_trusted_origin.map_or_else(
        || {
            "The delivered Origin/Referer headers were missing or invalid, so there is no safe TRUSTED_ORIGINS addition to suggest.".to_string()
        },
        |origin| {
            format!(
                "Add {origin} to TRUSTED_ORIGINS or widen an existing wildcard so it matches this origin."
            )
        },
    )
}

fn method_name(method: &HttpMethod) -> &'static str {
    match method {
        HttpMethod::Get => "GET",
        HttpMethod::Post => "POST",
        HttpMethod::Put => "PUT",
        HttpMethod::Delete => "DELETE",
        HttpMethod::Patch => "PATCH",
        HttpMethod::Options => "OPTIONS",
        HttpMethod::Head => "HEAD",
    }
}

pub fn describe_origin_trust_for_headers(
    config: &AuthConfig,
    origin: Option<&str>,
    referer: Option<&str>,
) -> OriginTrustCurrentRequest {
    let expected_base_origin: Option<String> = normalized_base_origin(config);
    let expected_trusted_origins: Vec<String> = normalized_trusted_origins(config);
    let expected_trusted_origins_raw: String = raw_trusted_origins(config);
    let (source_header, delivered_origin, delivered_referer) =
        resolved_request_origin(origin, referer);
    let normalized_delivered_origin: Option<String> = delivered_origin.clone();
    let allowed: bool = delivered_origin
        .as_deref()
        .is_some_and(|value| config.is_origin_trusted(value));
    let suggested_trusted_origin: Option<String> = if allowed {
        None
    } else {
        normalized_delivered_origin.clone()
    };
    let suggested_trusted_origins_raw: String =
        build_suggested_trusted_origins_raw(config, suggested_trusted_origin.as_deref());
    let suggested_trusted_origins_env_line: Option<String> =
        if suggested_trusted_origins_raw.is_empty() {
            None
        } else {
            Some(format!("TRUSTED_ORIGINS={suggested_trusted_origins_raw}"))
        };

    OriginTrustCurrentRequest {
        origin_present: origin.is_some() || referer.is_some(),
        source_header,
        delivered_origin,
        normalized_delivered_origin: normalized_delivered_origin.clone(),
        delivered_referer,
        trusted_origins_configured: !config.trusted_origins.is_empty(),
        expected_base_origin,
        expected_trusted_origins,
        expected_trusted_origins_raw,
        allowed,
        suggested_trusted_origin: suggested_trusted_origin.clone(),
        suggested_trusted_origins_raw,
        suggested_trusted_origins_env_line,
        suggestion_reason: build_suggestion_reason(suggested_trusted_origin.as_deref()),
    }
}

pub fn build_origin_trust_runtime_snapshot(config: &AuthConfig) -> OriginTrustRuntimeSnapshot {
    OriginTrustRuntimeSnapshot {
        base_origin: normalized_base_origin(config),
        trusted_origins_raw: raw_trusted_origins(config),
        trusted_origins: normalized_trusted_origins(config),
    }
}

pub fn record_origin_trust_violation(
    config: &AuthConfig,
    req: &AuthRequest,
    phase: OriginTrustViolationPhase,
    origin: Option<&str>,
    referer: Option<&str>,
) -> OriginTrustViolationAttempt {
    let described: OriginTrustCurrentRequest =
        describe_origin_trust_for_headers(config, origin, referer);
    let attempt = OriginTrustViolationAttempt {
        id: uuid::Uuid::new_v4().to_string(),
        occurred_at: chrono::Utc::now().to_rfc3339(),
        phase,
        method: method_name(req.method()).to_string(),
        request_path: req.path().to_string(),
        source_header: described.source_header,
        delivered_origin: described.delivered_origin,
        normalized_delivered_origin: described.normalized_delivered_origin,
        delivered_referer: described.delivered_referer,
        expected_base_origin: described.expected_base_origin,
        expected_trusted_origins: described.expected_trusted_origins,
        expected_trusted_origins_raw: described.expected_trusted_origins_raw,
        suggested_trusted_origin: described.suggested_trusted_origin,
        suggested_trusted_origins_raw: described.suggested_trusted_origins_raw,
        suggested_trusted_origins_env_line: described.suggested_trusted_origins_env_line,
        suggestion_reason: described.suggestion_reason,
    };

    let mut guard = recorder().lock().expect("origin trust recorder poisoned");
    guard.push_front(attempt.clone());
    while guard.len() > MAX_STORED_REJECTED_ATTEMPTS {
        guard.pop_back();
    }

    attempt
}

pub fn snapshot_origin_trust_debug(
    config: &AuthConfig,
    origin: Option<&str>,
    referer: Option<&str>,
) -> OriginTrustDebugSnapshot {
    let guard = recorder().lock().expect("origin trust recorder poisoned");
    OriginTrustDebugSnapshot {
        current_request: Some(describe_origin_trust_for_headers(config, origin, referer)),
        runtime: build_origin_trust_runtime_snapshot(config),
        max_stored_rejected_attempts: MAX_STORED_REJECTED_ATTEMPTS,
        rejected_attempt_count: guard.len(),
        recent_rejected_attempts: guard.iter().cloned().collect(),
        note: ORIGIN_DEBUG_NOTE,
    }
}

pub fn maybe_record_suppressed_cors_attempt(config: &AuthConfig, req: &AuthRequest) {
    if !matches!(req.method(), HttpMethod::Get | HttpMethod::Head) {
        return;
    }

    let origin = req.header_ignore_ascii_case("origin").map(String::as_str);
    if origin.is_none() {
        return;
    }

    let referer = req.header_ignore_ascii_case("referer").map(String::as_str);
    let described = describe_origin_trust_for_headers(config, origin, referer);
    if described.allowed {
        return;
    }

    record_origin_trust_violation(
        config,
        req,
        OriginTrustViolationPhase::CorsHeadersSuppressed,
        origin,
        referer,
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_config() -> AuthConfig {
        AuthConfig::new("server-test-secret-key-at-least-32-chars")
            .base_url("https://auth.example.com")
            .trusted_origins(vec![
                "https://app.example.com".to_string(),
                "https://*.preview.example.com".to_string(),
            ])
    }

    #[test]
    fn describes_origin_with_expected_and_suggested_entries() {
        let described = describe_origin_trust_for_headers(
            &test_config(),
            Some("https://evil.example.com"),
            None,
        );

        assert_eq!(
            described.expected_base_origin,
            Some("https://auth.example.com".to_string())
        );
        assert_eq!(
            described.suggested_trusted_origin,
            Some("https://evil.example.com".to_string())
        );
        assert_eq!(
            described.suggested_trusted_origins_env_line,
            Some(
                "TRUSTED_ORIGINS=https://app.example.com,https://*.preview.example.com,https://evil.example.com"
                    .to_string()
            )
        );
    }

    #[test]
    fn falls_back_to_referer_when_origin_is_missing() {
        let described = describe_origin_trust_for_headers(
            &test_config(),
            None,
            Some("https://evil.example.com/login?x=1"),
        );

        assert!(matches!(
            described.source_header,
            Some(OriginTrustSourceHeader::Referer)
        ));
        assert_eq!(
            described.delivered_origin,
            Some("https://evil.example.com".to_string())
        );
        assert_eq!(
            described.delivered_referer,
            Some("https://evil.example.com/login?x=1".to_string())
        );
    }
}
