//! Core types for shadow mode testing.
//!
//! These types track the lifecycle of an extractor from shadow mode
//! through graduation or rollback.

use std::path::PathBuf;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::config::ShadowConfig;
use crate::evidence::{EvidenceLevel, EvidenceSource};

/// State for an extractor under shadow testing.
///
/// Tracks the extractor's journey from auto-promotion through
/// shadow testing to graduation or rollback.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowTest {
    /// Unique identifier for this shadow test.
    pub id: Uuid,

    /// Name of the extractor being tested.
    pub extractor_name: String,

    /// Path to the extractor YAML file.
    pub extractor_path: PathBuf,

    /// ID of the source pattern that was auto-promoted.
    pub source_pattern_id: Uuid,

    /// Current status of the shadow test.
    pub status: ShadowStatus,

    /// Metrics for this shadow test.
    pub metrics: ShadowMetrics,

    /// When this shadow test was created.
    pub created_at: DateTime<Utc>,

    /// When this shadow test was last updated.
    pub updated_at: DateTime<Utc>,

    /// When this extractor was graduated (if applicable).
    pub graduated_at: Option<DateTime<Utc>>,

    /// When this extractor was rolled back (if applicable).
    pub rolled_back_at: Option<DateTime<Utc>>,

    /// Reason for rollback (if applicable).
    pub rollback_reason: Option<String>,

    /// Evidence level for this pattern (determines graduation threshold).
    #[serde(default)]
    pub evidence_level: Option<EvidenceLevel>,

    /// Evidence sources backing this pattern.
    #[serde(default)]
    pub evidence_sources: Vec<EvidenceSource>,
}

impl ShadowTest {
    /// Create a new shadow test for an auto-promoted extractor.
    pub fn new(extractor_name: String, extractor_path: PathBuf, source_pattern_id: Uuid) -> Self {
        let now = Utc::now();
        Self {
            id: Uuid::new_v4(),
            extractor_name,
            extractor_path,
            source_pattern_id,
            status: ShadowStatus::Active,
            metrics: ShadowMetrics::default(),
            created_at: now,
            updated_at: now,
            graduated_at: None,
            rolled_back_at: None,
            rollback_reason: None,
            evidence_level: None,
            evidence_sources: Vec::new(),
        }
    }

    /// Create a new shadow test with evidence.
    pub fn with_evidence(
        extractor_name: String,
        extractor_path: PathBuf,
        source_pattern_id: Uuid,
        evidence_level: Option<EvidenceLevel>,
        evidence_sources: Vec<EvidenceSource>,
    ) -> Self {
        let mut test = Self::new(extractor_name, extractor_path, source_pattern_id);
        test.evidence_level = evidence_level;
        test.evidence_sources = evidence_sources;
        test
    }

    /// Get the effective evidence level.
    ///
    /// Returns the cached level or computes from sources.
    pub fn effective_evidence_level(&self) -> EvidenceLevel {
        self.evidence_level.unwrap_or_else(|| {
            self.evidence_sources.iter().map(|s| s.level()).max().unwrap_or(EvidenceLevel::Commit)
        })
    }

    /// Check if this test meets graduation criteria.
    pub fn meets_graduation_criteria(&self, config: &ShadowConfig) -> bool {
        self.status == ShadowStatus::Active
            && self.metrics.meets_graduation_criteria(config.min_scans, config.max_fp_rate)
    }

    /// Check if this test exceeds rollback threshold.
    pub fn exceeds_rollback_threshold(&self, config: &ShadowConfig) -> bool {
        self.status == ShadowStatus::Active
            && self.metrics.total_reviewed() >= config.min_rollback_samples
            && self.metrics.exceeds_rollback_threshold(config.rollback_threshold)
    }

    /// Mark as graduated.
    pub fn graduate(&mut self) {
        self.status = ShadowStatus::Graduated;
        self.graduated_at = Some(Utc::now());
        self.updated_at = Utc::now();
    }

    /// Mark as rolled back.
    pub fn rollback(&mut self, reason: String) {
        self.status = ShadowStatus::RolledBack;
        self.rolled_back_at = Some(Utc::now());
        self.rollback_reason = Some(reason);
        self.updated_at = Utc::now();
    }

    /// Pause the test (e.g., for investigation).
    pub fn pause(&mut self) {
        self.status = ShadowStatus::Paused;
        self.updated_at = Utc::now();
    }

