//! Extractor registry and collection logic.

use tracing::instrument;

use crate::config::AphoriaConfig;
use crate::types::{ExtractedClaim, Language};

use super::api_key_security::ApiKeySecurityExtractor;
use super::aspnet_security::AspNetSecurityExtractor;
use super::auth_bypass::AuthBypassExtractor;
use super::circuit_breaker_config::CircuitBreakerConfigExtractor;
use super::command_injection::CommandInjectionExtractor;
use super::config_security::ConfigSecurityExtractor;
use super::const_declarations::ConstDeclarationsExtractor;
use super::cors_config::CorsConfigExtractor;
use super::declarative::{DeclarativeExtractor, DeclarativeExtractorDef};
use super::dep_versions::DepVersionsExtractor;
use super::derive_pattern::DerivePatternExtractor;
use super::django_security::DjangoSecurityExtractor;
use super::durability_config::DurabilityConfigExtractor;
use super::express_security::ExpressSecurityExtractor;
use super::fastapi_security::FastApiSecurityExtractor;
use super::flask_security::FlaskSecurityExtractor;
use super::hardcoded_secrets::HardcodedSecretsExtractor;
use super::high_entropy::HighEntropySecretsExtractor;
use super::ignore_comments::IgnoreCommentParser;
use super::import_graph::ImportGraphExtractor;
use super::insecure_cookies::InsecureCookiesExtractor;
use super::insecure_deserialization::InsecureDeserializationExtractor;
use super::jwt_config::JwtConfigExtractor;
use super::laravel_security::LaravelSecurityExtractor;
use super::nestjs_security::NestJsSecurityExtractor;
use super::nextjs_security::NextJsSecurityExtractor;
use super::orm_injection::OrmInjectionExtractor;
use super::path_traversal::PathTraversalExtractor;
use super::rails_security::RailsSecurityExtractor;
use super::rate_limit::RateLimitExtractor;
use super::security_headers::SecurityHeadersExtractor;
use super::spring_security::SpringSecurityExtractor;
use super::sql_injection::SqlInjectionExtractor;
use super::ssrf::SsrfExtractor;
use super::timeout_config::{TimeoutConfigExtractor, TimeoutThresholds};
use super::tls_verify::TlsVerifyExtractor;
use super::tls_version::TlsVersionExtractor;
use super::traits::Extractor;
use super::unreal_config::UnrealConfigExtractor;
use super::unreal_cpp::UnrealCppExtractor;
use super::unreal_performance::UnrealPerformanceExtractor;
use super::unsafe_atomic::UnsafeAtomicExtractor;
use super::unvalidated_redirects::UnvalidatedRedirectsExtractor;
use super::weak_crypto::WeakCryptoExtractor;
use super::weak_password::WeakPasswordExtractor;
use super::xxe::XxeExtractor;

/// Registry of available extractors.
pub struct ExtractorRegistry {
    extractors: Vec<Box<dyn Extractor>>,
}

impl Default for ExtractorRegistry {
    fn default() -> Self {
        Self::new(&AphoriaConfig::default())
    }
}

