//! Extractor validation for promotion candidates.
//!
//! Validates generated regex patterns against stored examples,
//! checks for ReDoS vulnerabilities, and measures performance.

use std::time::Instant;

use regex::{Regex, RegexBuilder};
use tracing::{debug, warn};

use super::types::ValidationResult;
use crate::extractors::DeclarativeExtractorDef;
use crate::learning::LearnedPattern;
use crate::AphoriaError;

/// Validates generated extractors against examples.
pub struct ExtractorValidator {
    /// Maximum time to compile a regex (milliseconds).
    max_compile_time_ms: u64,

    /// Maximum average match time (microseconds).
    max_match_time_us: u64,

    /// Regex size limit for ReDoS protection.
    regex_size_limit: usize,
}

impl Default for ExtractorValidator {
    fn default() -> Self {
        Self {
            max_compile_time_ms: 100,
            max_match_time_us: 1000,
            regex_size_limit: 10_000_000, // 10MB
        }
    }
}

impl ExtractorValidator {
    /// Create a new validator with custom limits.
    pub fn new(max_compile_time_ms: u64, max_match_time_us: u64, regex_size_limit: usize) -> Self {
        Self { max_compile_time_ms, max_match_time_us, regex_size_limit }
    }

    /// Validate an extractor definition against a learned pattern.
    ///
    /// Returns a `ValidationResult` indicating whether the extractor is
    /// ready for promotion.
    pub fn validate(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
    ) -> Result<ValidationResult, AphoriaError> {
        let mut result = ValidationResult::default();

        // 1. Check for ReDoS patterns before compilation
        if let Some(warning) = Self::detect_redos_pattern(&extractor.pattern) {
            result.warnings.push(warning.clone());
            return Ok(ValidationResult::failure(vec![], vec![warning]));
        }

        // 2. Compile regex with size limits and measure time
        let compile_start = Instant::now();
        let compiled = RegexBuilder::new(&extractor.pattern)
            .size_limit(self.regex_size_limit)
            .dfa_size_limit(self.regex_size_limit)
            .build()
            .map_err(|e| AphoriaError::Promotion(format!("Invalid regex: {}", e)))?;

        result.compile_time_ms = compile_start.elapsed().as_millis() as u64;

        // Check compile time
        if result.compile_time_ms > self.max_compile_time_ms {
            result.mark_slow(format!(
                "Compile time {}ms exceeds limit {}ms",
                result.compile_time_ms, self.max_compile_time_ms
            ));
        }

        // 3. Test against stored example
        let match_start = Instant::now();
        let matched = compiled.is_match(&pattern.example_code);
        let match_time_us = match_start.elapsed().as_micros() as u64;

        result.avg_match_time_us = match_time_us;

        // Check match time
        if match_time_us > self.max_match_time_us {
            result.mark_slow(format!(
                "Match time {}us exceeds limit {}us",
                match_time_us, self.max_match_time_us
            ));
        }

        // 4. Record match results
        if matched {
            result.positive_matches.push(pattern.example_code.clone());
            result.passed = true;
            result.performance_ok = result.compile_time_ms <= self.max_compile_time_ms
                && match_time_us <= self.max_match_time_us;

            debug!(
                pattern_id = %pattern.id,
                compile_ms = result.compile_time_ms,
                match_us = match_time_us,
                "Validation passed"
            );
        } else {
            result.positive_failures.push(pattern.example_code.clone());
            result.passed = false;
            result.add_warning(format!(
                "Regex did not match example: {}",
                truncate_string(&pattern.example_code, 50)
            ));

            warn!(
                pattern_id = %pattern.id,
                regex = %extractor.pattern,
                example = %truncate_string(&pattern.example_code, 100),
                "Validation failed: regex did not match example"
            );
        }

        Ok(result)
    }

    /// Detect potentially dangerous ReDoS patterns.
    ///
    /// Returns a warning message if a dangerous pattern is detected.
    fn detect_redos_pattern(pattern: &str) -> Option<String> {
        // Check for nested quantifiers which can cause catastrophic backtracking
        let dangerous_patterns = [
            // (a+)+, (a*)+, (a+)*, (a*)*
            (r"\([^)]*[+*]\)[+*]", "Nested quantifiers detected (e.g., (a+)+)"),
            // (.+)+, (.*)+
            (r"\(\.\*?\)[+*]", "Nested dot quantifier detected (e.g., (.+)+)"),
            // Overlapping alternation with quantifiers
            (r"\([^)]*\|[^)]*\)\{", "Quantified alternation with repetition"),
        ];

        for (check_pattern, message) in dangerous_patterns {
            if let Ok(re) = Regex::new(check_pattern) {
                if re.is_match(pattern) {
                    return Some(format!("Potential ReDoS vulnerability: {}", message));
                }
            }
        }

        None
    }

