//! Graduation management for shadow mode testing.
//!
//! Handles promoting shadow extractors to production and rolling back
//! failed extractors.

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

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

use super::registry::ShadowExtractorRegistry;
use super::types::{ShadowDecision, ShadowTest};
use crate::config::ShadowConfig;
use crate::AphoriaError;

/// Result of checking graduation eligibility.
#[derive(Debug)]
pub struct GraduationCandidate {
    /// The shadow test.
    pub test: ShadowTest,
    /// Whether the test meets graduation criteria.
    pub is_ready: bool,
    /// Reason if not ready.
    pub not_ready_reason: Option<String>,
}

/// Result of a graduation or rollback operation.
#[derive(Debug)]
pub struct GraduationResult {
    /// Whether the operation succeeded.
    pub success: bool,
    /// Path to the extractor file (for graduation).
    pub extractor_path: Option<PathBuf>,
    /// Message describing the result.
    pub message: String,
}

/// Result of auto-rollback check.
#[derive(Debug, Default)]
pub struct AutoRollbackResult {
    /// Number of tests checked.
    pub checked: usize,
    /// Number of extractors rolled back.
    pub rolled_back: usize,
    /// Names of rolled back extractors.
    pub rolled_back_names: Vec<String>,
    /// Errors encountered.
    pub errors: Vec<String>,
}

/// Manager for shadow-to-production graduation.
pub struct GraduationManager<'a> {
    registry: &'a ShadowExtractorRegistry,
    config: &'a ShadowConfig,
    /// Production extractors directory.
    production_dir: PathBuf,
}

impl<'a> GraduationManager<'a> {
    /// Create a new graduation manager.
    pub fn new(
        registry: &'a ShadowExtractorRegistry,
        config: &'a ShadowConfig,
        production_dir: impl AsRef<Path>,
    ) -> Self {
        Self { registry, config, production_dir: production_dir.as_ref().to_path_buf() }
    }

    /// Get effective graduation threshold for a test, scaled by evidence level.
    ///
    /// Higher evidence levels require fewer scans to graduate:
    /// - ProductSpec: 10% of base (0.1x)
    /// - Standard: 30% of base (0.3x)
    /// - Research: 50% of base (0.5x)
    /// - Commit: 100% of base (1.0x)
    pub fn effective_min_scans(&self, test: &ShadowTest) -> usize {
        let multiplier = test.effective_evidence_level().threshold_multiplier();
        let scaled = (self.config.min_scans as f32 * multiplier).ceil() as usize;
        // Minimum of 1 scan required
        scaled.max(1)
    }

    /// Check if a test meets evidence-aware graduation criteria.
    pub fn meets_evidence_aware_criteria(&self, test: &ShadowTest) -> bool {
        test.status == super::types::ShadowStatus::Active
            && test.metrics.total_scans >= self.effective_min_scans(test)
            && test.metrics.fp_rate() <= self.config.max_fp_rate
            && test.metrics.total_reviewed() > 0
    }

    /// Check if a test is ready for graduation.
    pub fn is_ready(&self, test_id: &Uuid) -> Result<bool, AphoriaError> {
        let test = self
            .registry
            .get_test(test_id)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test {} not found", test_id)))?;

        Ok(self.meets_evidence_aware_criteria(&test))
    }

    /// Check if a test is ready for graduation by name.
    pub fn is_ready_by_name(&self, name: &str) -> Result<bool, AphoriaError> {
        let test = self
            .registry
            .get_test_by_name(name)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test '{}' not found", name)))?;

        Ok(self.meets_evidence_aware_criteria(&test))
    }

