//! Audit logging for autonomous promotion decisions.
//!
//! Every autonomous decision (promoted or not) is logged to a JSONL file
//! for compliance, debugging, and review.

use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
use uuid::Uuid;

use super::types::PromotionCandidate;
use crate::config::AutonomousConfig;
use crate::AphoriaError;

/// Outcome of an autonomous promotion decision.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionOutcome {
    /// Pattern was auto-promoted (no human review required).
    AutoPromoted,
    /// Pattern requires human review (did not meet thresholds).
    RequiresReview,
    /// Autonomous promotion is disabled (kill switch off).
    Disabled,
}

/// Thresholds that were applied to make the decision.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppliedThresholds {
    /// Whether autonomous promotion was enabled.
    pub enabled: bool,
    /// Minimum confidence threshold applied.
    pub min_confidence: f32,
    /// Minimum project count threshold applied.
    pub min_projects: usize,
    /// Whether zero failures was required.
    pub require_zero_failures: bool,
    /// Whether zero warnings was required.
    pub require_zero_warnings: bool,
}

impl From<&AutonomousConfig> for AppliedThresholds {
    fn from(config: &AutonomousConfig) -> Self {
        Self {
            enabled: config.enabled,
            min_confidence: config.min_confidence,
            min_projects: config.min_projects,
            require_zero_failures: config.require_zero_failures,
            require_zero_warnings: config.require_zero_warnings,
        }
    }
}

/// Actual values from the pattern being evaluated.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternValues {
    /// Pattern's average confidence.
    pub confidence: f32,
    /// Number of projects where pattern was observed.
    pub project_count: usize,
    /// Total occurrences across all projects.
    pub occurrences: u32,
}

/// Validation state at the time of decision.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationState {
    /// Whether validation passed.
    pub passed: bool,
    /// Whether performance was acceptable.
    pub performance_ok: bool,
    /// Number of positive test failures.
    pub failure_count: usize,
    /// Number of validation warnings.
    pub warning_count: usize,
    /// Whether false positive warning was set.
    pub false_positive_warning: bool,
    /// Whether performance warning was set.
    pub performance_warning: bool,
}

/// An autonomous decision record for audit.
///
/// Contains all information needed to understand why a pattern
/// was or was not auto-promoted.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutonomousDecision {
    /// Unique ID for this decision record.
    pub id: Uuid,

    /// When this decision was made.
    #[serde(with = "chrono::serde::ts_seconds")]
    pub timestamp: DateTime<Utc>,

    /// ID of the pattern being evaluated.
    pub pattern_id: Uuid,

    /// The normalized pattern string.
    pub normalized_pattern: String,

    /// Outcome of the decision.
    pub decision: DecisionOutcome,

    /// Thresholds that were applied.
    pub thresholds: AppliedThresholds,

    /// Actual values from the pattern.
    pub pattern_values: PatternValues,

    /// Validation state at decision time.
    pub validation_state: ValidationState,

    /// List of reasons why auto-promotion was blocked (empty if promoted).
    pub blockers: Vec<String>,

    /// Path to YAML file if promoted.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_path: Option<PathBuf>,
}

impl AutonomousDecision {
    /// Create a decision record from a candidate and config.
    pub fn create(
        candidate: &PromotionCandidate,
        config: &AutonomousConfig,
        decision: DecisionOutcome,
        output_path: Option<PathBuf>,
    ) -> Self {
        let blockers = if decision == DecisionOutcome::AutoPromoted {
            vec![]
        } else {
            candidate.auto_promotion_blockers(config)
        };

        Self {
            id: Uuid::new_v4(),
            timestamp: Utc::now(),
            pattern_id: candidate.pattern.id,
            normalized_pattern: candidate.pattern.normalized_pattern.clone(),
            decision,
            thresholds: AppliedThresholds::from(config),
            pattern_values: PatternValues {
                confidence: candidate.pattern.avg_confidence,
                project_count: candidate.pattern.project_count(),
                occurrences: candidate.pattern.occurrences,
            },
            validation_state: ValidationState {
                passed: candidate.validation.passed,
                performance_ok: candidate.validation.performance_ok,
                failure_count: candidate.validation.positive_failures.len(),
                warning_count: candidate.validation.warnings.len(),
                false_positive_warning: candidate.validation.false_positive_warning,
                performance_warning: candidate.validation.performance_warning,
            },
            blockers,
            output_path,
        }
    }
}

/// Logger for autonomous promotion decisions.
///
/// Writes decisions to a JSONL file for compliance and audit trail.
pub struct AutonomousAuditLog {
    /// Path to the JSONL log file.
    log_path: PathBuf,
}

