use super::{build_claim, Extractor};
use crate::types::{Language, Observation};
use regex::Regex;
use stemedb_core::types::ObjectValue;

/// Extracts actual values from Option<T> fields set to Some(n).
///
/// This extractor identifies configuration fields that use Option<T> types
/// and extracts the concrete value when set to Some(value), enabling
/// threshold comparisons (e.g., "max_redirects should be <= 10").
///
/// # Examples
///
/// Detects patterns like:
/// ```rust
/// pub struct Config {
///     pub max_redirects: Option<usize>,  // ← Field declaration
/// }
///
/// impl Default for Config {
///     fn default() -> Self {
///         Self {
///             max_redirects: Some(20),  // ← Extract value: 20
///         }
///     }
/// }
/// ```
///
/// Creates observation:
/// ```
/// concept_path: "httpclient/max_redirects"
/// predicate: "max_value"
/// value: "20"  // Extracted for threshold comparison
/// ```
pub struct OptionValueExtractor {
    /// Matches: pub field_name: Option<Type>
    field_pattern: Regex,
    /// Matches: field_name: Some(value)
    some_pattern: Regex,
}

impl OptionValueExtractor {
    /// Create a new OptionValueExtractor.
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            field_pattern: Regex::new(r"pub\s+(\w+):\s*Option<(?:usize|u32|u64|i32|i64|Duration)>")
                .expect("valid regex"),
            some_pattern: Regex::new(r"(\w+):\s*Some\((\d+)\)").expect("valid regex"),
        }
    }
}

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

impl Extractor for OptionValueExtractor {
    fn name(&self) -> &str {
        "option_value"
    }

    fn languages(&self) -> &[Language] {
        &[Language::Rust]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        _language: Language,
        file: &str,
    ) -> Vec<Observation> {
        let mut observations = Vec::new();

        // Find all Option<T> fields in struct declarations
        let option_fields: Vec<String> =
            self.field_pattern.captures_iter(content).map(|cap| cap[1].to_string()).collect();

        // Find all Some(value) assignments and extract values
        for (line_num, line) in content.lines().enumerate() {
            if let Some(cap) = self.some_pattern.captures(line) {
                let field_name = &cap[1];
                let value = &cap[2];

                // Only create observation if this field is declared as Option<T>
                if option_fields.iter().any(|f| f == field_name) {
                    observations.push(build_claim(
                        path_segments,
                        &[field_name],
                        "max_value",
                        ObjectValue::Text(value.to_string()),
                        file,
                        line_num + 1,
                        line.trim(),
                        1.0, // Exact match - high confidence
                        &format!("{} set to Some({})", field_name, value),
                    ));
                }
            }
        }

        observations
    }

    fn screening_patterns(&self) -> Vec<&str> {
        vec!["Option<", "Some("]
    }

    fn verifiable_predicates(&self) -> Vec<(&str, &str)> {
        vec![
            ("max_redirects", "max_value"),
            ("max_retries", "max_value"),
            ("max_connections", "max_value"),
            ("idle_timeout", "max_value"),
            ("pool_size", "max_value"),
            ("max_lifetime", "max_value"),
        ]
    }
}

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

    #[test]
    fn test_extracts_some_value() {
        let content = r#"
            pub struct Config {
                pub max_redirects: Option<usize>,
            }

            impl Default for Config {
                fn default() -> Self {
                    Self {
                        max_redirects: Some(20),
                    }
                }
            }
        "#;

        let extractor = OptionValueExtractor::new();
        let obs = extractor.extract(
            &["httpclient".to_string(), "config".to_string()],
            content,
            Language::Rust,
            "config.rs",
        );

        assert_eq!(obs.len(), 1);
        assert_eq!(obs[0].predicate, "max_value");
        assert_eq!(obs[0].value, ObjectValue::Text("20".to_string()));
        assert!(obs[0].concept_path.contains("max_redirects"));
    }

    #[test]
    fn test_extracts_multiple_values() {
        let content = r#"
            pub struct Config {
                pub max_redirects: Option<usize>,
                pub max_retries: Option<u32>,
            }

            impl Default for Config {
                fn default() -> Self {
                    Self {
                        max_redirects: Some(20),
                        max_retries: Some(5),
                    }
                }
            }
        "#;

        let extractor = OptionValueExtractor::new();
        let obs = extractor.extract(&[], content, Language::Rust, "config.rs");

        assert_eq!(obs.len(), 2);

        let redirects = obs.iter().find(|o| o.concept_path.contains("max_redirects")).unwrap();
        assert_eq!(redirects.value, ObjectValue::Text("20".to_string()));

        let retries = obs.iter().find(|o| o.concept_path.contains("max_retries")).unwrap();
        assert_eq!(retries.value, ObjectValue::Text("5".to_string()));
    }

    #[test]
    fn test_ignores_none_assignments() {
        let content = r#"
            pub struct Config {
                pub max_redirects: Option<usize>,
            }

            impl Default for Config {
                fn default() -> Self {
                    Self {
                        max_redirects: None,
                    }
                }
            }
        "#;

        let extractor = OptionValueExtractor::new();
        let obs = extractor.extract(&[], content, Language::Rust, "config.rs");
        assert_eq!(obs.len(), 0); // Should not extract from None
    }

    #[test]
    fn test_ignores_non_option_fields() {
        let content = r#"
            pub struct Config {
                pub timeout: u64,
            }

            impl Default for Config {
                fn default() -> Self {
                    Self {
                        timeout: 30,
                    }
                }
            }
        "#;

        let extractor = OptionValueExtractor::new();
        let obs = extractor.extract(&[], content, Language::Rust, "config.rs");
        assert_eq!(obs.len(), 0); // Should not extract from non-Option fields
    }

    #[test]
    fn test_screening_patterns() {
        let extractor = OptionValueExtractor::new();
        let patterns = extractor.screening_patterns();
        assert!(patterns.contains(&"Option<"));
        assert!(patterns.contains(&"Some("));
    }

    #[test]
    fn test_verifiable_predicates() {
        let extractor = OptionValueExtractor::new();
        let predicates = extractor.verifiable_predicates();
        assert!(predicates.contains(&("max_redirects", "max_value")));
        assert!(predicates.contains(&("max_retries", "max_value")));
    }

    #[test]
    fn test_extracts_different_numeric_types() {
        let content = r#"
            pub struct Config {
                pub max_redirects: Option<usize>,
                pub timeout: Option<u32>,
                pub pool_size: Option<u64>,
            }

            impl Default for Config {
                fn default() -> Self {
                    Self {
                        max_redirects: Some(10),
                        timeout: Some(30),
                        pool_size: Some(100),
                    }
                }
            }
        "#;

        let extractor = OptionValueExtractor::new();
        let obs = extractor.extract(&[], content, Language::Rust, "config.rs");

        assert_eq!(obs.len(), 3);
        assert!(obs.iter().any(|o| o.value == ObjectValue::Text("10".to_string())));
        assert!(obs.iter().any(|o| o.value == ObjectValue::Text("30".to_string())));
        assert!(obs.iter().any(|o| o.value == ObjectValue::Text("100".to_string())));
    }
}
