use regex::Regex;
use serde::Deserialize;
use std::path::Path;

#[derive(Debug, Deserialize)]
struct FilterFile {
    #[serde(default)]
    rules: Vec<RawFilterRule>,
}

#[derive(Debug, Deserialize)]
struct RawFilterRule {
    command: Option<String>,
    pattern: Option<String>,
    replace: Option<String>,
    #[serde(default)]
    keep_lines: Vec<String>,
}

#[derive(Debug)]
struct CompiledRule {
    command_re: Option<Regex>,
    pattern_re: Option<Regex>,
    replace: Option<String>,
    keep_lines: Vec<String>,
}

/// Regex-based output filter engine loaded from user-defined TOML rules.
pub struct FilterEngine {
    rules: Vec<CompiledRule>,
}

impl FilterEngine {
    /// Loads and compiles all filter rules from `~/.lean-ctx/filters/*.toml`.
    pub fn load() -> Option<Self> {
        let dir = crate::core::data_dir::lean_ctx_data_dir()
            .ok()?
            .join("filters");
        if !dir.exists() {
            return None;
        }
        let entries = std::fs::read_dir(&dir).ok()?;
        let mut rules = Vec::new();

        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("toml") {
                continue;
            }
            if let Ok(content) = std::fs::read_to_string(&path) {
                match toml::from_str::<FilterFile>(&content) {
                    Ok(file) => {
                        for raw in file.rules {
                            rules.push(compile_rule(raw, &path));
                        }
                    }
                    Err(e) => {
                        tracing::warn!("lean-ctx: filter parse error in {}: {e}", path.display());
                    }
                }
            }
        }

        if rules.is_empty() {
            None
        } else {
            Some(FilterEngine { rules })
        }
    }

    /// Applies the first matching filter rule to the command output.
    pub fn apply(&self, command: &str, output: &str) -> Option<String> {
        let cmd_lower = command.to_ascii_lowercase();

        for rule in &self.rules {
            if let Some(ref cmd_re) = rule.command_re
                && !cmd_re.is_match(&cmd_lower)
            {
                continue;
            }

            if !rule.keep_lines.is_empty() {
                let filtered: Vec<&str> = output
                    .lines()
                    .filter(|line| {
                        rule.keep_lines
                            .iter()
                            .any(|pattern| line.contains(pattern.as_str()))
                    })
                    .collect();
                if !filtered.is_empty() {
                    return Some(filtered.join("\n"));
                }
            }

            if let (Some(pat_re), Some(replacement)) = (&rule.pattern_re, &rule.replace) {
                let result = pat_re.replace_all(output, replacement.as_str());
                if result != output {
                    return Some(result.to_string());
                }
            }
        }

        None
    }

    /// Returns a human-readable summary of each loaded filter rule.
    pub fn list_rules(&self) -> Vec<String> {
        self.rules
            .iter()
            .map(|r| {
                let cmd = r
                    .command_re
                    .as_ref()
                    .map_or_else(|| "*".to_string(), |re| re.as_str().to_string());
                if !r.keep_lines.is_empty() {
                    format!("  {cmd} -> keep lines: {:?}", r.keep_lines)
                } else if let Some(ref pat) = r.pattern_re {
                    let repl = r.replace.as_deref().unwrap_or("...");
                    format!("  {cmd} -> /{pat}/ => {repl}")
                } else {
                    format!("  {cmd} -> (no action)")
                }
            })
            .collect()
    }
}

const MAX_FILTER_REGEX_SIZE: usize = 1 << 20; // 1 MiB DFA limit (matches ctx_search)

fn build_bounded_regex(pattern: &str, source_path: &Path) -> Option<Regex> {
    regex::RegexBuilder::new(pattern)
        .size_limit(MAX_FILTER_REGEX_SIZE)
        .dfa_size_limit(MAX_FILTER_REGEX_SIZE)
        .build()
        .map_err(|e| {
            tracing::warn!("lean-ctx: invalid regex in {}: {e}", source_path.display());
        })
        .ok()
}

fn compile_rule(raw: RawFilterRule, path: &Path) -> CompiledRule {
    let command_re = raw
        .command
        .as_ref()
        .and_then(|s| build_bounded_regex(s, path));
    let pattern_re = raw
        .pattern
        .as_ref()
        .and_then(|s| build_bounded_regex(s, path));

    CompiledRule {
        command_re,
        pattern_re,
        replace: raw.replace,
        keep_lines: raw.keep_lines,
    }
}