    /// Resume a paused test.
    pub fn resume(&mut self) {
        if self.status == ShadowStatus::Paused {
            self.status = ShadowStatus::Active;
            self.updated_at = Utc::now();
        }
    }

    /// Record a scan (increments total_scans).
    pub fn record_scan(&mut self) {
        self.metrics.total_scans += 1;
        self.updated_at = Utc::now();
    }

    /// Record a match (increments pending_review).
    pub fn record_match(&mut self) {
        self.metrics.pending_review += 1;
        self.updated_at = Utc::now();
    }

    /// Record feedback for a match.
    pub fn record_feedback(&mut self, feedback: MatchFeedback) {
        match feedback {
            MatchFeedback::TruePositive => self.metrics.true_positives += 1,
            MatchFeedback::FalsePositive => self.metrics.false_positives += 1,
        }
        if self.metrics.pending_review > 0 {
            self.metrics.pending_review -= 1;
        }
        self.updated_at = Utc::now();
    }
}

/// Status of a shadow test.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShadowStatus {
    /// Actively running during scans.
    Active,
    /// Graduated to production.
    Graduated,
    /// Rolled back (removed from shadow mode).
    RolledBack,
    /// Paused (not running, but not removed).
    Paused,
}

impl std::fmt::Display for ShadowStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ShadowStatus::Active => write!(f, "active"),
            ShadowStatus::Graduated => write!(f, "graduated"),
            ShadowStatus::RolledBack => write!(f, "rolled_back"),
            ShadowStatus::Paused => write!(f, "paused"),
        }
    }
}

/// Metrics for a shadow test.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ShadowMetrics {
    /// Total number of files scanned.
    pub total_scans: usize,

    /// Number of matches marked as true positives.
    pub true_positives: usize,

    /// Number of matches marked as false positives.
    pub false_positives: usize,

    /// Number of matches pending review.
    pub pending_review: usize,
}

impl ShadowMetrics {
    /// Calculate the false positive rate.
    ///
    /// Returns 0.0 if no feedback has been given yet.
    pub fn fp_rate(&self) -> f32 {
        let total = self.total_reviewed();
        if total == 0 {
            0.0
        } else {
            self.false_positives as f32 / total as f32
        }
    }

    /// Get total number of reviewed matches (TP + FP).
    pub fn total_reviewed(&self) -> usize {
        self.true_positives + self.false_positives
    }

    /// Get total matches (reviewed + pending).
    pub fn total_matches(&self) -> usize {
        self.total_reviewed() + self.pending_review
    }

    /// Check if metrics meet graduation criteria.
    pub fn meets_graduation_criteria(&self, min_scans: usize, max_fp_rate: f32) -> bool {
        self.total_scans >= min_scans && self.fp_rate() <= max_fp_rate && self.total_reviewed() > 0
    }

    /// Check if FP rate exceeds rollback threshold.
    pub fn exceeds_rollback_threshold(&self, threshold: f32) -> bool {
        self.fp_rate() >= threshold
    }
}

/// An individual match for feedback collection.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowMatch {
    /// Unique identifier for this match.
    pub id: Uuid,

    /// ID of the shadow test this match belongs to.
    pub test_id: Uuid,

    /// Path to the file where the match was found.
    pub file_path: PathBuf,

    /// Line number in the file.
    pub line_number: usize,

    /// The matched text.
    pub matched_text: String,

    /// Context around the match (for review).
    pub context: String,

    /// When this match was found.
    pub found_at: DateTime<Utc>,

    /// Feedback for this match (if reviewed).
    pub feedback: Option<MatchFeedback>,

    /// When feedback was given.
    pub reviewed_at: Option<DateTime<Utc>>,
}

impl ShadowMatch {
    /// Create a new shadow match.
    pub fn new(
        test_id: Uuid,
        file_path: PathBuf,
        line_number: usize,
        matched_text: String,
        context: String,
    ) -> Self {
        Self {
            id: Uuid::new_v4(),
            test_id,
            file_path,
            line_number,
            matched_text,
            context,
            found_at: Utc::now(),
            feedback: None,
            reviewed_at: None,
        }
    }

    /// Mark as true positive.
    pub fn mark_true_positive(&mut self) {
        self.feedback = Some(MatchFeedback::TruePositive);
        self.reviewed_at = Some(Utc::now());
    }

