use crate::observability::config::{FuzzyMatchType, FuzzyRuleConfig, FuzzyRuleKind};
use anyhow::{bail, Result};
use regex::Regex;

const MAX_REGEX_RULES: usize = 32;
const MAX_REGEX_PATTERN_LEN: usize = 256;

/// Compiled target/span fuzzy rule.
#[derive(Debug, Clone)]
pub struct FuzzyRule {
    /// Rule kind.
    pub kind: FuzzyRuleKind,
    /// Match operation.
    pub match_type: FuzzyMatchType,
    /// Original pattern.
    pub pattern: String,
    /// Optional diagnostic level marker.
    pub level: Option<String>,
    regex: Option<Regex>,
}

impl FuzzyRule {
    /// Compiles one fuzzy rule.
    pub fn compile(config: FuzzyRuleConfig) -> Result<Self> {
        let regex = match config.match_type {
            FuzzyMatchType::Regex => {
                validate_regex_pattern(&config.pattern)?;
                Some(Regex::new(&config.pattern)?)
            }
            FuzzyMatchType::Glob => {
                let pattern = glob_to_regex(&config.pattern);
                validate_regex_pattern(&pattern)?;
                Some(Regex::new(&pattern)?)
            }
            _ => None,
        };

        Ok(Self {
            kind: config.kind,
            match_type: config.match_type,
            pattern: config.pattern,
            level: config.level,
            regex,
        })
    }

    fn matches_text(&self, value: &str) -> bool {
        match self.match_type {
            FuzzyMatchType::Exact => value == self.pattern,
            FuzzyMatchType::Prefix => value.starts_with(&self.pattern),
            FuzzyMatchType::Contains => value.contains(&self.pattern),
            FuzzyMatchType::Glob | FuzzyMatchType::Regex => self
                .regex
                .as_ref()
                .map(|regex| regex.is_match(value))
                .unwrap_or(false),
        }
    }
}

/// Target/span fuzzy filter.
#[derive(Debug, Clone)]
pub struct FuzzyFilter {
    target_rules: Vec<FuzzyRule>,
    span_rules: Vec<FuzzyRule>,
}

/// Indexable target fuzzy rule.
#[derive(Debug, Clone, Copy)]
pub(crate) enum TargetIndexRule<'a> {
    /// Exact target match.
    Exact(&'a str),
    /// Target prefix match.
    Prefix(&'a str),
}

impl FuzzyFilter {
    /// Compiles fuzzy rules into a runtime filter.
    ///
    /// Empty rule lists should be represented as `None` by callers so no fuzzy
    /// work is performed after `EnvFilter`.
    pub fn compile(configs: Vec<FuzzyRuleConfig>) -> Result<Option<Self>> {
        if configs.is_empty() {
            return Ok(None);
        }

        let regex_count = configs
            .iter()
            .filter(|rule| {
                matches!(
                    rule.match_type,
                    FuzzyMatchType::Regex | FuzzyMatchType::Glob
                )
            })
            .count();
        if regex_count > MAX_REGEX_RULES {
            bail!(
                "too many fuzzy regex/glob rules: {} > {}",
                regex_count,
                MAX_REGEX_RULES
            );
        }

        let mut target_rules = Vec::new();
        let mut span_rules = Vec::new();
        for rule in configs
            .into_iter()
            .map(FuzzyRule::compile)
            .collect::<Result<Vec<_>>>()?
        {
            match rule.kind {
                FuzzyRuleKind::Target => target_rules.push(rule),
                FuzzyRuleKind::Span => span_rules.push(rule),
            }
        }

        Ok(Some(Self {
            target_rules,
            span_rules,
        }))
    }

    /// Returns true when at least one target rule matches.
    pub fn matches_target(&self, target: &str) -> bool {
        self.target_rules
            .iter()
            .any(|rule| rule.matches_text(target))
    }

    /// Returns true when at least one span rule matches.
    pub fn matches_spans(&self, spans: &[&str]) -> bool {
        self.span_rules
            .iter()
            .any(|rule| spans.iter().any(|span| rule.matches_text(span)))
    }

    /// Returns true when at least one target or span rule matches.
    pub fn matches(&self, target: &str, spans: &[&str]) -> bool {
        self.matches_target(target) || self.matches_spans(spans)
    }

    /// Returns true when any rule needs active span names.
    pub fn needs_spans(&self) -> bool {
        !self.span_rules.is_empty()
    }

    /// Returns exact/prefix target patterns when the filter is fully indexable.
    pub(crate) fn target_index_rules(&self) -> Option<Vec<TargetIndexRule<'_>>> {
        if !self.span_rules.is_empty() {
            return None;
        }

        let mut index_rules = Vec::with_capacity(self.target_rules.len());
        for rule in &self.target_rules {
            match rule.match_type {
                FuzzyMatchType::Exact => index_rules.push(TargetIndexRule::Exact(&rule.pattern)),
                FuzzyMatchType::Prefix => index_rules.push(TargetIndexRule::Prefix(&rule.pattern)),
                FuzzyMatchType::Contains | FuzzyMatchType::Glob | FuzzyMatchType::Regex => {
                    return None;
                }
            }
        }

        Some(index_rules)
    }
}

