//! Parser and validator for declarative extractor definitions.

use regex::{Regex, RegexBuilder};

use super::types::DeclarativeExtractorDef;
use crate::types::Language;
use crate::AphoriaError;

/// Compiled declarative extractor ready for use.
///
/// This struct holds the validated and compiled form of a `DeclarativeExtractorDef`,
/// including the pre-compiled regex and parsed language list.
pub struct DeclarativeExtractor {
    pub(super) def: DeclarativeExtractorDef,
    pub(super) compiled_pattern: Regex,
    pub(super) languages: Vec<Language>,
}

impl std::fmt::Debug for DeclarativeExtractor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DeclarativeExtractor")
            .field("name", &self.def.name)
            .field("pattern", &self.def.pattern)
            .field("languages", &self.languages)
            .finish()
    }
}

impl DeclarativeExtractor {
    /// Validate and compile a declarative extractor definition.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The name is empty
    /// - The claim subject is empty
    /// - The claim predicate is empty
    /// - The confidence is outside 0.0-1.0
    /// - The regex pattern is invalid
    /// - Any language string is unrecognized
    /// - No languages are specified
    pub fn try_new(def: DeclarativeExtractorDef) -> Result<Self, AphoriaError> {
        // Validate name
        if def.name.is_empty() {
            return Err(AphoriaError::DeclarativeExtractor {
                name: "(empty)".into(),
                message: "name cannot be empty".into(),
            });
        }

        // Validate claim subject
        if def.claim.subject.is_empty() {
            return Err(AphoriaError::DeclarativeExtractor {
                name: def.name.clone(),
                message: "claim.subject cannot be empty".into(),
            });
        }

        // Validate claim predicate
        if def.claim.predicate.is_empty() {
            return Err(AphoriaError::DeclarativeExtractor {
                name: def.name.clone(),
                message: "claim.predicate cannot be empty".into(),
            });
        }

        // Validate confidence
        if !(0.0..=1.0).contains(&def.confidence) {
            return Err(AphoriaError::DeclarativeExtractor {
                name: def.name.clone(),
                message: format!("confidence {} out of range 0.0-1.0", def.confidence),
            });
        }

        // Compile regex with size limits to prevent ReDoS attacks.
        // These limits bound the memory used by the regex engine's DFA cache,
        // preventing pathological patterns from consuming excessive resources.
        const REGEX_SIZE_LIMIT: usize = 10_000_000; // 10MB compiled size limit
        const REGEX_DFA_SIZE_LIMIT: usize = 10_000_000; // 10MB DFA cache limit

        let compiled_pattern = RegexBuilder::new(&def.pattern)
            .size_limit(REGEX_SIZE_LIMIT)
            .dfa_size_limit(REGEX_DFA_SIZE_LIMIT)
            .build()
            .map_err(|e| AphoriaError::DeclarativeExtractor {
                name: def.name.clone(),
                message: format!("invalid regex: {}", e),
            })?;

        // Parse languages
        let mut languages = Vec::with_capacity(def.languages.len());
        for lang_str in &def.languages {
            let lang = Language::from_str(lang_str).map_err(|unknown| {
                AphoriaError::DeclarativeExtractor {
                    name: def.name.clone(),
                    message: format!("unknown language: {}", unknown),
                }
            })?;
            languages.push(lang);
        }

        if languages.is_empty() {
            return Err(AphoriaError::DeclarativeExtractor {
                name: def.name.clone(),
                message: "at least one language required".into(),
            });
        }

        Ok(Self { def, compiled_pattern, languages })
    }

    /// Get the extractor name.
    pub fn name(&self) -> &str {
        &self.def.name
    }

    /// Get the languages this extractor applies to.
    pub fn languages(&self) -> &[Language] {
        &self.languages
    }

    /// Get access to the compiled regex pattern.
    pub(super) fn pattern(&self) -> &Regex {
        &self.compiled_pattern
    }

    /// Get access to the definition.
    pub(super) fn def(&self) -> &DeclarativeExtractorDef {
        &self.def
    }
}

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

    fn make_def(name: &str, pattern: &str, languages: Vec<&str>) -> DeclarativeExtractorDef {
        DeclarativeExtractorDef {
            name: name.to_string(),
            description: "Test extractor".to_string(),
            languages: languages.into_iter().map(String::from).collect(),
            pattern: pattern.to_string(),
            claim: DeclarativeClaimDef {
                subject: "test/subject".to_string(),
                predicate: "test_predicate".to_string(),
                value: DeclarativeValue::Boolean { value: true },
            },
            confidence: 0.9,
            source: None,
        }
    }

    #[test]
    fn test_valid_extractor_creation() {
        let def = make_def("test_extractor", r"unwrap\(\)", vec!["rust"]);
        let extractor = DeclarativeExtractor::try_new(def);
        assert!(extractor.is_ok());
    }

    #[test]
    fn test_empty_name_error() {
        let def = make_def("", r"pattern", vec!["rust"]);
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.to_string().contains("name cannot be empty"));
    }

    #[test]
    fn test_invalid_confidence_too_high() {
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.confidence = 1.5;
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("out of range"));
    }

    #[test]
    fn test_invalid_confidence_negative() {
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.confidence = -0.1;
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_regex() {
        let def = make_def("test", r"[invalid(", vec!["rust"]);
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("invalid regex"));
    }

    #[test]
    fn test_unknown_language() {
        let def = make_def("test", r"pattern", vec!["cobol"]);
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("unknown language"));
    }

    #[test]
    fn test_no_languages_error() {
        let def = make_def("test", r"pattern", vec![]);
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("at least one language"));
    }

    #[test]
    fn test_empty_subject_error() {
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.claim.subject = String::new();
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("claim.subject cannot be empty"));
    }

    #[test]
    fn test_empty_predicate_error() {
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.claim.predicate = String::new();
        let result = DeclarativeExtractor::try_new(def);
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("claim.predicate cannot be empty"));
    }

    #[test]
    fn test_boundary_confidence_values() {
        // Test 0.0 is valid
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.confidence = 0.0;
        assert!(DeclarativeExtractor::try_new(def).is_ok());

        // Test 1.0 is valid
        let mut def = make_def("test", r"pattern", vec!["rust"]);
        def.confidence = 1.0;
        assert!(DeclarativeExtractor::try_new(def).is_ok());
    }

    #[test]
    fn test_languages_method() {
        let def = make_def("test", r"pattern", vec!["rust", "go", "python"]);
        let extractor = DeclarativeExtractor::try_new(def).expect("valid extractor");

        let languages = extractor.languages();
        assert_eq!(languages.len(), 3);
        assert!(languages.contains(&Language::Rust));
        assert!(languages.contains(&Language::Go));
        assert!(languages.contains(&Language::Python));
    }

    #[test]
    fn test_name_method() {
        let def = make_def("my_custom_extractor", r"pattern", vec!["rust"]);
        let extractor = DeclarativeExtractor::try_new(def).expect("valid extractor");

        assert_eq!(extractor.name(), "my_custom_extractor");
    }
}