/// Validates a filter TOML file, returning the number of valid rules or an error.
pub fn validate_filter_file(path: &str) -> Result<usize, String> {
    let content = std::fs::read_to_string(path).map_err(|e| format!("Cannot read {path}: {e}"))?;
    let file: FilterFile =
        toml::from_str(&content).map_err(|e| format!("TOML parse error: {e}"))?;

    let source = std::path::Path::new(path);
    let mut valid = 0;
    for (i, rule) in file.rules.iter().enumerate() {
        if let Some(ref cmd) = rule.command {
            regex::RegexBuilder::new(cmd)
                .size_limit(MAX_FILTER_REGEX_SIZE)
                .dfa_size_limit(MAX_FILTER_REGEX_SIZE)
                .build()
                .map_err(|e| format!("Rule {}: invalid command regex: {e}", i + 1))?;
        }
        if let Some(ref pat) = rule.pattern {
            regex::RegexBuilder::new(pat)
                .size_limit(MAX_FILTER_REGEX_SIZE)
                .dfa_size_limit(MAX_FILTER_REGEX_SIZE)
                .build()
                .map_err(|e| format!("Rule {}: invalid pattern regex: {e}", i + 1))?;
        }
        valid += 1;
    }
    let _ = source;
    Ok(valid)
}

/// Creates an example filter TOML file in the filters directory.
pub fn create_example_filter() -> Result<String, String> {
    let dir = crate::core::data_dir::lean_ctx_data_dir()?.join("filters");
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;

    let path = dir.join("example.toml");
    if path.exists() {
        return Err(format!("{} already exists", path.display()));
    }

    let content = r#"# lean-ctx custom filter example
# Place .toml files in $LEAN_CTX_DATA_DIR/filters (default: ~/.lean-ctx/filters) to define custom compression rules.
# User filters are applied BEFORE builtin patterns.

# Rule 1: Replace verbose upload logs with a summary
# [[rules]]
# command = "myapp deploy"
# pattern = "Uploading .+ to s3://.+"
# replace = "[uploaded to S3]"

# Rule 2: Keep only important lines from terraform plan
# [[rules]]
# command = "terraform plan"
# keep_lines = ["Plan:", "Changes:", "Error:", "No changes"]
"#;

    std::fs::write(&path, content).map_err(|e| e.to_string())?;
    Ok(path.to_string_lossy().to_string())
}

/// Applies aggressive compression (strips comments, blank lines, normalizes indent).
pub fn aggressive_filter(content: &str) -> String {
    crate::core::compressor::aggressive_compress(content, None)
}

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

    #[test]
    fn empty_engine_returns_none() {
        let engine = FilterEngine { rules: Vec::new() };
        assert!(engine.apply("git status", "on branch main").is_none());
    }

    #[test]
    fn keep_lines_filter() {
        let engine = FilterEngine {
            rules: vec![CompiledRule {
                command_re: Some(Regex::new("terraform").unwrap()),
                pattern_re: None,
                replace: None,
                keep_lines: vec!["Plan:".to_string(), "Error:".to_string()],
            }],
        };
        let output = "Loading...\nInitializing...\nPlan: 3 to add\nDone.";
        let result = engine.apply("terraform plan", output);
        assert_eq!(result, Some("Plan: 3 to add".to_string()));
    }

    #[test]
    fn regex_replace_filter() {
        let engine = FilterEngine {
            rules: vec![CompiledRule {
                command_re: Some(Regex::new("deploy").unwrap()),
                pattern_re: Some(Regex::new(r"Uploading \S+ to s3://\S+").unwrap()),
                replace: Some("[uploaded to S3]".to_string()),
                keep_lines: Vec::new(),
            }],
        };
        let output = "Starting deploy\nUploading app.zip to s3://bucket/key\nDone";
        let result = engine.apply("myapp deploy prod", output);
        assert!(result.unwrap().contains("[uploaded to S3]"));
    }

    #[test]
    fn command_mismatch_skips() {
        let engine = FilterEngine {
            rules: vec![CompiledRule {
                command_re: Some(Regex::new("terraform").unwrap()),
                pattern_re: None,
                replace: None,
                keep_lines: vec!["Plan:".to_string()],
            }],
        };
        assert!(engine.apply("git status", "Plan: something").is_none());
    }
}
