//! Shadow extractor registry.
//!
//! Loads shadow extractors from the learned/ directory and pairs
//! them with their ShadowTest state.

use std::path::{Path, PathBuf};

use tracing::{debug, info, warn};
use uuid::Uuid;

use super::store::ShadowStore;
use super::types::{ShadowDecision, ShadowStatus, ShadowTest};
use crate::config::ShadowConfig;
use crate::extractors::DeclarativeExtractorDef;
use crate::AphoriaError;

/// Entry for a shadow extractor with its test state.
pub struct ShadowExtractorEntry {
    /// The shadow test state.
    pub test: ShadowTest,
    /// The extractor definition.
    pub extractor: DeclarativeExtractorDef,
}

/// Registry for shadow extractors.
///
/// Loads shadow extractors from the learned/ directory and tracks
/// their test state in the shadow store.
pub struct ShadowExtractorRegistry {
    /// Shadow store for persistence.
    store: ShadowStore,
    /// Configuration.
    config: ShadowConfig,
    /// Path to the learned extractors directory.
    #[allow(dead_code)]
    learned_dir: PathBuf,
}

impl ShadowExtractorRegistry {
    /// Create a new shadow extractor registry.
    pub fn new(config: &ShadowConfig, learned_dir: impl AsRef<Path>) -> Result<Self, AphoriaError> {
        let store = ShadowStore::new(config.get_shadow_dir())?;
        Ok(Self { store, config: config.clone(), learned_dir: learned_dir.as_ref().to_path_buf() })
    }

    /// Register a new shadow test for an auto-promoted extractor.
    ///
    /// Called when an extractor is auto-promoted. Creates a new
    /// ShadowTest and logs the decision.
    pub fn register_shadow_test(
        &self,
        extractor: &DeclarativeExtractorDef,
        source_pattern_id: Uuid,
        extractor_path: PathBuf,
    ) -> Result<ShadowTest, AphoriaError> {
        // Check if test already exists
        if let Some(existing) = self.store.get_test_by_name(&extractor.name)? {
            if existing.status == ShadowStatus::Active {
                debug!(name = %extractor.name, "Shadow test already exists and is active");
                return Ok(existing);
            }
        }

        // Create new shadow test
        let test = ShadowTest::new(extractor.name.clone(), extractor_path, source_pattern_id);

        // Save and log decision
        self.store.save_test(&test)?;
        self.store.log_decision(&ShadowDecision::entered_shadow(&test))?;

        info!(
            test_id = %test.id,
            extractor = %extractor.name,
            "Registered shadow test for auto-promoted extractor"
        );

        Ok(test)
    }

    /// Get all active shadow extractors.
    ///
    /// Loads extractor definitions from YAML files and pairs them
    /// with their ShadowTest state.
    pub fn get_active_extractors(&self) -> Result<Vec<ShadowExtractorEntry>, AphoriaError> {
        let tests = self.store.list_active_tests()?;
        let mut entries = Vec::new();

        for test in tests {
            // Load extractor from YAML
            match self.load_extractor(&test.extractor_path) {
                Ok(extractor) => {
                    entries.push(ShadowExtractorEntry { test, extractor });
                }
                Err(e) => {
                    warn!(
                        path = %test.extractor_path.display(),
                        error = %e,
                        "Failed to load shadow extractor, skipping"
                    );
                }
            }
        }

        debug!(count = entries.len(), "Loaded active shadow extractors");
        Ok(entries)
    }

    /// Get a shadow test by name.
    pub fn get_test_by_name(&self, name: &str) -> Result<Option<ShadowTest>, AphoriaError> {
        self.store.get_test_by_name(name)
    }

    /// Get a shadow test by ID.
    pub fn get_test(&self, id: &Uuid) -> Result<Option<ShadowTest>, AphoriaError> {
        self.store.get_test(id)
    }

    /// Update a shadow test.
    pub fn update_test(&self, test: &ShadowTest) -> Result<(), AphoriaError> {
        self.store.save_test(test)
    }

    /// Get all shadow tests.
    pub fn list_all_tests(&self) -> Result<Vec<ShadowTest>, AphoriaError> {
        self.store.list_all_tests()
    }

    /// Get the store for direct access.
    pub fn store(&self) -> &ShadowStore {
        &self.store
    }

    /// Get the config.
    pub fn config(&self) -> &ShadowConfig {
        &self.config
    }

    /// Load an extractor from a YAML file.
    fn load_extractor(&self, path: &Path) -> Result<DeclarativeExtractorDef, AphoriaError> {
        if !path.exists() {
            return Err(AphoriaError::Shadow(format!(
                "Extractor file not found: {}",
                path.display()
            )));
        }

        let content = std::fs::read_to_string(path)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to read extractor file: {e}")))?;

        // Skip the metadata header if present
        let yaml_content =
            if content.starts_with("# AUTO-PROMOTED") || content.starts_with("# PROMOTED") {
                // Find the first line that starts with a non-comment
                let mut lines = content.lines();
                let mut yaml_start = 0;
                for line in lines.by_ref() {
                    if !line.starts_with('#') && !line.is_empty() {
                        break;
                    }
                    yaml_start += line.len() + 1;
                }
                &content[yaml_start.saturating_sub(lines.next().map_or(0, |l| l.len() + 1))..]
            } else {
                &content
            };

        serde_yaml::from_str(yaml_content)
            .map_err(|e| AphoriaError::Shadow(format!("Failed to parse extractor YAML: {e}")))
    }
}

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

    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*(?P<value>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 create_test_registry() -> (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");

        (registry, shadow_temp, learned_temp)
    }

    #[test]
    fn test_register_shadow_test() {
        let (registry, _shadow_temp, learned_temp) = create_test_registry();

        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("test.yaml");

        let test = registry
            .register_shadow_test(&extractor, Uuid::new_v4(), extractor_path.clone())
            .expect("register");

        assert_eq!(test.extractor_name, "test_shadow_extractor");
        assert_eq!(test.status, ShadowStatus::Active);

        // Verify it's in the store
        let loaded =
            registry.get_test_by_name("test_shadow_extractor").expect("get").expect("exists");
        assert_eq!(loaded.id, test.id);
    }

    #[test]
    fn test_register_duplicate_active_test() {
        let (registry, _shadow_temp, learned_temp) = create_test_registry();

        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("test.yaml");

        let test1 = registry
            .register_shadow_test(&extractor, Uuid::new_v4(), extractor_path.clone())
            .expect("register 1");

        // Register again - should return existing
        let test2 = registry
            .register_shadow_test(&extractor, Uuid::new_v4(), extractor_path)
            .expect("register 2");

        assert_eq!(test1.id, test2.id);
    }

    #[test]
    fn test_list_all_tests() {
        let (registry, _shadow_temp, learned_temp) = create_test_registry();

        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("test.yaml");

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

        let tests = registry.list_all_tests().expect("list");
        assert_eq!(tests.len(), 1);
    }
}