    /// Mark as false positive.
    pub fn mark_false_positive(&mut self) {
        self.feedback = Some(MatchFeedback::FalsePositive);
        self.reviewed_at = Some(Utc::now());
    }

    /// Check if this match has been reviewed.
    pub fn is_reviewed(&self) -> bool {
        self.feedback.is_some()
    }
}

/// Feedback for a match.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchFeedback {
    /// This match is a true positive (correct detection).
    TruePositive,
    /// This match is a false positive (incorrect detection).
    FalsePositive,
}

impl std::fmt::Display for MatchFeedback {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MatchFeedback::TruePositive => write!(f, "true_positive"),
            MatchFeedback::FalsePositive => write!(f, "false_positive"),
        }
    }
}

/// Audit record for a shadow lifecycle event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowDecision {
    /// Unique identifier for this decision.
    pub id: Uuid,

    /// ID of the shadow test this decision applies to.
    pub test_id: Uuid,

    /// Name of the extractor.
    pub extractor_name: String,

    /// The kind of decision made.
    pub kind: ShadowDecisionKind,

    /// Reason for the decision.
    pub reason: String,

    /// When this decision was made.
    pub timestamp: DateTime<Utc>,

    /// Metrics at the time of decision.
    pub metrics_snapshot: ShadowMetrics,
}

impl ShadowDecision {
    /// Create a new decision record.
    pub fn new(test: &ShadowTest, kind: ShadowDecisionKind, reason: String) -> Self {
        Self {
            id: Uuid::new_v4(),
            test_id: test.id,
            extractor_name: test.extractor_name.clone(),
            kind,
            reason,
            timestamp: Utc::now(),
            metrics_snapshot: test.metrics.clone(),
        }
    }

    /// Create a decision for entering shadow mode.
    pub fn entered_shadow(test: &ShadowTest) -> Self {
        Self::new(
            test,
            ShadowDecisionKind::EnteredShadow,
            "Auto-promoted extractor entered shadow mode".to_string(),
        )
    }

    /// Create a decision for graduation.
    pub fn graduated(test: &ShadowTest) -> Self {
        Self::new(
            test,
            ShadowDecisionKind::Graduated,
            format!(
                "Met graduation criteria: {} scans, {:.1}% FP rate",
                test.metrics.total_scans,
                test.metrics.fp_rate() * 100.0
            ),
        )
    }

    /// Create a decision for manual rollback.
    pub fn manual_rollback(test: &ShadowTest, reason: String) -> Self {
        Self::new(test, ShadowDecisionKind::ManualRollback, reason)
    }

    /// Create a decision for auto-rollback.
    pub fn auto_rollback(test: &ShadowTest) -> Self {
        Self::new(
            test,
            ShadowDecisionKind::AutoRollback,
            format!("Exceeded rollback threshold: {:.1}% FP rate", test.metrics.fp_rate() * 100.0),
        )
    }
}

/// Kind of shadow lifecycle decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShadowDecisionKind {
    /// Extractor entered shadow mode.
    EnteredShadow,
    /// Extractor graduated to production.
    Graduated,
    /// Extractor was manually rolled back.
    ManualRollback,
    /// Extractor was automatically rolled back due to high FP rate.
    AutoRollback,
    /// Shadow test was paused.
    Paused,
    /// Shadow test was resumed.
    Resumed,
}