    /// Validate multiple patterns and return aggregate results.
    pub fn validate_batch(
        &self,
        candidates: &[(DeclarativeExtractorDef, LearnedPattern)],
    ) -> Vec<(usize, ValidationResult)> {
        candidates
            .iter()
            .enumerate()
            .map(|(idx, (extractor, pattern))| match self.validate(extractor, pattern) {
                Ok(result) => (idx, result),
                Err(e) => {
                    warn!(error = %e, "Validation error for candidate {}", idx);
                    (idx, ValidationResult::failure(vec![], vec![format!("Error: {}", e)]))
                }
            })
            .collect()
    }
}

/// Truncate a string for display.
fn truncate_string(s: &str, max_len: usize) -> String {
    if s.len() <= max_len {
        s.to_string()
    } else {
        format!("{}...", &s[..max_len.saturating_sub(3)])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extractors::{DeclarativeClaimDef, DeclarativeValue};
    use crate::learning::{ClaimTemplate, ValueType};
    use crate::types::Language;

    fn create_test_pattern(example: &str, normalized: &str) -> LearnedPattern {
        LearnedPattern::new(
            example,
            normalized,
            ClaimTemplate::new("test/subject", "predicate", ValueType::Text, "description"),
            Language::Rust,
            "project1",
            0.9,
        )
    }

    fn create_test_extractor(name: &str, pattern: &str) -> DeclarativeExtractorDef {
        DeclarativeExtractorDef {
            name: name.to_string(),
            description: "Test extractor".to_string(),
            languages: vec!["rust".to_string()],
            pattern: pattern.to_string(),
            claim: DeclarativeClaimDef {
                subject: "test/subject".to_string(),
                predicate: "test".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 0.9,
            source: None,
        }
    }

    #[test]
    fn test_validator_default() {
        let validator = ExtractorValidator::default();
        assert_eq!(validator.max_compile_time_ms, 100);
        assert_eq!(validator.max_match_time_us, 1000);
    }

    #[test]
    fn test_validate_matching_pattern() {
        let validator = ExtractorValidator::default();
        let pattern = create_test_pattern("verify_ssl = false", "verify_ssl = <boolean>");
        let extractor = create_test_extractor("test", r"verify_ssl\s*=\s*\w+");

        let result = validator.validate(&extractor, &pattern).expect("validation");

        assert!(result.passed);
        assert!(result.performance_ok);
        assert_eq!(result.positive_matches.len(), 1);
        assert!(result.positive_failures.is_empty());
    }

    #[test]
    fn test_validate_non_matching_pattern() {
        let validator = ExtractorValidator::default();
        let pattern = create_test_pattern("verify_ssl = false", "verify_ssl = <boolean>");
        let extractor = create_test_extractor("test", r"something_completely_different");

        let result = validator.validate(&extractor, &pattern).expect("validation");

        assert!(!result.passed);
        assert_eq!(result.positive_failures.len(), 1);
        assert!(!result.warnings.is_empty());
    }

    #[test]
    fn test_validate_invalid_regex() {
        let validator = ExtractorValidator::default();
        let pattern = create_test_pattern("test", "test");
        let extractor = create_test_extractor("test", r"[invalid(");

        let result = validator.validate(&extractor, &pattern);
        assert!(result.is_err());
    }

    #[test]
    fn test_detect_redos_nested_quantifier() {
        let warning = ExtractorValidator::detect_redos_pattern(r"(a+)+");
        assert!(warning.is_some());
        assert!(warning.as_ref().map(|w| w.contains("ReDoS")).unwrap_or(false));

        let warning = ExtractorValidator::detect_redos_pattern(r"(.*)*");
        assert!(warning.is_some());
    }

    #[test]
    fn test_detect_redos_safe_pattern() {
        let warning = ExtractorValidator::detect_redos_pattern(r"verify_ssl\s*=\s*\w+");
        assert!(warning.is_none());

        let warning = ExtractorValidator::detect_redos_pattern(r"(?i)tls_min_version");
        assert!(warning.is_none());
    }

    #[test]
    fn test_validate_batch() {
        let validator = ExtractorValidator::default();

        let candidates = vec![
            (
                create_test_extractor("test1", r"pattern1"),
                create_test_pattern("pattern1 here", "pattern1"),
            ),
            (
                create_test_extractor("test2", r"pattern2"),
                create_test_pattern("different content", "pattern2"),
            ),
        ];

        let results = validator.validate_batch(&candidates);

        assert_eq!(results.len(), 2);
        // First should pass (pattern matches)
        assert!(results[0].1.passed);
        // Second should fail (pattern doesn't match)
        assert!(!results[1].1.passed);
    }

    #[test]
    fn test_truncate_string() {
        assert_eq!(truncate_string("short", 10), "short");
        assert_eq!(truncate_string("this is a longer string", 10), "this is...");
        assert_eq!(truncate_string("exactly10!", 10), "exactly10!");
    }
}
