//! Shadow executor for running shadow extractors during scans.
//!
//! The executor runs shadow extractors alongside production extractors
//! and stores matches separately for review.

use std::path::Path;

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

use super::registry::ShadowExtractorRegistry;
use super::types::ShadowMatch;
use crate::AphoriaError;

/// Result of running shadow extractors on a file.
#[derive(Debug, Default)]
pub struct ShadowExecutionResult {
    /// Number of shadow extractors run.
    pub extractors_run: usize,
    /// Total matches found.
    pub matches_found: usize,
    /// Errors encountered.
    pub errors: Vec<String>,
    /// Number of metrics update failures.
    ///
    /// When non-zero, indicates the registry failed to persist test metrics.
    /// This could indicate storage issues (disk full, permissions) that should
    /// be investigated if the count is consistently high.
    pub metrics_update_failures: usize,
}

/// Executor for running shadow extractors during scans.
///
/// Shadow extractors run alongside production extractors but their
/// matches are stored separately and don't appear in the main output.
pub struct ShadowExecutor<'a> {
    registry: &'a ShadowExtractorRegistry,
}

impl<'a> ShadowExecutor<'a> {
    /// Create a new shadow executor.
    pub fn new(registry: &'a ShadowExtractorRegistry) -> Self {
        Self { registry }
    }

    /// Run all active shadow extractors on a file.
    ///
    /// Matches are stored in the shadow store for later review.
    /// This does NOT add matches to the main scan output.
    pub fn run_on_file(
        &self,
        file_path: &Path,
        content: &str,
        language: &str,
    ) -> Result<ShadowExecutionResult, AphoriaError> {
        let mut result = ShadowExecutionResult::default();

        // Get active shadow extractors
        let entries = self.registry.get_active_extractors()?;

        for entry in &entries {
            // Check if extractor applies to this language
            if !entry.extractor.languages.iter().any(|l| l.eq_ignore_ascii_case(language)) {
                continue;
            }

            result.extractors_run += 1;

            // Compile regex
            let regex = match Regex::new(&entry.extractor.pattern) {
                Ok(r) => r,
                Err(e) => {
                    result.errors.push(format!(
                        "Failed to compile regex for {}: {}",
                        entry.extractor.name, e
                    ));
                    continue;
                }
            };

            // Find matches
            for (line_idx, line) in content.lines().enumerate() {
                if let Some(m) = regex.find(line) {
                    // Get context (3 lines before and after)
                    let context = self.get_context(content, line_idx, 3);

                    let shadow_match = ShadowMatch::new(
                        entry.test.id,
                        file_path.to_path_buf(),
                        line_idx + 1, // 1-based line number
                        m.as_str().to_string(),
                        context,
                    );

                    // Save match
                    if let Err(e) = self.registry.store().save_match(&shadow_match) {
                        result.errors.push(format!(
                            "Failed to save match for {}: {}",
                            entry.extractor.name, e
                        ));
                        continue;
                    }

                    // Update test metrics
                    let mut test = entry.test.clone();
                    test.record_match();
                    if let Err(e) = self.registry.update_test(&test) {
                        warn!(
                            error = %e,
                            extractor = %entry.extractor.name,
                            test_id = %entry.test.id,
                            "Failed to update test metrics"
                        );
                        result.metrics_update_failures += 1;
                    }

                    result.matches_found += 1;
                    debug!(
                        extractor = %entry.extractor.name,
                        file = %file_path.display(),
                        line = line_idx + 1,
                        "Shadow match found"
                    );
                }
            }
        }

        // Record scan for all active tests
        self.record_scan_for_all()?;

        Ok(result)
    }

    /// Record a scan for all active shadow tests.
    ///
    /// Called once per file to update scan counts.
    pub fn record_scan_for_all(&self) -> Result<(), AphoriaError> {
        let tests = self.registry.store().list_active_tests()?;

        for mut test in tests {
            test.record_scan();
            self.registry.update_test(&test)?;
        }

        Ok(())
    }