impl ExtractorRegistry {
    /// Create a new registry with all built-in extractors.
    pub fn new(config: &AphoriaConfig) -> Self {
        let mut extractors: Vec<Box<dyn Extractor>> = Vec::new();

        // Build set of enabled extractors
        let enabled: std::collections::HashSet<&str> =
            config.extractors.enabled.iter().map(|s| s.as_str()).collect();
        let disabled: std::collections::HashSet<&str> =
            config.extractors.disabled.iter().map(|s| s.as_str()).collect();

        let is_enabled = |name: &str| -> bool {
            if !disabled.is_empty() {
                !disabled.contains(name)
            } else if !enabled.is_empty() {
                enabled.contains(name)
            } else {
                true
            }
        };

        // Register extractors based on configuration
        if is_enabled("tls_verify") {
            extractors.push(Box::new(TlsVerifyExtractor::new()));
        }
        if is_enabled("tls_version") {
            extractors.push(Box::new(TlsVersionExtractor::new()));
        }
        if is_enabled("jwt_config") {
            extractors.push(Box::new(JwtConfigExtractor::new()));
        }
        if is_enabled("hardcoded_secrets") {
            extractors.push(Box::new(HardcodedSecretsExtractor::new()));
        }
        if is_enabled("timeout_config") {
            let thresholds = TimeoutThresholds {
                min_reasonable_ms: config.extractors.timeout_config.min_reasonable_ms,
                max_reasonable_ms: config.extractors.timeout_config.max_reasonable_ms,
            };
            extractors.push(Box::new(TimeoutConfigExtractor::new(thresholds)));
        }
        if is_enabled("dep_versions") && config.extractors.dep_versions.enabled {
            extractors.push(Box::new(DepVersionsExtractor::new()));
        }
        if is_enabled("cors_config") {
            extractors.push(Box::new(CorsConfigExtractor::new()));
        }
        if is_enabled("durability_config") {
            extractors.push(Box::new(DurabilityConfigExtractor::new()));
        }
        if is_enabled("rate_limit") {
            extractors.push(Box::new(RateLimitExtractor::default()));
        }
        // Phase 2 extractors
        if is_enabled("weak_crypto") {
            extractors.push(Box::new(WeakCryptoExtractor::new()));
        }
        if is_enabled("sql_injection") {
            extractors.push(Box::new(SqlInjectionExtractor::new()));
        }
        if is_enabled("command_injection") {
            extractors.push(Box::new(CommandInjectionExtractor::new()));
        }
        // Unreal Engine extractors
        if is_enabled("unreal_cpp") {
            extractors.push(Box::new(UnrealCppExtractor::new()));
        }
        if is_enabled("unreal_config") {
            extractors.push(Box::new(UnrealConfigExtractor::new()));
        }
        if is_enabled("unreal_performance") {
            extractors.push(Box::new(UnrealPerformanceExtractor::new()));
        }
        // Phase 8: Enterprise extractors
        if is_enabled("high_entropy_secrets") {
            extractors.push(Box::new(HighEntropySecretsExtractor::new(&config.extractors.entropy)));
        }
        if is_enabled("auth_bypass") {
            extractors.push(Box::new(AuthBypassExtractor::new()));
        }
        if is_enabled("api_key_security") {
            extractors.push(Box::new(ApiKeySecurityExtractor::new()));
        }
        if is_enabled("circuit_breaker_config") {
            extractors.push(Box::new(CircuitBreakerConfigExtractor::new()));
        }
        if is_enabled("import_graph") {
            extractors.push(Box::new(ImportGraphExtractor::new()));
        }
        if is_enabled("derive_pattern") {
            extractors.push(Box::new(DerivePatternExtractor::new()));
        }
        if is_enabled("const_declarations") {
            extractors.push(Box::new(ConstDeclarationsExtractor::new()));
        }
        if is_enabled("unsafe_atomic") {
            extractors.push(Box::new(UnsafeAtomicExtractor::new()));
        }
        if is_enabled("insecure_cookies") {
            extractors.push(Box::new(InsecureCookiesExtractor::new()));
        }
        // Phase 8: Enterprise security extractors
        if is_enabled("path_traversal") {
            extractors.push(Box::new(PathTraversalExtractor::new()));
        }
        if is_enabled("unvalidated_redirects") {
            extractors.push(Box::new(UnvalidatedRedirectsExtractor::new()));
        }
        if is_enabled("weak_password") {
            extractors.push(Box::new(WeakPasswordExtractor::new()));
        }
        if is_enabled("security_headers") {
            extractors.push(Box::new(SecurityHeadersExtractor::new()));
        }
        if is_enabled("insecure_deserialization") {
            extractors.push(Box::new(InsecureDeserializationExtractor::new()));
        }
        if is_enabled("ssrf") {
            extractors.push(Box::new(SsrfExtractor::new()));
        }
        if is_enabled("orm_injection") {
            extractors.push(Box::new(OrmInjectionExtractor::new()));
        }
        if is_enabled("xxe") {
            extractors.push(Box::new(XxeExtractor::new()));
        }
        // Phase 8.3: Config file deep parsing
        if is_enabled("config_security") {
            extractors.push(Box::new(ConfigSecurityExtractor::new()));
        }

        // Phase 8.2: Framework-specific security extractors
        if is_enabled("django_security") {
            extractors.push(Box::new(DjangoSecurityExtractor::new()));
        }
        if is_enabled("express_security") {
            extractors.push(Box::new(ExpressSecurityExtractor::new()));
        }
        if is_enabled("flask_security") {
            extractors.push(Box::new(FlaskSecurityExtractor::new()));
        }
        if is_enabled("fastapi_security") {
            extractors.push(Box::new(FastApiSecurityExtractor::new()));
        }
        if is_enabled("nestjs_security") {
            extractors.push(Box::new(NestJsSecurityExtractor::new()));
        }
        if is_enabled("nextjs_security") {
            extractors.push(Box::new(NextJsSecurityExtractor::new()));
        }
        if is_enabled("spring_security") {
            extractors.push(Box::new(SpringSecurityExtractor::new()));
        }
        if is_enabled("laravel_security") {
            extractors.push(Box::new(LaravelSecurityExtractor::new()));
        }
        if is_enabled("rails_security") {
            extractors.push(Box::new(RailsSecurityExtractor::new()));
        }
        if is_enabled("aspnet_security") {
            extractors.push(Box::new(AspNetSecurityExtractor::new()));
        }

        // Register declarative extractors from config
        // Declarative extractors are always enabled unless explicitly disabled.
        // They don't need to be in the `enabled` list because they're user-defined.
        let is_declarative_enabled = |name: &str| -> bool { !disabled.contains(name) };

        for def in &config.extractors.declarative {
            match DeclarativeExtractor::try_new(def.clone()) {
                Ok(extractor) => {
                    if is_declarative_enabled(extractor.name()) {
                        extractors.push(Box::new(extractor));
                    }
                }
                Err(e) => {
                    tracing::warn!(
                        name = %def.name,
                        error = %e,
                        "Failed to compile declarative extractor"
                    );
                }
            }
        }

        Self { extractors }
    }

