//! Derive pattern extractor for Rust.
//!
//! Tracks `#[derive(...)]` annotations to detect API consistency patterns.
//! Enables learning loop conventions like "all message types derive Serialize + Deserialize"
//! or "all errors derive Debug + Display + Error".

use regex::Regex;
use stemedb_core::types::ObjectValue;

use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// Extractor for Rust derive patterns.
///
/// Detects `#[derive(...)]` annotations to track API consistency.
/// This enables the learning loop to establish patterns like:
/// - All message types: Serialize, Deserialize, Debug, Clone
/// - All error types: Debug, Display, Error
/// - All config types: Deserialize, Debug, Clone
pub struct DerivePatternExtractor {
    /// Matches: #[derive(Debug, Clone, ...)]
    derive_attr: Regex,
    /// Matches: struct/enum name after derive
    type_decl: Regex,
}

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

impl DerivePatternExtractor {
    /// Create a new derive pattern extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Matches: #[derive(Debug, Clone, Serialize)]
            derive_attr: Regex::new(r#"#\[derive\s*\((.*?)\)\]"#).expect("valid regex"),

            // Matches struct/enum declarations
            type_decl: Regex::new(r"^\s*(?:pub\s+)?(?:struct|enum)\s+([A-Z][a-zA-Z0-9_]*)")
                .expect("valid regex"),
        }
    }

    /// Parse derive traits from the attribute string.
    fn parse_derives(&self, derives_str: &str) -> Vec<String> {
        derives_str
            .split(',')
            .map(|s| s.trim())
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string())
            .collect()
    }

    /// Determine confidence based on context.
    fn confidence_for_file(&self, file: &str) -> f32 {
        if file.contains("test") || file.contains("example") || file.contains("bench") {
            0.5 // Test/example types don't reflect production API patterns
        } else {
            1.0
        }
    }

    /// Infer type category from name or context.
    fn infer_type_category(&self, type_name: &str, derives: &[String]) -> &'static str {
        // Heuristics to categorize types
        if type_name.ends_with("Error") || type_name.ends_with("Exception") {
            "error"
        } else if type_name.ends_with("Config") || type_name.ends_with("Settings") {
            "config"
        } else if type_name.ends_with("Request")
            || type_name.ends_with("Response")
            || type_name.ends_with("Message")
            || type_name.ends_with("Event")
        {
            "message"
        } else if derives.iter().any(|d| d == "Serialize" || d == "Deserialize") {
            "data" // Serializable types
        } else {
            "type" // Generic
        }
    }
}

impl Extractor for DerivePatternExtractor {
    fn name(&self) -> &str {
        "derive_pattern"
    }

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

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        _language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();
        let confidence = self.confidence_for_file(file);

        let lines: Vec<&str> = content.lines().collect();

        for i in 0..lines.len() {
            let line = lines[i];

            // Look for #[derive(...)]
            if let Some(cap) = self.derive_attr.captures(line) {
                let derives_str = cap.get(1).map_or("", |m| m.as_str());
                let derives = self.parse_derives(derives_str);

                // Look ahead for the type declaration (within next 3 lines)
                let mut type_name = None;
                for line in lines.iter().skip(i + 1).take(3) {
                    if let Some(type_cap) = self.type_decl.captures(line) {
                        type_name = type_cap.get(1).map(|m| m.as_str().to_string());
                        break;
                    }
                }

                if let Some(name) = type_name {
                    let category = self.infer_type_category(&name, &derives);

                    // Create a concept path based on category
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push(category.to_string());
                    concept_path.push(name.to_lowercase());
                    concept_path.push("derives".to_string());

                    // Sort derives for consistency
                    let mut sorted_derives = derives.clone();
                    sorted_derives.sort();

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: "traits".to_string(),
                        value: ObjectValue::Text(sorted_derives.join(",")),
                        file: file.to_string(),
                        line: i + 1,
                        matched_text: line.trim().to_string(),
                        confidence,
                        description: format!("{} derives {}", name, sorted_derives.join(", ")),
                    });
                }
            }
        }

        claims
    }
}

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

    #[test]
    fn test_simple_derive() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Debug, Clone)]
pub struct Wallet {
    balance: u64,
}
        "#;

        let claims = extractor.extract(
            &["rust".to_string(), "maxwell".to_string()],
            content,
            Language::Rust,
            "src/wallet.rs",
        );

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("wallet"));
        assert!(claims[0].concept_path.contains("derives"));
        if let ObjectValue::Text(ref val) = claims[0].value {
            assert!(val.contains("Clone"));
            assert!(val.contains("Debug"));
        } else {
            panic!("Expected Text value");
        }
    }

    #[test]
    fn test_message_type_pattern() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BidMessage {
    amount: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AckMessage {
    id: String,
}
        "#;

        let claims = extractor.extract(
            &["rust".to_string(), "maxwell".to_string(), "vsock".to_string()],
            content,
            Language::Rust,
            "src/messages.rs",
        );

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

        // Both should be categorized as "message"
        assert!(claims.iter().all(|c| c.concept_path.contains("message")));

        // Both should have the same derives (sorted)
        if let ObjectValue::Text(ref val1) = claims[0].value {
            if let ObjectValue::Text(ref val2) = claims[1].value {
                assert_eq!(val1, val2); // Same pattern!
                assert!(val1.contains("Clone"));
                assert!(val1.contains("Debug"));
                assert!(val1.contains("Deserialize"));
                assert!(val1.contains("Serialize"));
            }
        }
    }

    #[test]
    fn test_error_type_categorization() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Debug, Display, Error)]
pub enum WalletError {
    InsufficientFunds,
}
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/error.rs");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("error"));
        assert!(claims[0].concept_path.contains("walleterror"));
    }

    #[test]
    fn test_config_type_categorization() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Deserialize, Debug, Clone)]
pub struct AppConfig {
    port: u16,
}
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/config.rs");

        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("config"));
    }

    #[test]
    fn test_multiline_struct() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Debug, Clone)]
pub struct Wallet {
    balance: u64,
    owner: String,
}
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/lib.rs");

        assert_eq!(claims.len(), 1);
    }

    #[test]
    fn test_confidence_in_test_file() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Debug, Clone)]
struct TestHelper {
    data: Vec<u8>,
}
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/wallet_test.rs");

        assert_eq!(claims.len(), 1);
        assert_eq!(claims[0].confidence, 0.5);
    }

    #[test]
    fn test_sorted_derives() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Foo {}
        "#;

        let claims =
            extractor.extract(&["rust".to_string()], content, Language::Rust, "src/lib.rs");

        assert_eq!(claims.len(), 1);
        // Should be alphabetically sorted
        if let ObjectValue::Text(ref val) = claims[0].value {
            assert_eq!(val, "Clone,Debug,Deserialize,Serialize");
        }
    }

    #[test]
    fn test_real_world_example() {
        let extractor = DerivePatternExtractor::new();
        let content = r#"
//! Message types for vsock communication

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BidMessage {
    pub amount: u64,
    pub timestamp: u64,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct AckMessage {
    pub id: String,
}
        "#;

        let claims = extractor.extract(
            &[
                "rust".to_string(),
                "maxwell".to_string(),
                "vsock".to_string(),
                "messages".to_string(),
            ],
            content,
            Language::Rust,
            "src/vsock/messages.rs",
        );

        assert_eq!(claims.len(), 2);
        // Both should have consistent derives
        assert!(claims.iter().all(|c| {
            if let ObjectValue::Text(ref v) = c.value {
                v.contains("Clone") && v.contains("Debug") && v.contains("Serialize")
            } else {
                false
            }
        }));
    }
}