    /// Get context around a line.
    fn get_context(&self, content: &str, line_idx: usize, context_lines: usize) -> String {
        let lines: Vec<&str> = content.lines().collect();
        let start = line_idx.saturating_sub(context_lines);
        let end = (line_idx + context_lines + 1).min(lines.len());

        lines[start..end].join("\n")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ShadowConfig;
    use crate::extractors::{DeclarativeClaimDef, DeclarativeExtractorDef, DeclarativeValue};
    use std::fs;
    use tempfile::TempDir;
    use uuid::Uuid;

    fn create_test_extractor() -> DeclarativeExtractorDef {
        DeclarativeExtractorDef {
            name: "test_shadow_extractor".to_string(),
            description: "Test shadow extractor".to_string(),
            languages: vec!["python".to_string()],
            pattern: r"verify_ssl\s*=\s*(true|false)".to_string(),
            claim: DeclarativeClaimDef {
                subject: "ssl/verify".to_string(),
                predicate: "enabled".to_string(),
                value: DeclarativeValue::MatchedText { value_from_match: true },
            },
            confidence: 0.9,
            source: None,
        }
    }

    fn setup_registry_with_extractor() -> (ShadowExtractorRegistry, TempDir, TempDir) {
        let shadow_temp = TempDir::new().expect("shadow temp dir");
        let learned_temp = TempDir::new().expect("learned temp dir");

        let config = ShadowConfig {
            shadow_dir: Some(shadow_temp.path().to_path_buf()),
            ..Default::default()
        };

        let registry =
            ShadowExtractorRegistry::new(&config, learned_temp.path()).expect("create registry");

        // Create and save extractor YAML
        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("test.yaml");
        let yaml = r#"name: test_shadow_extractor
description: Test shadow extractor
languages:
  - python
pattern: "verify_ssl\\s*=\\s*(true|false)"
claim:
  subject: ssl/verify
  predicate: enabled
  value_from_match: true
confidence: 0.9
"#;
        fs::write(&extractor_path, yaml).expect("write yaml");

        // Register shadow test
        registry
            .register_shadow_test(&extractor, Uuid::new_v4(), extractor_path)
            .expect("register");

        (registry, shadow_temp, learned_temp)
    }

    #[test]
    fn test_run_on_file_with_matches() {
        let (registry, _shadow_temp, _learned_temp) = setup_registry_with_extractor();
        let executor = ShadowExecutor::new(&registry);

        let content = r#"
import requests

def connect():
    verify_ssl = false  # Should match
    response = requests.get(url, verify=verify_ssl)
    return response
"#;

        let file_path = Path::new("/test/file.py");
        let result = executor.run_on_file(file_path, content, "python").expect("run");

        assert_eq!(result.extractors_run, 1);
        assert_eq!(result.matches_found, 1);
        assert!(result.errors.is_empty());
    }

    #[test]
    fn test_run_on_file_no_matches() {
        let (registry, _shadow_temp, _learned_temp) = setup_registry_with_extractor();
        let executor = ShadowExecutor::new(&registry);

        let content = r#"
import requests

def connect():
    response = requests.get(url, verify=True)
    return response
"#;

        let file_path = Path::new("/test/file.py");
        let result = executor.run_on_file(file_path, content, "python").expect("run");

        assert_eq!(result.extractors_run, 1);
        assert_eq!(result.matches_found, 0);
    }

    #[test]
    fn test_run_on_file_wrong_language() {
        let (registry, _shadow_temp, _learned_temp) = setup_registry_with_extractor();
        let executor = ShadowExecutor::new(&registry);

        let content = r#"verify_ssl = false"#;

        let file_path = Path::new("/test/file.rs");
        let result = executor.run_on_file(file_path, content, "rust").expect("run");

        assert_eq!(result.extractors_run, 0); // Extractor doesn't apply to Rust
        assert_eq!(result.matches_found, 0);
    }

    #[test]
    fn test_get_context() {
        let (registry, _shadow_temp, _learned_temp) = setup_registry_with_extractor();
        let executor = ShadowExecutor::new(&registry);

        let content = "line1\nline2\nline3\nline4\nline5\nline6\nline7";

        // Context around line 3 (index 2)
        let context = executor.get_context(content, 2, 1);
        assert_eq!(context, "line2\nline3\nline4");

        // Context at start
        let context = executor.get_context(content, 0, 2);
        assert_eq!(context, "line1\nline2\nline3");

        // Context at end
        let context = executor.get_context(content, 6, 2);
        assert_eq!(context, "line5\nline6\nline7");
    }
}