    /// Graduate a shadow extractor to production.
    ///
    /// Moves the YAML file from learned/ to production/ and updates
    /// the test status.
    pub fn graduate(&self, test_id: &Uuid) -> Result<GraduationResult, AphoriaError> {
        let mut test = self
            .registry
            .get_test(test_id)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test {} not found", test_id)))?;

        // Check if ready (using evidence-aware criteria)
        if !self.meets_evidence_aware_criteria(&test) {
            let metrics = &test.metrics;
            let effective_min = self.effective_min_scans(&test);
            let evidence_level = test.effective_evidence_level();
            let reason = format!(
                "Not ready: {} scans (need {}, {} level), {:.1}% FP rate (max {:.1}%), {} reviewed",
                metrics.total_scans,
                effective_min,
                evidence_level.badge(),
                metrics.fp_rate() * 100.0,
                self.config.max_fp_rate * 100.0,
                metrics.total_reviewed()
            );
            return Ok(GraduationResult { success: false, extractor_path: None, message: reason });
        }

        // Move YAML file to production directory
        let source_path = &test.extractor_path;
        let file_name = source_path
            .file_name()
            .ok_or_else(|| AphoriaError::Shadow("Invalid extractor path".to_string()))?;
        let dest_path = self.production_dir.join(file_name);

        // Create production directory if needed
        fs::create_dir_all(&self.production_dir).map_err(|e| {
            AphoriaError::Shadow(format!("Failed to create production directory: {e}"))
        })?;

        // Copy file (keep original in learned/ for rollback)
        fs::copy(source_path, &dest_path).map_err(|e| {
            AphoriaError::Shadow(format!("Failed to copy extractor to production: {e}"))
        })?;

        // Update test status
        test.graduate();
        self.registry.update_test(&test)?;

        // Log decision
        self.registry.store().log_decision(&ShadowDecision::graduated(&test))?;

        info!(
            test_id = %test_id,
            extractor = %test.extractor_name,
            dest = %dest_path.display(),
            "Graduated shadow extractor to production"
        );

        Ok(GraduationResult {
            success: true,
            extractor_path: Some(dest_path),
            message: format!(
                "Graduated '{}' to production ({} scans, {:.1}% FP rate)",
                test.extractor_name,
                test.metrics.total_scans,
                test.metrics.fp_rate() * 100.0
            ),
        })
    }

    /// Graduate a shadow extractor by name.
    pub fn graduate_by_name(&self, name: &str) -> Result<GraduationResult, AphoriaError> {
        let test = self
            .registry
            .get_test_by_name(name)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test '{}' not found", name)))?;

        self.graduate(&test.id)
    }

    /// Rollback a shadow extractor.
    ///
    /// Removes the YAML file from learned/ and marks the test as rolled back.
    pub fn rollback(
        &self,
        test_id: &Uuid,
        reason: String,
    ) -> Result<GraduationResult, AphoriaError> {
        let mut test = self
            .registry
            .get_test(test_id)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test {} not found", test_id)))?;

        // Delete YAML file
        if test.extractor_path.exists() {
            fs::remove_file(&test.extractor_path).map_err(|e| {
                AphoriaError::Shadow(format!("Failed to delete extractor file: {e}"))
            })?;
        }

        // Update test status
        test.rollback(reason.clone());
        self.registry.update_test(&test)?;

        // Log decision
        self.registry
            .store()
            .log_decision(&ShadowDecision::manual_rollback(&test, reason.clone()))?;

        info!(
            test_id = %test_id,
            extractor = %test.extractor_name,
            reason = %reason,
            "Rolled back shadow extractor"
        );

        Ok(GraduationResult {
            success: true,
            extractor_path: None,
            message: format!("Rolled back '{}': {}", test.extractor_name, reason),
        })
    }

    /// Rollback a shadow extractor by name.
    pub fn rollback_by_name(
        &self,
        name: &str,
        reason: String,
    ) -> Result<GraduationResult, AphoriaError> {
        let test = self
            .registry
            .get_test_by_name(name)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test '{}' not found", name)))?;

        self.rollback(&test.id, reason)
    }

    /// Check for and perform auto-rollbacks.
    ///
    /// Scans all active tests and rolls back any that exceed the
    /// FP threshold.
    pub fn check_auto_rollback(&self) -> Result<AutoRollbackResult, AphoriaError> {
        let mut result = AutoRollbackResult::default();

        let tests = self.registry.store().list_active_tests()?;
        result.checked = tests.len();

        for test in tests {
            if test.exceeds_rollback_threshold(self.config) {
                let reason = format!(
                    "Auto-rollback: {:.1}% FP rate exceeds {:.1}% threshold",
                    test.metrics.fp_rate() * 100.0,
                    self.config.rollback_threshold * 100.0
                );

                match self.rollback(&test.id, reason) {
                    Ok(_) => {
                        result.rolled_back += 1;
                        result.rolled_back_names.push(test.extractor_name.clone());

                        // Also log auto-rollback decision
                        if let Err(e) = self
                            .registry
                            .store()
                            .log_decision(&ShadowDecision::auto_rollback(&test))
                        {
                            warn!(error = %e, "Failed to log auto-rollback decision");
                        }
                    }
                    Err(e) => {
                        result.errors.push(format!("{}: {}", test.extractor_name, e));
                    }
                }
            }
        }

        if result.rolled_back > 0 {
            warn!(
                count = result.rolled_back,
                names = ?result.rolled_back_names,
                "Auto-rolled back extractors due to high FP rate"
            );
        }

        Ok(result)
    }