impl std::fmt::Display for ShadowDecisionKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ShadowDecisionKind::EnteredShadow => write!(f, "entered_shadow"),
            ShadowDecisionKind::Graduated => write!(f, "graduated"),
            ShadowDecisionKind::ManualRollback => write!(f, "manual_rollback"),
            ShadowDecisionKind::AutoRollback => write!(f, "auto_rollback"),
            ShadowDecisionKind::Paused => write!(f, "paused"),
            ShadowDecisionKind::Resumed => write!(f, "resumed"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_shadow_test_new() {
        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );
        assert_eq!(test.status, ShadowStatus::Active);
        assert_eq!(test.metrics.total_scans, 0);
        assert!(test.graduated_at.is_none());
        assert!(test.rolled_back_at.is_none());
    }

    #[test]
    fn test_shadow_test_lifecycle() {
        let mut test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        // Record some scans and matches
        for _ in 0..100 {
            test.record_scan();
        }
        assert_eq!(test.metrics.total_scans, 100);

        // Record matches with feedback
        test.record_match();
        test.record_match();
        test.record_feedback(MatchFeedback::TruePositive);
        test.record_feedback(MatchFeedback::FalsePositive);

        assert_eq!(test.metrics.true_positives, 1);
        assert_eq!(test.metrics.false_positives, 1);
        assert_eq!(test.metrics.pending_review, 0);
    }

    #[test]
    fn test_shadow_test_graduation() {
        let mut test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        test.graduate();
        assert_eq!(test.status, ShadowStatus::Graduated);
        assert!(test.graduated_at.is_some());
    }

    #[test]
    fn test_shadow_test_rollback() {
        let mut test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        test.rollback("Too many false positives".to_string());
        assert_eq!(test.status, ShadowStatus::RolledBack);
        assert!(test.rolled_back_at.is_some());
        assert_eq!(test.rollback_reason, Some("Too many false positives".to_string()));
    }

    #[test]
    fn test_shadow_metrics_fp_rate() {
        let mut metrics = ShadowMetrics::default();
        assert_eq!(metrics.fp_rate(), 0.0); // No data

        metrics.true_positives = 9;
        metrics.false_positives = 1;
        assert!((metrics.fp_rate() - 0.1).abs() < 0.001); // 10% FP rate

        metrics.true_positives = 19;
        metrics.false_positives = 1;
        assert!((metrics.fp_rate() - 0.05).abs() < 0.001); // 5% FP rate
    }

    #[test]
    fn test_shadow_metrics_graduation_criteria() {
        let mut metrics = ShadowMetrics::default();

        // Not enough scans
        assert!(!metrics.meets_graduation_criteria(100, 0.05));

        metrics.total_scans = 100;
        // No reviews yet
        assert!(!metrics.meets_graduation_criteria(100, 0.05));

        metrics.true_positives = 19;
        metrics.false_positives = 1;
        // Now meets criteria (100 scans, 5% FP rate)
        assert!(metrics.meets_graduation_criteria(100, 0.05));

        metrics.false_positives = 2;
        // FP rate too high (9.5%)
        assert!(!metrics.meets_graduation_criteria(100, 0.05));
    }

    #[test]
    fn test_shadow_match_lifecycle() {
        let mut match_ = ShadowMatch::new(
            Uuid::new_v4(),
            PathBuf::from("/path/to/file.py"),
            42,
            "verify_ssl = false".to_string(),
            "def connect():\n    verify_ssl = false".to_string(),
        );

        assert!(!match_.is_reviewed());
        assert!(match_.feedback.is_none());

        match_.mark_true_positive();
        assert!(match_.is_reviewed());
        assert_eq!(match_.feedback, Some(MatchFeedback::TruePositive));
    }

    #[test]
    fn test_shadow_decision_creation() {
        let test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        let decision = ShadowDecision::entered_shadow(&test);
        assert_eq!(decision.kind, ShadowDecisionKind::EnteredShadow);
        assert_eq!(decision.test_id, test.id);
    }

    #[test]
    fn test_exceeds_rollback_threshold_respects_min_samples() {
        let mut test = ShadowTest::new(
            "test_extractor".to_string(),
            PathBuf::from("/path/to/extractor.yaml"),
            Uuid::new_v4(),
        );

        // Set up high FP rate with only 5 samples
        test.metrics.true_positives = 2;
        test.metrics.false_positives = 3; // 60% FP rate

        // Default config requires 10 samples
        let config = ShadowConfig::default();
        assert!(
            !test.exceeds_rollback_threshold(&config),
            "Should not trigger with only 5 samples"
        );

        // Custom config with 5 min samples should trigger
        let low_threshold_config = ShadowConfig {
            min_rollback_samples: 5,
            rollback_threshold: 0.50,
            ..Default::default()
        };
        assert!(
            test.exceeds_rollback_threshold(&low_threshold_config),
            "Should trigger with 5 samples when min is 5"
        );

        // Add more samples to reach default threshold
        test.metrics.true_positives = 4;
        test.metrics.false_positives = 6; // Still 60% FP rate, now 10 samples
        assert!(test.exceeds_rollback_threshold(&config), "Should trigger with 10 samples");
    }
}
