//! L7 Routing Engine
//!
//! Evaluates L7 policies and rules to determine request routing.

use axum::extract::Request;
use axum::http::{HeaderMap, Uri};
use std::sync::Arc;

use crate::metadata::LbMetadataStore;
use fiberlb_types::{
    L7CompareType, L7Policy, L7PolicyAction, L7Rule, L7RuleType, ListenerId, PoolId,
};

/// Request information extracted for routing (Send + Sync safe)
#[derive(Debug, Clone)]
pub struct RequestInfo {
    pub headers: HeaderMap,
    pub uri: Uri,
    pub sni_hostname: Option<String>,
}

impl RequestInfo {
    /// Extract routing info from request
    pub fn from_request(request: &Request) -> Self {
        Self {
            headers: request.headers().clone(),
            uri: request.uri().clone(),
            sni_hostname: request.extensions().get::<SniHostname>().map(|s| s.0.clone()),
        }
    }
}

/// Routing decision result
#[derive(Debug, Clone)]
pub enum RoutingResult {
    /// Route to a specific pool
    Pool(PoolId),
    /// HTTP redirect to URL
    Redirect { url: String, status: u32 },
    /// Reject with status code
    Reject { status: u32 },
    /// Use default pool (no policy matched)
    Default,
}

/// L7 routing engine
pub struct L7Router {
    metadata: Arc<LbMetadataStore>,
}

impl L7Router {
    /// Create a new L7 router
    pub fn new(metadata: Arc<LbMetadataStore>) -> Self {
        Self { metadata }
    }

    /// Evaluate policies for a request
    pub async fn evaluate(
        &self,
        listener_id: &ListenerId,
        request_info: &RequestInfo,
    ) -> RoutingResult {
        // Load policies ordered by position
        let policies = match self.metadata.list_l7_policies(listener_id).await {
            Ok(p) => p,
            Err(e) => {
                tracing::warn!("Failed to load L7 policies: {}", e);
                return RoutingResult::Default;
            }
        };

        // Iterate through policies in order
        for policy in policies.iter().filter(|p| p.enabled) {
            // Load rules for this policy
            let rules = match self.metadata.list_l7_rules(&policy.id).await {
                Ok(r) => r,
                Err(e) => {
                    tracing::warn!("Failed to load L7 rules for policy {}: {}", policy.id, e);
                    continue;
                }
            };

            // All rules must match (AND logic)
            let all_match = rules.iter().all(|rule| self.evaluate_rule(rule, request_info));

            if all_match {
                return self.apply_policy_action(policy);
            }
        }

        RoutingResult::Default
    }

    /// Evaluate a single rule
    fn evaluate_rule(&self, rule: &L7Rule, info: &RequestInfo) -> bool {
        let value = match rule.rule_type {
            L7RuleType::HostName => {
                // Extract from Host header
                info.headers
                    .get("host")
                    .and_then(|v| v.to_str().ok())
                    .map(|s| s.to_string())
            }
            L7RuleType::Path => {
                // Extract from request URI
                Some(info.uri.path().to_string())
            }
            L7RuleType::FileType => {
                // Extract file extension from path
                info.uri
                    .path()
                    .rsplit('.')
                    .next()
                    .filter(|ext| !ext.is_empty() && !ext.contains('/'))
                    .map(|s| format!(".{}", s))
            }
            L7RuleType::Header => {
                // Extract specific header by key
                rule.key.as_ref().and_then(|key| {
                    info.headers
                        .get(key)
                        .and_then(|v| v.to_str().ok())
                        .map(|s| s.to_string())
                })
            }
            L7RuleType::Cookie => {
                // Extract cookie value by key
                self.extract_cookie(info, rule.key.as_deref())
            }
            L7RuleType::SslConnHasSni => {
                // SNI extracted during TLS handshake (Phase 3)
                info.sni_hostname.clone()
            }
        };

        let matched = match value {
            Some(v) => self.compare(&v, &rule.value, rule.compare_type),
            None => false,
        };

        // Apply invert logic
        if rule.invert {
            !matched
        } else {
            matched
        }
    }

    /// Compare a value against a pattern
    fn compare(&self, value: &str, pattern: &str, compare_type: L7CompareType) -> bool {
        match compare_type {
            L7CompareType::EqualTo => value == pattern,
            L7CompareType::StartsWith => value.starts_with(pattern),
            L7CompareType::EndsWith => value.ends_with(pattern),
            L7CompareType::Contains => value.contains(pattern),
            L7CompareType::Regex => {
                // Compile regex on-the-fly (production should cache)
                regex::Regex::new(pattern)
                    .map(|r| r.is_match(value))
                    .unwrap_or(false)
            }
        }
    }

    /// Extract cookie value from request
    fn extract_cookie(&self, info: &RequestInfo, cookie_name: Option<&str>) -> Option<String> {
        let name = cookie_name?;

        info.headers
            .get("cookie")
            .and_then(|v| v.to_str().ok())
            .and_then(|cookies| {
                cookies.split(';').find_map(|c| {
                    let parts: Vec<_> = c.trim().splitn(2, '=').collect();
                    if parts.len() == 2 && parts[0] == name {
                        Some(parts[1].to_string())
                    } else {
                        None
                    }
                })
            })
    }

    /// Apply policy action
    fn apply_policy_action(&self, policy: &L7Policy) -> RoutingResult {
        match policy.action {
            L7PolicyAction::RedirectToPool => {
                if let Some(pool_id) = &policy.redirect_pool_id {
                    RoutingResult::Pool(*pool_id)
                } else {
                    tracing::warn!(
                        policy_id = %policy.id,
                        "RedirectToPool action but no pool_id configured"
                    );
                    RoutingResult::Default
                }
            }
            L7PolicyAction::RedirectToUrl => {
                if let Some(url) = &policy.redirect_url {
                    let status = policy.redirect_http_status_code.unwrap_or(302) as u32;
                    RoutingResult::Redirect {
                        url: url.clone(),
                        status,
                    }
                } else {
                    tracing::warn!(
                        policy_id = %policy.id,
                        "RedirectToUrl action but no URL configured"
                    );
                    RoutingResult::Default
                }
            }
            L7PolicyAction::Reject => {
                let status = policy.redirect_http_status_code.unwrap_or(403) as u32;
                RoutingResult::Reject { status }
            }
        }
    }
}

/// SNI hostname extension (for TLS connections)
#[derive(Debug, Clone)]
pub struct SniHostname(pub String);
