//! Feedback collection for shadow mode testing.
//!
//! Engineers review shadow matches and mark them as true positives
//! or false positives. This feedback is used to calculate FP rates
//! and determine graduation eligibility.

use std::path::PathBuf;

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

use super::graduation::{AutoRollbackResult, GraduationManager};
use super::registry::ShadowExtractorRegistry;
use super::store::ShadowStore;
use super::types::{MatchFeedback, ShadowDecision, ShadowMatch, ShadowTest};
use crate::config::ShadowConfig;
use crate::AphoriaError;

/// Result of a feedback operation.
#[derive(Debug, Default)]
pub struct FeedbackResult {
    /// Number of matches marked as true positives.
    pub true_positives: usize,
    /// Number of matches marked as false positives.
    pub false_positives: usize,
    /// Errors encountered.
    pub errors: Vec<String>,
}

/// Result of recording feedback, including auto-rollback info.
#[derive(Debug, Default)]
pub struct FeedbackWithRollback {
    /// The feedback was recorded successfully.
    pub recorded: bool,
    /// If auto-rollback was triggered, contains the result.
    pub auto_rollback: Option<AutoRollbackResult>,
}

/// Collector for TP/FP feedback on shadow matches.
pub struct FeedbackCollector<'a> {
    registry: &'a ShadowExtractorRegistry,
    config: &'a ShadowConfig,
    production_dir: PathBuf,
}

impl<'a> FeedbackCollector<'a> {
    /// Create a new feedback collector with auto-rollback support.
    pub fn new(
        registry: &'a ShadowExtractorRegistry,
        config: &'a ShadowConfig,
        production_dir: PathBuf,
    ) -> Self {
        Self { registry, config, production_dir }
    }

    /// Get the store.
    fn store(&self) -> &ShadowStore {
        self.registry.store()
    }

    /// Record feedback for a single match.
    ///
    /// Returns `FeedbackWithRollback` indicating whether feedback was recorded
    /// and whether auto-rollback was triggered (for false positives exceeding
    /// the rollback threshold).
    pub fn record_feedback(
        &self,
        test_id: &Uuid,
        match_id: &Uuid,
        feedback: MatchFeedback,
    ) -> Result<FeedbackWithRollback, AphoriaError> {
        // Update match in store
        self.store().update_match_feedback(test_id, match_id, feedback)?;

        // Update test metrics
        let mut test = self
            .registry
            .get_test(test_id)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test {} not found", test_id)))?;

        test.record_feedback(feedback);
        self.registry.update_test(&test)?;

        info!(
            test_id = %test_id,
            match_id = %match_id,
            feedback = %feedback,
            "Recorded feedback for shadow match"
        );

        // Check for auto-rollback after false positive feedback
        let auto_rollback = if feedback == MatchFeedback::FalsePositive
            && self.config.auto_rollback_enabled
            && test.exceeds_rollback_threshold(self.config)
        {
            let manager = GraduationManager::new(self.registry, self.config, &self.production_dir);
            let reason = format!(
                "Auto-rollback: {:.1}% FP rate exceeds {:.1}% threshold",
                test.metrics.fp_rate() * 100.0,
                self.config.rollback_threshold * 100.0
            );

            warn!(
                test_id = %test_id,
                extractor = %test.extractor_name,
                fp_rate = test.metrics.fp_rate() * 100.0,
                threshold = self.config.rollback_threshold * 100.0,
                "Auto-rollback triggered due to high FP rate"
            );

            match manager.rollback(test_id, reason.clone()) {
                Ok(_) => {
                    // Log the 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");
                    }

                    Some(AutoRollbackResult {
                        checked: 1,
                        rolled_back: 1,
                        rolled_back_names: vec![test.extractor_name.clone()],
                        errors: vec![],
                    })
                }
                Err(e) => {
                    warn!(
                        test_id = %test_id,
                        error = %e,
                        "Failed to auto-rollback extractor"
                    );
                    Some(AutoRollbackResult {
                        checked: 1,
                        rolled_back: 0,
                        rolled_back_names: vec![],
                        errors: vec![format!("{}: {}", test.extractor_name, e)],
                    })
                }
            }
        } else {
            None
        };