impl AutonomousAuditLog {
    /// Create a new audit log.
    ///
    /// Creates the audit directory if it doesn't exist.
    pub fn new(audit_dir: Option<&PathBuf>) -> Result<Self, AphoriaError> {
        let dir = if let Some(d) = audit_dir {
            d.clone()
        } else if let Some(home) = dirs::home_dir() {
            home.join(".aphoria").join("audit")
        } else {
            PathBuf::from(".aphoria/audit")
        };

        // Create directory if needed
        if !dir.exists() {
            fs::create_dir_all(&dir).map_err(|e| {
                AphoriaError::Promotion(format!(
                    "Failed to create audit directory {}: {}",
                    dir.display(),
                    e
                ))
            })?;
            debug!(path = %dir.display(), "Created audit directory");
        }

        let log_path = dir.join("autonomous-decisions.jsonl");
        Ok(Self { log_path })
    }

    /// Record a decision to the audit log.
    pub fn record(&self, decision: &AutonomousDecision) -> Result<(), AphoriaError> {
        let json = serde_json::to_string(decision)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to serialize decision: {}", e)))?;

        let mut file =
            OpenOptions::new().create(true).append(true).open(&self.log_path).map_err(|e| {
                AphoriaError::Promotion(format!(
                    "Failed to open audit log {}: {}",
                    self.log_path.display(),
                    e
                ))
            })?;

        writeln!(file, "{}", json).map_err(|e| {
            AphoriaError::Promotion(format!(
                "Failed to write to audit log {}: {}",
                self.log_path.display(),
                e
            ))
        })?;

        debug!(
            decision_id = %decision.id,
            pattern_id = %decision.pattern_id,
            outcome = ?decision.decision,
            "Recorded autonomous decision"
        );

        Ok(())
    }

    /// Record an auto-promoted decision.
    pub fn record_promoted(
        &self,
        candidate: &PromotionCandidate,
        config: &AutonomousConfig,
        output_path: PathBuf,
    ) -> Result<Uuid, AphoriaError> {
        let decision = AutonomousDecision::create(
            candidate,
            config,
            DecisionOutcome::AutoPromoted,
            Some(output_path),
        );
        let id = decision.id;
        self.record(&decision)?;

        info!(
            decision_id = %id,
            pattern_id = %candidate.pattern.id,
            "Auto-promoted pattern (logged to audit)"
        );

        Ok(id)
    }

    /// Record a decision that requires human review.
    pub fn record_requires_review(
        &self,
        candidate: &PromotionCandidate,
        config: &AutonomousConfig,
    ) -> Result<Uuid, AphoriaError> {
        let decision =
            AutonomousDecision::create(candidate, config, DecisionOutcome::RequiresReview, None);
        let id = decision.id;
        self.record(&decision)?;

        debug!(
            decision_id = %id,
            pattern_id = %candidate.pattern.id,
            blockers = ?decision.blockers,
            "Pattern requires review (logged to audit)"
        );

        Ok(id)
    }

    /// Record a decision when autonomous promotion is disabled.
    pub fn record_disabled(
        &self,
        candidate: &PromotionCandidate,
        config: &AutonomousConfig,
    ) -> Result<Uuid, AphoriaError> {
        let decision =
            AutonomousDecision::create(candidate, config, DecisionOutcome::Disabled, None);
        let id = decision.id;
        self.record(&decision)?;
        Ok(id)
    }

    /// Get the path to the audit log file.
    pub fn log_path(&self) -> &Path {
        &self.log_path
    }

    /// Read all decisions from the audit log.
    ///
    /// Returns decisions in order they were written.
    pub fn read_all(&self) -> Result<Vec<AutonomousDecision>, AphoriaError> {
        if !self.log_path.exists() {
            return Ok(vec![]);
        }

        let content = fs::read_to_string(&self.log_path).map_err(|e| {
            AphoriaError::Promotion(format!(
                "Failed to read audit log {}: {}",
                self.log_path.display(),
                e
            ))
        })?;

        let mut decisions = Vec::new();
        for (line_num, line) in content.lines().enumerate() {
            if line.trim().is_empty() {
                continue;
            }
            match serde_json::from_str::<AutonomousDecision>(line) {
                Ok(decision) => decisions.push(decision),
                Err(e) => {
                    warn!(
                        line = line_num + 1,
                        error = %e,
                        "Skipping malformed audit log entry"
                    );
                }
            }
        }

        Ok(decisions)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::extractors::{DeclarativeClaimDef, DeclarativeExtractorDef, DeclarativeValue};
    use crate::learning::{ClaimTemplate, LearnedPattern, ValueType};
    use crate::promotion::ValidationResult;
    use crate::types::Language;
    use tempfile::TempDir;

    fn create_test_pattern() -> LearnedPattern {
        let mut pattern = LearnedPattern::new(
            "verify_ssl = false",
            "verify_ssl = <boolean>",
            ClaimTemplate::new("ssl/verify", "enabled", ValueType::Boolean, "SSL verification"),
            Language::Python,
            "project1",
            0.97,
        );
        for i in 2..=12 {
            pattern.record_observation(format!("project{}", i), 0.96, Utc::now());
        }
        pattern
    }

    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*(?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.96,
            source: None,
        }
    }

    fn create_test_candidate() -> PromotionCandidate {
        PromotionCandidate::new(
            create_test_pattern(),
            create_test_extractor(),
            ValidationResult::success(vec!["match".to_string()], 10, 50),
        )
    }