    /// Get all graduation candidates.
    ///
    /// Returns all active tests with their graduation readiness status.
    pub fn get_candidates(&self) -> Result<Vec<GraduationCandidate>, AphoriaError> {
        let tests = self.registry.store().list_active_tests()?;
        let mut candidates = Vec::new();

        for test in tests {
            let is_ready = self.meets_evidence_aware_criteria(&test);
            let not_ready_reason = if is_ready {
                None
            } else {
                let metrics = &test.metrics;
                let effective_min = self.effective_min_scans(&test);
                let mut reasons = Vec::new();

                if metrics.total_scans < effective_min {
                    let evidence_badge = test.effective_evidence_level().badge();
                    reasons.push(format!(
                        "{}/{} scans {}",
                        metrics.total_scans, effective_min, evidence_badge
                    ));
                }

                if metrics.total_reviewed() == 0 {
                    reasons.push("no feedback yet".to_string());
                } else if metrics.fp_rate() > self.config.max_fp_rate {
                    reasons.push(format!(
                        "{:.1}% FP (max {:.1}%)",
                        metrics.fp_rate() * 100.0,
                        self.config.max_fp_rate * 100.0
                    ));
                }

                Some(reasons.join(", "))
            };

            candidates.push(GraduationCandidate { test, is_ready, not_ready_reason });
        }

        debug!(count = candidates.len(), "Got graduation candidates");
        Ok(candidates)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extractors::{DeclarativeClaimDef, DeclarativeExtractorDef, DeclarativeValue};
    use crate::shadow::types::{MatchFeedback, ShadowStatus};
    use std::fs;
    use tempfile::TempDir;

    fn create_test_extractor() -> DeclarativeExtractorDef {
        DeclarativeExtractorDef {
            name: "test_extractor".to_string(),
            description: "Test 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_environment() -> (ShadowExtractorRegistry, ShadowConfig, TempDir, TempDir, TempDir) {
        let shadow_temp = TempDir::new().expect("shadow temp dir");
        let learned_temp = TempDir::new().expect("learned temp dir");
        let production_temp = TempDir::new().expect("production temp dir");

        let config = ShadowConfig {
            shadow_dir: Some(shadow_temp.path().to_path_buf()),
            min_scans: 10, // Lower for testing
            max_fp_rate: 0.10,
            rollback_threshold: 0.20,
            ..Default::default()
        };

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

        (registry, config, shadow_temp, learned_temp, production_temp)
    }

    fn create_test_with_metrics(
        registry: &ShadowExtractorRegistry,
        learned_temp: &TempDir,
        scans: usize,
        tp: usize,
        fp: usize,
    ) -> ShadowTest {
        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("test.yaml");
        let yaml = r#"name: test_extractor
description: Test 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");

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

        // Add metrics
        for _ in 0..scans {
            test.record_scan();
        }
        for _ in 0..tp {
            test.record_feedback(MatchFeedback::TruePositive);
        }
        for _ in 0..fp {
            test.record_feedback(MatchFeedback::FalsePositive);
        }

        registry.update_test(&test).expect("update");
        test
    }

    #[test]
    fn test_is_ready_not_enough_scans() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 5, 10, 0);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        assert!(!manager.is_ready(&test.id).expect("is_ready"));
    }