        Ok(FeedbackWithRollback { recorded: true, auto_rollback })
    }

    /// Get pending (unreviewed) matches for a test.
    pub fn get_pending(&self, test_id: &Uuid) -> Result<Vec<ShadowMatch>, AphoriaError> {
        self.store().get_pending_matches(test_id)
    }

    /// Get pending matches for a test by name.
    pub fn get_pending_by_name(&self, name: &str) -> Result<Vec<ShadowMatch>, AphoriaError> {
        let test = self
            .registry
            .get_test_by_name(name)?
            .ok_or_else(|| AphoriaError::Shadow(format!("Shadow test '{}' not found", name)))?;

        self.get_pending(&test.id)
    }

    /// Bulk mark matches as true positives.
    pub fn bulk_mark_tp(
        &self,
        test_id: &Uuid,
        match_ids: &[Uuid],
    ) -> Result<FeedbackResult, AphoriaError> {
        let mut result = FeedbackResult::default();

        for match_id in match_ids {
            match self.record_feedback(test_id, match_id, MatchFeedback::TruePositive) {
                Ok(feedback_result) => {
                    if feedback_result.recorded {
                        result.true_positives += 1;
                    }
                }
                Err(e) => result.errors.push(format!("{}: {}", match_id, e)),
            }
        }

        debug!(
            test_id = %test_id,
            count = result.true_positives,
            "Bulk marked matches as true positives"
        );

        Ok(result)
    }

    /// Bulk mark matches as false positives.
    ///
    /// Note: This may trigger auto-rollback if the FP rate exceeds the threshold.
    /// The last auto-rollback result (if any) is not returned from bulk operations.
    /// Use `record_feedback` directly if you need to handle auto-rollback responses.
    pub fn bulk_mark_fp(
        &self,
        test_id: &Uuid,
        match_ids: &[Uuid],
    ) -> Result<FeedbackResult, AphoriaError> {
        let mut result = FeedbackResult::default();

        for match_id in match_ids {
            match self.record_feedback(test_id, match_id, MatchFeedback::FalsePositive) {
                Ok(feedback_result) => {
                    if feedback_result.recorded {
                        result.false_positives += 1;
                    }
                }
                Err(e) => result.errors.push(format!("{}: {}", match_id, e)),
            }
        }

        debug!(
            test_id = %test_id,
            count = result.false_positives,
            "Bulk marked matches as false positives"
        );

        Ok(result)
    }

    /// Get the current test state with updated metrics.
    pub fn get_test_state(&self, test_id: &Uuid) -> Result<Option<ShadowTest>, AphoriaError> {
        self.registry.get_test(test_id)
    }

    /// Get the current test state by name.
    pub fn get_test_state_by_name(&self, name: &str) -> Result<Option<ShadowTest>, AphoriaError> {
        self.registry.get_test_by_name(name)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ShadowConfig;
    use crate::extractors::{DeclarativeClaimDef, DeclarativeExtractorDef, DeclarativeValue};
    use crate::shadow::types::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,
        }
    }

    struct TestSetup {
        registry: ShadowExtractorRegistry,
        test: ShadowTest,
        config: ShadowConfig,
        production_dir: PathBuf,
        _shadow_temp: TempDir,
        _learned_temp: TempDir,
        _production_temp: TempDir,
    }

    fn setup_registry_with_test() -> TestSetup {
        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()),
            ..Default::default()
        };

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

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

        // Write the YAML file so rollback can delete it
        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 test = registry
            .register_shadow_test(&extractor, Uuid::new_v4(), extractor_path)
            .expect("register");

        TestSetup {
            registry,
            test,
            config,
            production_dir: production_temp.path().to_path_buf(),
            _shadow_temp: shadow_temp,
            _learned_temp: learned_temp,
            _production_temp: production_temp,
        }
    }

    #[test]
    fn test_record_feedback() {
        let setup = setup_registry_with_test();
        let collector =
            FeedbackCollector::new(&setup.registry, &setup.config, setup.production_dir.clone());

        // Create a match
        let match_ = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file.py"),
            10,
            "verify_ssl = false".to_string(),
            "context".to_string(),
        );
        setup.registry.store().save_match(&match_).expect("save match");

        // Record feedback
        let result = collector
            .record_feedback(&setup.test.id, &match_.id, MatchFeedback::TruePositive)
            .expect("record");

        assert!(result.recorded);
        assert!(result.auto_rollback.is_none());

        // Verify test metrics updated
        let updated_test = collector.get_test_state(&setup.test.id).expect("get").expect("exists");
        assert_eq!(updated_test.metrics.true_positives, 1);
    }

    #[test]
    fn test_get_pending() {
        let setup = setup_registry_with_test();
        let collector =
            FeedbackCollector::new(&setup.registry, &setup.config, setup.production_dir.clone());

        // Create matches
        let match1 = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file1.py"),
            10,
            "match1".to_string(),
            "context1".to_string(),
        );
        let match2 = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file2.py"),
            20,
            "match2".to_string(),
            "context2".to_string(),
        );
        setup.registry.store().save_match(&match1).expect("save 1");
        setup.registry.store().save_match(&match2).expect("save 2");

        // All should be pending
        let pending = collector.get_pending(&setup.test.id).expect("get pending");
        assert_eq!(pending.len(), 2);

        // Mark one as reviewed
        collector
            .record_feedback(&setup.test.id, &match1.id, MatchFeedback::FalsePositive)
            .expect("record");

        // Only one should be pending now
        let pending = collector.get_pending(&setup.test.id).expect("get pending");
        assert_eq!(pending.len(), 1);
        assert_eq!(pending[0].id, match2.id);
    }

    #[test]
    fn test_bulk_mark_tp() {
        let setup = setup_registry_with_test();
        let collector =
            FeedbackCollector::new(&setup.registry, &setup.config, setup.production_dir.clone());

        // Create matches
        let match1 = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file1.py"),
            10,
            "match1".to_string(),
            "context1".to_string(),
        );
        let match2 = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file2.py"),
            20,
            "match2".to_string(),
            "context2".to_string(),
        );
        setup.registry.store().save_match(&match1).expect("save 1");
        setup.registry.store().save_match(&match2).expect("save 2");

        // Bulk mark as TP
        let result =
            collector.bulk_mark_tp(&setup.test.id, &[match1.id, match2.id]).expect("bulk mark");

        assert_eq!(result.true_positives, 2);
        assert!(result.errors.is_empty());

        // Verify metrics
        let updated_test = collector.get_test_state(&setup.test.id).expect("get").expect("exists");
        assert_eq!(updated_test.metrics.true_positives, 2);
    }

    #[test]
    fn test_get_pending_by_name() {
        let setup = setup_registry_with_test();
        let collector =
            FeedbackCollector::new(&setup.registry, &setup.config, setup.production_dir.clone());

        let match_ = ShadowMatch::new(
            setup.test.id,
            PathBuf::from("/file.py"),
            10,
            "match".to_string(),
            "context".to_string(),
        );
        setup.registry.store().save_match(&match_).expect("save");

        let pending = collector.get_pending_by_name("test_extractor").expect("get");
        assert_eq!(pending.len(), 1);
    }

    #[test]
    fn test_auto_rollback_on_feedback() {
        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");

        // Use low thresholds for testing
        let config = ShadowConfig {
            shadow_dir: Some(shadow_temp.path().to_path_buf()),
            rollback_threshold: 0.15,
            auto_rollback_enabled: true,
            ..Default::default()
        };

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

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

        // Write the YAML file so rollback can delete it
        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.clone())
            .expect("register");

        // Add some TPs and FPs to get close to threshold (need 10+ reviews for rollback)
        // 8 TPs + 2 FPs = 10 reviews, 20% FP rate (above 15% threshold)
        for _ in 0..8 {
            test.record_feedback(MatchFeedback::TruePositive);
        }
        for _ in 0..2 {
            test.record_feedback(MatchFeedback::FalsePositive);
        }
        registry.update_test(&test).expect("update");

        let collector =
            FeedbackCollector::new(&registry, &config, production_temp.path().to_path_buf());

        // Create a match and mark as FP - this should push over threshold
        let match_ = ShadowMatch::new(
            test.id,
            PathBuf::from("/file.py"),
            10,
            "verify_ssl = false".to_string(),
            "context".to_string(),
        );
        registry.store().save_match(&match_).expect("save match");

        // Record FP feedback - should trigger auto-rollback
        let result = collector
            .record_feedback(&test.id, &match_.id, MatchFeedback::FalsePositive)
            .expect("record");

        assert!(result.recorded);
        assert!(result.auto_rollback.is_some());
        let rollback = result.auto_rollback.expect("rollback result");
        assert_eq!(rollback.rolled_back, 1);
        assert_eq!(rollback.rolled_back_names, vec!["test_extractor".to_string()]);

        // Verify test status is now RolledBack
        let updated_test = registry.get_test(&test.id).expect("get").expect("exists");
        assert_eq!(updated_test.status, ShadowStatus::RolledBack);

        // Verify YAML file was deleted
        assert!(!extractor_path.exists());
    }

    #[test]
    fn test_no_auto_rollback_when_disabled() {
        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");

        // Disable auto-rollback
        let config = ShadowConfig {
            shadow_dir: Some(shadow_temp.path().to_path_buf()),
            rollback_threshold: 0.15,
            auto_rollback_enabled: false, // Disabled
            ..Default::default()
        };

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

        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.clone())
            .expect("register");

        // Setup test to exceed threshold
        for _ in 0..8 {
            test.record_feedback(MatchFeedback::TruePositive);
        }
        for _ in 0..2 {
            test.record_feedback(MatchFeedback::FalsePositive);
        }
        registry.update_test(&test).expect("update");

        let collector =
            FeedbackCollector::new(&registry, &config, production_temp.path().to_path_buf());

        // Create and mark match as FP
        let match_ = ShadowMatch::new(
            test.id,
            PathBuf::from("/file.py"),
            10,
            "verify_ssl = false".to_string(),
            "context".to_string(),
        );
        registry.store().save_match(&match_).expect("save match");

        // Record FP feedback - should NOT trigger auto-rollback
        let result = collector
            .record_feedback(&test.id, &match_.id, MatchFeedback::FalsePositive)
            .expect("record");

        assert!(result.recorded);
        assert!(result.auto_rollback.is_none()); // No rollback because disabled

        // Verify test status is still Active
        let updated_test = registry.get_test(&test.id).expect("get").expect("exists");
        assert_eq!(updated_test.status, ShadowStatus::Active);

        // Verify YAML file still exists
        assert!(extractor_path.exists());
    }

    #[test]
    fn test_auto_rollback_at_exact_threshold() {
        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()),
            rollback_threshold: 0.15, // 15%
            auto_rollback_enabled: true,
            ..Default::default()
        };

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

        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.clone())
            .expect("register");

        // Setup test at exactly 15% (need 10+ reviews)
        // 17 TPs + 3 FPs = 20 reviews, 15% FP rate (at threshold)
        for _ in 0..17 {
            test.record_feedback(MatchFeedback::TruePositive);
        }
        for _ in 0..3 {
            test.record_feedback(MatchFeedback::FalsePositive);
        }
        registry.update_test(&test).expect("update");

        // Verify we're exactly at threshold
        let fp_rate = test.metrics.fp_rate();
        assert!((fp_rate - 0.15).abs() < 0.001);

        let collector =
            FeedbackCollector::new(&registry, &config, production_temp.path().to_path_buf());

        // Create and mark match as FP - pushes over threshold
        let match_ = ShadowMatch::new(
            test.id,
            PathBuf::from("/file.py"),
            10,
            "verify_ssl = false".to_string(),
            "context".to_string(),
        );
        registry.store().save_match(&match_).expect("save match");

        // Record FP - threshold check is >= so this should trigger rollback
        let result = collector
            .record_feedback(&test.id, &match_.id, MatchFeedback::FalsePositive)
            .expect("record");

        assert!(result.recorded);
        // The FP pushes us to 4/21 = 19%, which exceeds 15%
        assert!(result.auto_rollback.is_some());
    }
}