    /// Add declarative extractors from definitions.
    ///
    /// This is useful for adding extractors from Trust Packs at runtime.
    /// The extractors are added unconditionally (no enabled/disabled filtering).
    pub fn add_from_definitions(&mut self, defs: &[DeclarativeExtractorDef]) {
        for def in defs {
            match DeclarativeExtractor::try_new(def.clone()) {
                Ok(extractor) => {
                    self.extractors.push(Box::new(extractor));
                }
                Err(e) => {
                    tracing::warn!(
                        name = %def.name,
                        error = %e,
                        "Failed to compile declarative extractor from policy"
                    );
                }
            }
        }
    }

    /// Get extractors applicable to a given language.
    pub fn for_language(&self, language: Language) -> Vec<&dyn Extractor> {
        self.extractors
            .iter()
            .filter(|e| e.languages().contains(&language))
            .map(|e| e.as_ref())
            .collect()
    }

    /// Extract claims from content using all applicable extractors.
    ///
    /// This method also filters out claims on lines marked with `// aphoria:ignore`
    /// or similar inline ignore comments. See [`IgnoreCommentParser`] for details.
    #[instrument(skip(self, path_segments, content), fields(file = %file, language = ?language))]
    pub fn extract_all(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        // Parse inline ignore comments
        let ignore_parser = IgnoreCommentParser::parse(content);

        self.for_language(language)
            .iter()
            .flat_map(|e| e.extract(path_segments, content, language, file))
            .filter(|claim| !ignore_parser.is_ignored(claim.line))
            .collect()
    }