    #[test]
    fn test_audit_log_creation() {
        let temp = TempDir::new().expect("temp dir");
        let log = AutonomousAuditLog::new(Some(&temp.path().to_path_buf())).expect("create log");
        assert!(log.log_path().parent().expect("parent").exists());
    }

    #[test]
    fn test_record_promoted() {
        let temp = TempDir::new().expect("temp dir");
        let log = AutonomousAuditLog::new(Some(&temp.path().to_path_buf())).expect("create log");

        let candidate = create_test_candidate();
        let config = AutonomousConfig {
            enabled: true,
            min_confidence: 0.95,
            min_projects: 10,
            ..Default::default()
        };

        let id =
            log.record_promoted(&candidate, &config, PathBuf::from("test.yaml")).expect("record");
        assert!(!id.is_nil());

        // Read back
        let decisions = log.read_all().expect("read");
        assert_eq!(decisions.len(), 1);
        assert_eq!(decisions[0].decision, DecisionOutcome::AutoPromoted);
        assert!(decisions[0].blockers.is_empty());
        assert!(decisions[0].output_path.is_some());
    }

    #[test]
    fn test_record_requires_review() {
        let temp = TempDir::new().expect("temp dir");
        let log = AutonomousAuditLog::new(Some(&temp.path().to_path_buf())).expect("create log");

        // Create candidate that doesn't meet thresholds
        let pattern = LearnedPattern::new(
            "test = true",
            "test = <boolean>",
            ClaimTemplate::new("test", "value", ValueType::Boolean, "Test"),
            Language::Rust,
            "project1",
            0.8,
        );
        let candidate = PromotionCandidate::new(
            pattern,
            create_test_extractor(),
            ValidationResult::success(vec!["match".to_string()], 10, 50),
        );

        let config = AutonomousConfig {
            enabled: true,
            min_confidence: 0.95,
            min_projects: 10,
            ..Default::default()
        };

        let id = log.record_requires_review(&candidate, &config).expect("record");
        assert!(!id.is_nil());

        let decisions = log.read_all().expect("read");
        assert_eq!(decisions.len(), 1);
        assert_eq!(decisions[0].decision, DecisionOutcome::RequiresReview);
        assert!(!decisions[0].blockers.is_empty());
    }

    #[test]
    fn test_record_disabled() {
        let temp = TempDir::new().expect("temp dir");
        let log = AutonomousAuditLog::new(Some(&temp.path().to_path_buf())).expect("create log");

        let candidate = create_test_candidate();
        let config = AutonomousConfig { enabled: false, ..Default::default() };

        let id = log.record_disabled(&candidate, &config).expect("record");
        assert!(!id.is_nil());

        let decisions = log.read_all().expect("read");
        assert_eq!(decisions.len(), 1);
        assert_eq!(decisions[0].decision, DecisionOutcome::Disabled);
    }

    #[test]
    fn test_multiple_records() {
        let temp = TempDir::new().expect("temp dir");
        let log = AutonomousAuditLog::new(Some(&temp.path().to_path_buf())).expect("create log");

        let candidate = create_test_candidate();
        let config = AutonomousConfig {
            enabled: true,
            min_confidence: 0.95,
            min_projects: 10,
            ..Default::default()
        };

        log.record_promoted(&candidate, &config, PathBuf::from("a.yaml")).expect("record");
        log.record_promoted(&candidate, &config, PathBuf::from("b.yaml")).expect("record");
        log.record_requires_review(&candidate, &config).expect("record");

        let decisions = log.read_all().expect("read");
        assert_eq!(decisions.len(), 3);
    }

    #[test]
    fn test_decision_serialization() {
        let candidate = create_test_candidate();
        let config = AutonomousConfig {
            enabled: true,
            min_confidence: 0.95,
            min_projects: 10,
            ..Default::default()
        };

        let decision = AutonomousDecision::create(
            &candidate,
            &config,
            DecisionOutcome::AutoPromoted,
            Some(PathBuf::from("test.yaml")),
        );

        let json = serde_json::to_string(&decision).expect("serialize");
        let parsed: AutonomousDecision = serde_json::from_str(&json).expect("deserialize");

        assert_eq!(parsed.id, decision.id);
        assert_eq!(parsed.pattern_id, decision.pattern_id);
        assert_eq!(parsed.decision, decision.decision);
    }

    #[test]
    fn test_applied_thresholds() {
        let config = AutonomousConfig {
            enabled: true,
            min_confidence: 0.98,
            min_projects: 15,
            require_zero_failures: false,
            require_zero_warnings: true,
            audit_log: true,
            audit_dir: None,
        };

        let thresholds = AppliedThresholds::from(&config);
        assert!(thresholds.enabled);
        assert!((thresholds.min_confidence - 0.98).abs() < 0.001);
        assert_eq!(thresholds.min_projects, 15);
        assert!(!thresholds.require_zero_failures);
        assert!(thresholds.require_zero_warnings);
    }
}