fn validate_regex_pattern(pattern: &str) -> Result<()> {
    if pattern.len() > MAX_REGEX_PATTERN_LEN {
        bail!(
            "fuzzy regex pattern too long: {} > {}",
            pattern.len(),
            MAX_REGEX_PATTERN_LEN
        );
    }
    Ok(())
}

fn glob_to_regex(pattern: &str) -> String {
    let mut out = String::from("^");
    for ch in pattern.chars() {
        match ch {
            '*' => out.push_str(".*"),
            '?' => out.push('.'),
            _ => out.push_str(&regex::escape(&ch.to_string())),
        }
    }
    out.push('$');
    out
}

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

    fn rule(kind: FuzzyRuleKind, match_type: FuzzyMatchType, pattern: &str) -> FuzzyRuleConfig {
        FuzzyRuleConfig {
            kind,
            match_type,
            pattern: pattern.to_string(),
            level: None,
        }
    }

    #[test]
    fn target_match_modes_work() {
        let filter = FuzzyFilter::compile(vec![
            rule(FuzzyRuleKind::Target, FuzzyMatchType::Exact, "my_app::rpc"),
            rule(FuzzyRuleKind::Target, FuzzyMatchType::Prefix, "my_app::api"),
            rule(FuzzyRuleKind::Target, FuzzyMatchType::Contains, "decoder"),
            rule(
                FuzzyRuleKind::Target,
                FuzzyMatchType::Regex,
                "^my_app::cache::.*",
            ),
        ])
        .unwrap()
        .unwrap();

        assert!(filter.matches("my_app::rpc", &[]));
        assert!(filter.matches("my_app::api::v1", &[]));
        assert!(filter.matches("my_app::decoder", &[]));
        assert!(filter.matches("my_app::cache::reader", &[]));
        assert!(!filter.matches("other", &[]));
    }

    #[test]
    fn span_contains_works() {
        let filter = FuzzyFilter::compile(vec![rule(
            FuzzyRuleKind::Span,
            FuzzyMatchType::Contains,
            "deserialize",
        )])
        .unwrap()
        .unwrap();

        assert!(filter.matches("my_app::decoder", &["root", "deserialize_packet"]));
        assert!(!filter.matches("my_app::decoder", &["root", "encode_packet"]));
    }

    #[test]
    fn empty_rules_compile_to_none() {
        assert!(FuzzyFilter::compile(Vec::new()).unwrap().is_none());
    }

    #[test]
    fn needs_spans_tracks_span_rules() {
        let target_filter = FuzzyFilter::compile(vec![rule(
            FuzzyRuleKind::Target,
            FuzzyMatchType::Contains,
            "decoder",
        )])
        .unwrap()
        .unwrap();
        let span_filter = FuzzyFilter::compile(vec![rule(
            FuzzyRuleKind::Span,
            FuzzyMatchType::Contains,
            "decode",
        )])
        .unwrap()
        .unwrap();

        assert!(!target_filter.needs_spans());
        assert!(span_filter.needs_spans());
    }

    #[test]
    fn only_exact_and_prefix_target_rules_are_indexable() {
        let indexable = FuzzyFilter::compile(vec![
            rule(FuzzyRuleKind::Target, FuzzyMatchType::Exact, "my_app::rpc"),
            rule(FuzzyRuleKind::Target, FuzzyMatchType::Prefix, "my_app::api"),
        ])
        .unwrap()
        .unwrap();
        let contains = FuzzyFilter::compile(vec![rule(
            FuzzyRuleKind::Target,
            FuzzyMatchType::Contains,
            "decoder",
        )])
        .unwrap()
        .unwrap();

        assert_eq!(indexable.target_index_rules().unwrap().len(), 2);
        assert!(contains.target_index_rules().is_none());
    }
}