    #[test]
    fn test_is_ready_high_fp_rate() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 10, 5, 5); // 50% FP rate

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        assert!(!manager.is_ready(&test.id).expect("is_ready"));
    }

    #[test]
    fn test_is_ready_meets_criteria() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 10, 19, 1); // 5% FP rate

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        assert!(manager.is_ready(&test.id).expect("is_ready"));
    }

    #[test]
    fn test_graduate_success() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 10, 19, 1);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        let result = manager.graduate(&test.id).expect("graduate");

        assert!(result.success);
        assert!(result.extractor_path.is_some());
        assert!(result.extractor_path.as_ref().expect("path").exists());

        // Check test status updated
        let updated = registry.get_test(&test.id).expect("get").expect("exists");
        assert_eq!(updated.status, ShadowStatus::Graduated);
    }

    #[test]
    fn test_graduate_not_ready() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 5, 10, 0);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        let result = manager.graduate(&test.id).expect("graduate");

        assert!(!result.success);
        assert!(result.extractor_path.is_none());
    }

    #[test]
    fn test_rollback() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let test = create_test_with_metrics(&registry, &learned_temp, 10, 5, 5);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        let result =
            manager.rollback(&test.id, "Too many false positives".to_string()).expect("rollback");

        assert!(result.success);

        // Check test status updated
        let updated = registry.get_test(&test.id).expect("get").expect("exists");
        assert_eq!(updated.status, ShadowStatus::RolledBack);
        assert_eq!(updated.rollback_reason, Some("Too many false positives".to_string()));

        // Check YAML file deleted
        assert!(!test.extractor_path.exists());
    }

    #[test]
    fn test_check_auto_rollback() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        // Create test with high FP rate
        let _test = create_test_with_metrics(&registry, &learned_temp, 10, 5, 5); // 50% FP rate

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        let result = manager.check_auto_rollback().expect("check");

        assert_eq!(result.rolled_back, 1);
        assert!(!result.rolled_back_names.is_empty());
    }

    #[test]
    fn test_get_candidates() {
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let _test = create_test_with_metrics(&registry, &learned_temp, 10, 19, 1);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());
        let candidates = manager.get_candidates().expect("get candidates");

        assert_eq!(candidates.len(), 1);
        assert!(candidates[0].is_ready);
        assert!(candidates[0].not_ready_reason.is_none());
    }

    #[test]
    fn test_evidence_aware_graduation_standard_level() {
        // Standard evidence level should require 30% of base scans (3 instead of 10)
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        // Create extractor and test
        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("evidence_test.yaml");
        let yaml = r#"name: evidence_test_extractor
description: Test extractor with evidence
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");

        // Create test with Standard evidence level
        let mut test = ShadowTest::with_evidence(
            extractor.name.clone(),
            extractor_path,
            Uuid::new_v4(),
            Some(crate::evidence::EvidenceLevel::Standard),
            vec![crate::evidence::EvidenceSource::Rfc { number: 7519, section: None }],
        );

        // Add only 3 scans (30% of 10) - should be enough for Standard level
        for _ in 0..3 {
            test.record_scan();
        }
        // Add good feedback (1 TP, 0 FP)
        test.record_feedback(MatchFeedback::TruePositive);

        registry.store().save_test(&test).expect("save test");

        let manager = GraduationManager::new(&registry, &config, production_temp.path());

        // Calculate expected min scans for Standard level
        let expected_min = manager.effective_min_scans(&test);
        assert_eq!(expected_min, 3, "Standard level should require 3 scans (30% of 10)");

        // Should be ready with only 3 scans at Standard evidence level
        assert!(
            manager.is_ready(&test.id).expect("is_ready"),
            "Standard evidence level should be ready with 3 scans"
        );
    }

    #[test]
    fn test_evidence_aware_graduation_product_spec_level() {
        // ProductSpec evidence level should require 10% of base scans (1 instead of 10)
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        let extractor = create_test_extractor();
        let extractor_path = learned_temp.path().join("spec_test.yaml");
        let yaml = r#"name: spec_test_extractor
description: Test extractor with spec evidence
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");

        // Create test with ProductSpec evidence level
        let mut test = ShadowTest::with_evidence(
            extractor.name.clone(),
            extractor_path,
            Uuid::new_v4(),
            Some(crate::evidence::EvidenceLevel::ProductSpec),
            vec![crate::evidence::EvidenceSource::Spec {
                path: "specs/security.md".to_string(),
                requirement_id: Some("REQ-SEC-001".to_string()),
            }],
        );

        // Add only 1 scan - should be enough for ProductSpec level
        test.record_scan();
        test.record_feedback(MatchFeedback::TruePositive);

        registry.store().save_test(&test).expect("save test");

        let manager = GraduationManager::new(&registry, &config, production_temp.path());

        // Calculate expected min scans for ProductSpec level
        let expected_min = manager.effective_min_scans(&test);
        assert_eq!(expected_min, 1, "ProductSpec level should require 1 scan (10% of 10, min 1)");

        // Should be ready with only 1 scan at ProductSpec evidence level
        assert!(
            manager.is_ready(&test.id).expect("is_ready"),
            "ProductSpec evidence level should be ready with 1 scan"
        );
    }

    #[test]
    fn test_commit_level_requires_full_scans() {
        // Commit (default) evidence level should require 100% of base scans (10)
        let (registry, config, _shadow_temp, learned_temp, production_temp) = setup_environment();

        // Create test with no evidence (defaults to Commit level)
        let test = create_test_with_metrics(&registry, &learned_temp, 5, 10, 0);

        let manager = GraduationManager::new(&registry, &config, production_temp.path());

        // Calculate expected min scans for Commit level
        let expected_min = manager.effective_min_scans(&test);
        assert_eq!(expected_min, 10, "Commit level should require 10 scans (100%)");

        // Should NOT be ready with only 5 scans at Commit evidence level
        assert!(
            !manager.is_ready(&test.id).expect("is_ready"),
            "Commit evidence level should NOT be ready with only 5 scans"
        );
    }
}