    /// Get the names of all registered extractors.
    pub fn extractor_names(&self) -> Vec<&str> {
        self.extractors.iter().map(|e| e.name()).collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extractors::declarative::{DeclarativeClaimDef, DeclarativeValue};

    /// Number of built-in extractors (not counting declarative).
    /// Phase 8.2 added 10 framework-specific extractors: 26 + 10 = 36
    /// dep_versions is now opt-in (disabled by default): 36 - 1 = 35
    /// durability_config added: 35 + 1 = 36
    /// api_key_security added: 36 + 1 = 37
    /// circuit_breaker_config added: 37 + 1 = 38
    /// import_graph added: 38 + 1 = 39
    /// derive_pattern added: 39 + 1 = 40
    /// const_declarations added: 40 + 1 = 41
    /// unsafe_atomic added: 41 + 1 = 42
    const BUILTIN_EXTRACTOR_COUNT: usize = 42;

    #[test]
    fn test_registry_creation() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        // Should have all 14 built-in extractors enabled by default
        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT);
    }

    #[test]
    fn test_registry_disabled_extractor() {
        let mut config = AphoriaConfig::default();
        config.extractors.disabled = vec!["tls_verify".to_string()];

        let registry = ExtractorRegistry::new(&config);

        assert!(!registry.extractor_names().contains(&"tls_verify"));
        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT - 1);
    }

    #[test]
    fn test_registry_for_language() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        let rust_extractors = registry.for_language(Language::Rust);
        // TLS, JWT, secrets, timeout, CORS, rate_limit work on Rust
        assert!(!rust_extractors.is_empty());

        let cargo_extractors = registry.for_language(Language::CargoManifest);
        // dep_versions is disabled by default (opt-in only)
        assert!(!cargo_extractors.iter().any(|e| e.name() == "dep_versions"));

        // Test with dep_versions explicitly enabled
        let mut config_with_deps = AphoriaConfig::default();
        config_with_deps.extractors.dep_versions.enabled = true;
        let registry_with_deps = ExtractorRegistry::new(&config_with_deps);
        let cargo_extractors_enabled = registry_with_deps.for_language(Language::CargoManifest);
        assert!(cargo_extractors_enabled.iter().any(|e| e.name() == "dep_versions"));
    }

    #[test]
    fn test_registry_for_unreal() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        let cpp_extractors = registry.for_language(Language::Cpp);
        assert!(cpp_extractors.iter().any(|e| e.name() == "unreal_cpp"));
        assert!(cpp_extractors.iter().any(|e| e.name() == "unreal_performance"));

        let ini_extractors = registry.for_language(Language::Ini);
        assert!(ini_extractors.iter().any(|e| e.name() == "unreal_config"));
    }

    #[test]
    fn test_extract_all() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        let content = r#"
            let client = reqwest::Client::builder()
                .danger_accept_invalid_certs(true)
                .build()?;
        "#;

        let claims =
            registry.extract_all(&["rust".to_string()], content, Language::Rust, "src/client.rs");

        assert!(!claims.is_empty());
        assert!(claims.iter().any(|c| c.concept_path.contains("tls")));
    }

    #[test]
    fn test_registry_with_declarative_extractor() {
        let mut config = AphoriaConfig::default();
        config.extractors.declarative.push(DeclarativeExtractorDef {
            name: "todo_finder".to_string(),
            description: "Finds TODO comments".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"TODO:".to_string(),
            claim: DeclarativeClaimDef {
                subject: "code_quality/todo".to_string(),
                predicate: "has_todo".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 0.8,
            source: None,
        });

        let registry = ExtractorRegistry::new(&config);

        // Should have 14 built-in + 1 declarative
        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT + 1);
        assert!(registry.extractor_names().contains(&"todo_finder"));
    }

    #[test]
    fn test_registry_declarative_extractor_disabled() {
        let mut config = AphoriaConfig::default();
        config.extractors.declarative.push(DeclarativeExtractorDef {
            name: "my_custom".to_string(),
            description: "Custom extractor".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"pattern".to_string(),
            claim: DeclarativeClaimDef {
                subject: "test".to_string(),
                predicate: "test".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 1.0,
            source: None,
        });
        // Disable the declarative extractor
        config.extractors.disabled.push("my_custom".to_string());

        let registry = ExtractorRegistry::new(&config);

        // Should have 14 - 1 (my_custom is disabled)
        // But "my_custom" is declarative and disabled, so it won't be added
        assert!(!registry.extractor_names().contains(&"my_custom"));
    }

    #[test]
    fn test_registry_invalid_declarative_extractor_logged_but_continues() {
        let mut config = AphoriaConfig::default();
        // Invalid: empty name
        config.extractors.declarative.push(DeclarativeExtractorDef {
            name: "".to_string(),
            description: "Invalid".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"pattern".to_string(),
            claim: DeclarativeClaimDef {
                subject: "test".to_string(),
                predicate: "test".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 1.0,
            source: None,
        });
        // Valid one after invalid
        config.extractors.declarative.push(DeclarativeExtractorDef {
            name: "valid_extractor".to_string(),
            description: "Valid".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"pattern".to_string(),
            claim: DeclarativeClaimDef {
                subject: "test".to_string(),
                predicate: "test".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 1.0,
            source: None,
        });

        let registry = ExtractorRegistry::new(&config);

        // Invalid one should be skipped, valid one should be registered
        assert!(registry.extractor_names().contains(&"valid_extractor"));
        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT + 1);
    }

    #[test]
    fn test_declarative_extractor_extracts_claims() {
        let mut config = AphoriaConfig::default();
        config.extractors.declarative.push(DeclarativeExtractorDef {
            name: "unwrap_detector".to_string(),
            description: "Detects .unwrap() calls".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"\.unwrap\(\)".to_string(),
            claim: DeclarativeClaimDef {
                subject: "error_handling/unwrap".to_string(),
                predicate: "unwrap_used".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 0.9,
            source: None,
        });

        let registry = ExtractorRegistry::new(&config);

        let content = r#"
            fn main() {
                let x = foo().unwrap();
            }
        "#;

        let claims =
            registry.extract_all(&["rust".to_string()], content, Language::Rust, "src/main.rs");

        // Should find the unwrap() call
        let unwrap_claims: Vec<_> =
            claims.iter().filter(|c| c.concept_path.contains("error_handling/unwrap")).collect();
        assert_eq!(unwrap_claims.len(), 1);
        assert_eq!(unwrap_claims[0].predicate, "unwrap_used");
    }

    #[test]
    fn test_add_from_definitions() {
        let config = AphoriaConfig::default();
        let mut registry = ExtractorRegistry::new(&config);

        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT);

        let defs = vec![DeclarativeExtractorDef {
            name: "runtime_added".to_string(),
            description: "Added at runtime".to_string(),
            languages: vec!["rust".to_string()],
            pattern: r"pattern".to_string(),
            claim: DeclarativeClaimDef {
                subject: "test".to_string(),
                predicate: "test".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 1.0,
            source: None,
        }];

        registry.add_from_definitions(&defs);

        assert_eq!(registry.extractor_names().len(), BUILTIN_EXTRACTOR_COUNT + 1);
        assert!(registry.extractor_names().contains(&"runtime_added"));
    }

    #[test]
    fn test_extract_all_respects_inline_ignore() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        // Same finding twice, but one is ignored
        let content = r#"
            let client = reqwest::Client::builder()
                .danger_accept_invalid_certs(true) // aphoria:ignore - Test only
                .build()?;

            let client2 = reqwest::Client::builder()
                .danger_accept_invalid_certs(true)
                .build()?;
        "#;

        let claims =
            registry.extract_all(&["rust".to_string()], content, Language::Rust, "src/client.rs");

        // Should only find one TLS claim (the non-ignored one)
        let tls_claims: Vec<_> = claims.iter().filter(|c| c.concept_path.contains("tls")).collect();
        assert_eq!(tls_claims.len(), 1, "Should have exactly 1 TLS claim (ignored one filtered)");
        assert_eq!(tls_claims[0].line, 7, "The claim should be from line 7");
    }

    #[test]
    fn test_extract_all_respects_ignore_next_line() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        let content = r#"
            // aphoria:ignore-next-line - Intentional for testing
            .danger_accept_invalid_certs(true)
            .danger_accept_invalid_certs(true)
        "#;

        let claims =
            registry.extract_all(&["rust".to_string()], content, Language::Rust, "src/client.rs");

        // Should only find one TLS claim (line 4, not line 3)
        let tls_claims: Vec<_> = claims.iter().filter(|c| c.concept_path.contains("tls")).collect();
        assert_eq!(tls_claims.len(), 1);
        assert_eq!(tls_claims[0].line, 4, "The claim should be from line 4");
    }

    #[test]
    fn test_extract_all_respects_ignore_block() {
        let config = AphoriaConfig::default();
        let registry = ExtractorRegistry::new(&config);

        let content = r#"
            // aphoria:ignore-block - Test fixtures
            .danger_accept_invalid_certs(true)
            let password = "test123";
            // aphoria:end-ignore
            .danger_accept_invalid_certs(true)
        "#;

        let claims =
            registry.extract_all(&["rust".to_string()], content, Language::Rust, "src/client.rs");

        // Should only find claims from line 6 (after block)
        // Lines 3 and 4 are in the block
        let tls_claims: Vec<_> = claims.iter().filter(|c| c.concept_path.contains("tls")).collect();
        assert_eq!(tls_claims.len(), 1);
        assert_eq!(tls_claims[0].line, 6, "The claim should be from line 6");
    }
}
