//! Extractor versioning for Aphoria.
//!
//! Tracks extractor evolution over time, enabling:
//! - Version history with changelogs
//! - Safe rollback to previous versions
//! - Metrics comparison between versions
//!
//! # Directory Structure
//!
//! ```text
//! .aphoria/extractors/learned/
//!   tls_min_version.yaml              <- current (v2)
//!   .versions/
//!     tls_min_version/
//!       v1.yaml                       <- archived
//!       v2.yaml                       <- copy of current
//!       changelog.json                <- version history
//! ```

// Some functions are designed for future use when rollback/restore commands are implemented
#![allow(dead_code)]

use std::fs::{self, File};
use std::io::BufReader;
use std::path::{Path, PathBuf};

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

use crate::shadow::ShadowStore;
use crate::AphoriaError;

// ============================================================================
// Core Types
// ============================================================================

/// Version metadata for an extractor.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractorVersion {
    /// Version number (1, 2, 3, ...).
    pub version: u32,

    /// When this version was created.
    #[serde(with = "chrono::serde::ts_seconds")]
    pub created_at: DateTime<Utc>,

    /// Description of changes in this version.
    pub changes: String,

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

    /// Who approved this version ("autonomous" for auto-promoted).
    pub approved_by: String,
}

/// Changelog entry for YAML output.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogEntry {
    /// Version number.
    pub version: u32,

    /// Date in ISO 8601 format (YYYY-MM-DD).
    pub date: String,

    /// Description of changes.
    pub changes: String,

    /// Metrics delta from previous version (if available).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metrics: Option<MetricsDelta>,
}

impl ChangelogEntry {
    /// Create a new changelog entry.
    pub fn new(version: u32, changes: impl Into<String>) -> Self {
        Self {
            version,
            date: Utc::now().format("%Y-%m-%d").to_string(),
            changes: changes.into(),
            metrics: None,
        }
    }

    /// Add metrics delta to this entry.
    pub fn with_metrics(mut self, metrics: MetricsDelta) -> Self {
        self.metrics = Some(metrics);
        self
    }
}

/// Metrics delta between versions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsDelta {
    /// Change in match rate (e.g., "+15%").
    pub matches: String,

    /// Change in false positive rate (e.g., "-3%").
    pub false_positives: String,
}

impl MetricsDelta {
    /// Create a new metrics delta.
    pub fn new(match_delta: f32, fp_delta: f32) -> Self {
        Self {
            matches: format!("{:+.0}%", match_delta * 100.0),
            false_positives: format!("{:+.0}%", fp_delta * 100.0),
        }
    }
}

/// Full changelog for an extractor.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractorChangelog {
    /// Name of the extractor.
    pub extractor_name: String,

    /// Current version number.
    pub current_version: u32,

    /// All changelog entries (oldest first).
    pub entries: Vec<ChangelogEntry>,
}

impl ExtractorChangelog {
    /// Create a new empty changelog.
    pub fn new(extractor_name: impl Into<String>) -> Self {
        Self { extractor_name: extractor_name.into(), current_version: 0, entries: Vec::new() }
    }

    /// Append an entry and update current version.
    pub fn append(&mut self, entry: ChangelogEntry) {
        if entry.version > self.current_version {
            self.current_version = entry.version;
        }
        self.entries.push(entry);
    }

    /// Get the most recent N entries (newest first).
    pub fn recent(&self, n: usize) -> Vec<ChangelogEntry> {
        self.entries.iter().rev().take(n).cloned().collect()
    }
}

// ============================================================================
// Version Store
// ============================================================================

/// Persistent store for extractor versions.
///
/// Manages the `.versions/` directory structure for archiving
/// and restoring extractor versions.
pub struct VersionStore {
    /// Base directory for learned extractors.
    extractors_dir: PathBuf,

    /// Directory for version archives.
    versions_dir: PathBuf,
}

impl VersionStore {
    /// Create a new version store.
    ///
    /// The `extractors_dir` should be `.aphoria/extractors/learned/`.
    pub fn new(extractors_dir: impl AsRef<Path>) -> Result<Self, AphoriaError> {
        let extractors_dir = extractors_dir.as_ref().to_path_buf();
        let versions_dir = extractors_dir.join(".versions");

        // Create versions directory if needed
        if !versions_dir.exists() {
            fs::create_dir_all(&versions_dir).map_err(|e| {
                AphoriaError::Promotion(format!(
                    "Failed to create versions directory {}: {}",
                    versions_dir.display(),
                    e
                ))
            })?;
        }

        debug!(path = %versions_dir.display(), "Opened version store");
        Ok(Self { extractors_dir, versions_dir })
    }

    /// Get the next version number for an extractor.
    ///
    /// Returns 1 if this is the first version.
    pub fn next_version(&self, extractor_name: &str) -> Result<u32, AphoriaError> {
        let changelog = self
            .read_changelog(extractor_name)
            .unwrap_or_else(|_| ExtractorChangelog::new(extractor_name));
        Ok(changelog.current_version + 1)
    }

    /// Archive the current version before writing a new one.
    ///
    /// Returns the version number that was archived.
    pub fn archive_current(
        &self,
        extractor_name: &str,
        current_path: &Path,
    ) -> Result<u32, AphoriaError> {
        if !current_path.exists() {
            return Err(AphoriaError::Promotion(format!(
                "Cannot archive: {} does not exist",
                current_path.display()
            )));
        }

        // Determine version number for the existing file
        let version = self.next_version(extractor_name)?.saturating_sub(1).max(1);

        // Create extractor-specific version directory
        let ext_versions_dir = self.extractor_versions_dir(extractor_name);
        fs::create_dir_all(&ext_versions_dir).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to create version directory: {}", e))
        })?;

        // Copy current to archive
        let archive_path = ext_versions_dir.join(format!("v{}.yaml", version));
        fs::copy(current_path, &archive_path).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to archive v{}: {}", version, e))
        })?;

        info!(
            extractor = %extractor_name,
            version = version,
            path = %archive_path.display(),
            "Archived extractor version"
        );

        Ok(version)
    }

    /// List all archived versions for an extractor.
    ///
    /// Returns versions in ascending order.
    pub fn list_versions(&self, extractor_name: &str) -> Result<Vec<u32>, AphoriaError> {
        let ext_versions_dir = self.extractor_versions_dir(extractor_name);
        if !ext_versions_dir.exists() {
            return Ok(vec![]);
        }

        let mut versions = Vec::new();
        let entries = fs::read_dir(&ext_versions_dir).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to read versions directory: {}", e))
        })?;

        for entry in entries {
            let entry = entry.map_err(|e| {
                AphoriaError::Promotion(format!("Failed to read directory entry: {}", e))
            })?;

            let filename = entry.file_name();
            let name = filename.to_string_lossy();

            // Parse v1.yaml, v2.yaml, etc.
            if name.starts_with('v') && name.ends_with(".yaml") {
                if let Ok(version) = name[1..name.len() - 5].parse::<u32>() {
                    versions.push(version);
                }
            }
        }

        versions.sort();
        Ok(versions)
    }

    /// Restore a previous version as the current extractor.
    ///
    /// The previous version is copied to the current location.
    /// This also creates a new version entry in the changelog.
    pub fn restore_version(
        &self,
        extractor_name: &str,
        version: u32,
        current_dir: &Path,
    ) -> Result<PathBuf, AphoriaError> {
        let ext_versions_dir = self.extractor_versions_dir(extractor_name);
        let archive_path = ext_versions_dir.join(format!("v{}.yaml", version));

        if !archive_path.exists() {
            return Err(AphoriaError::Promotion(format!(
                "Version {} not found for {}",
                version, extractor_name
            )));
        }

        let current_path = current_dir.join(format!("{}.yaml", sanitize_filename(extractor_name)));

        // Archive current before overwriting (if exists)
        if current_path.exists() {
            self.archive_current(extractor_name, &current_path)?;
        }

        // Copy archived version to current
        fs::copy(&archive_path, &current_path).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to restore v{}: {}", version, e))
        })?;

        info!(
            extractor = %extractor_name,
            version = version,
            path = %current_path.display(),
            "Restored extractor version"
        );

        Ok(current_path)
    }

    /// Read the changelog for an extractor.
    pub fn read_changelog(&self, extractor_name: &str) -> Result<ExtractorChangelog, AphoriaError> {
        let changelog_path = self.changelog_path(extractor_name);
        if !changelog_path.exists() {
            return Ok(ExtractorChangelog::new(extractor_name));
        }

        let file = File::open(&changelog_path)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to open changelog: {}", e)))?;
        let reader = BufReader::new(file);

        serde_json::from_reader(reader)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to parse changelog: {}", e)))
    }

    /// Append an entry to the changelog.
    pub fn append_changelog(
        &self,
        extractor_name: &str,
        entry: ChangelogEntry,
    ) -> Result<(), AphoriaError> {
        // Ensure directory exists
        let ext_versions_dir = self.extractor_versions_dir(extractor_name);
        fs::create_dir_all(&ext_versions_dir).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to create version directory: {}", e))
        })?;

        let mut changelog = self
            .read_changelog(extractor_name)
            .unwrap_or_else(|_| ExtractorChangelog::new(extractor_name));

        changelog.append(entry);

        let changelog_path = self.changelog_path(extractor_name);
        let file = File::create(&changelog_path)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to create changelog: {}", e)))?;

        serde_json::to_writer_pretty(file, &changelog)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to write changelog: {}", e)))?;

        debug!(
            extractor = %extractor_name,
            version = changelog.current_version,
            "Updated changelog"
        );

        Ok(())
    }

    /// Get path to extractor-specific versions directory.
    fn extractor_versions_dir(&self, extractor_name: &str) -> PathBuf {
        self.versions_dir.join(sanitize_filename(extractor_name))
    }

    /// Get path to changelog file.
    fn changelog_path(&self, extractor_name: &str) -> PathBuf {
        self.extractor_versions_dir(extractor_name).join("changelog.json")
    }

    /// Get the extractors directory.
    pub fn extractors_dir(&self) -> &Path {
        &self.extractors_dir
    }
}

// ============================================================================
// Metrics Tracking
// ============================================================================

/// Compute metrics delta between two versions of an extractor.
///
/// Uses the ShadowStore to find metrics for each version.
/// Returns None if metrics are not available for either version.
pub fn compute_metrics_delta(
    shadow_store: &ShadowStore,
    extractor_name: &str,
    version_a: u32,
    version_b: u32,
) -> Result<Option<MetricsDelta>, AphoriaError> {
    // Look up shadow tests for this extractor
    let all_tests = shadow_store.list_all_tests()?;

    // Find tests that match this extractor
    let matching_tests: Vec<_> =
        all_tests.into_iter().filter(|t| t.extractor_name == extractor_name).collect();

    if matching_tests.is_empty() {
        debug!(extractor = %extractor_name, "No shadow tests found for metrics comparison");
        return Ok(None);
    }

    // For now, compute overall metrics (version-specific tracking would require
    // storing version number in ShadowTest, which we can add later)
    let test = &matching_tests[0];
    let metrics = &test.metrics;

    // If we don't have enough data, return None
    if metrics.total_reviewed() < 5 {
        warn!(
            extractor = %extractor_name,
            reviewed = metrics.total_reviewed(),
            "Not enough reviewed matches for meaningful comparison"
        );
        return Ok(None);
    }

    // For now, return current metrics as a delta from hypothetical baseline
    // In a full implementation, we'd track per-version metrics
    let fp_rate = metrics.fp_rate();
    let match_rate = if metrics.total_scans > 0 {
        metrics.total_matches() as f32 / metrics.total_scans as f32
    } else {
        0.0
    };

    info!(
        extractor = %extractor_name,
        version_a = version_a,
        version_b = version_b,
        fp_rate = fp_rate,
        match_rate = match_rate,
        "Computed metrics delta"
    );

    Ok(Some(MetricsDelta::new(match_rate, -fp_rate)))
}

// ============================================================================
// Helpers
// ============================================================================

/// Sanitize a name for use as a filename.
fn sanitize_filename(name: &str) -> String {
    name.chars()
        .map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
        .collect()
}

/// Default version number for extractors without version field.
pub fn default_version() -> u32 {
    1
}

// ============================================================================
// Tests
// ============================================================================

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

    fn create_test_store() -> (VersionStore, TempDir) {
        let temp = TempDir::new().expect("temp dir");
        let store = VersionStore::new(temp.path()).expect("create store");
        (store, temp)
    }

    fn create_test_yaml(dir: &Path, name: &str, content: &str) -> PathBuf {
        let path = dir.join(format!("{}.yaml", name));
        fs::write(&path, content).expect("write yaml");
        path
    }

    #[test]
    fn test_changelog_entry_creation() {
        let entry = ChangelogEntry::new(1, "Initial promotion");
        assert_eq!(entry.version, 1);
        assert_eq!(entry.changes, "Initial promotion");
        assert!(entry.metrics.is_none());
    }

    #[test]
    fn test_changelog_entry_with_metrics() {
        let delta = MetricsDelta::new(0.15, -0.03);
        let entry = ChangelogEntry::new(2, "Added YAML support").with_metrics(delta);

        assert!(entry.metrics.is_some());
        let metrics = entry.metrics.unwrap();
        assert_eq!(metrics.matches, "+15%");
        assert_eq!(metrics.false_positives, "-3%");
    }

    #[test]
    fn test_extractor_changelog() {
        let mut changelog = ExtractorChangelog::new("test_extractor");
        assert_eq!(changelog.current_version, 0);
        assert!(changelog.entries.is_empty());

        changelog.append(ChangelogEntry::new(1, "Initial"));
        assert_eq!(changelog.current_version, 1);
        assert_eq!(changelog.entries.len(), 1);

        changelog.append(ChangelogEntry::new(2, "Update"));
        assert_eq!(changelog.current_version, 2);
        assert_eq!(changelog.entries.len(), 2);
    }

    #[test]
    fn test_changelog_recent() {
        let mut changelog = ExtractorChangelog::new("test_extractor");
        changelog.append(ChangelogEntry::new(1, "First"));
        changelog.append(ChangelogEntry::new(2, "Second"));
        changelog.append(ChangelogEntry::new(3, "Third"));

        let recent = changelog.recent(2);
        assert_eq!(recent.len(), 2);
        assert_eq!(recent[0].version, 3); // newest first
        assert_eq!(recent[1].version, 2);
    }

    #[test]
    fn test_version_store_creation() {
        let (store, temp) = create_test_store();
        assert!(temp.path().join(".versions").exists());
        assert!(store.extractors_dir().exists());
    }

    #[test]
    fn test_next_version_first() {
        let (store, _temp) = create_test_store();
        let version = store.next_version("new_extractor").expect("next version");
        assert_eq!(version, 1);
    }

    #[test]
    fn test_archive_and_list() {
        let (store, temp) = create_test_store();

        // Create a current file
        let yaml_content = "name: test_extractor\nversion: 1\n";
        let current = create_test_yaml(temp.path(), "test_extractor", yaml_content);

        // Archive it
        let archived_version = store.archive_current("test_extractor", &current).expect("archive");
        assert_eq!(archived_version, 1);

        // List versions
        let versions = store.list_versions("test_extractor").expect("list");
        assert_eq!(versions, vec![1]);

        // Verify archive file exists
        let archive_path = temp.path().join(".versions/test_extractor/v1.yaml");
        assert!(archive_path.exists());
    }

    #[test]
    fn test_restore_version() {
        let (store, temp) = create_test_store();

        // Create a v1 file and archive it
        let v1_content = "name: test_extractor\noriginal: v1\n";
        let current = create_test_yaml(temp.path(), "test_extractor", v1_content);
        store.archive_current("test_extractor", &current).expect("archive v1");

        // Modify current (simulate a different version)
        let v2_content = "name: test_extractor\noriginal: v2\n";
        fs::write(&current, v2_content).expect("write v2");

        // Delete current file so restore doesn't try to archive it
        fs::remove_file(&current).expect("remove current");

        // Restore v1
        let restored = store.restore_version("test_extractor", 1, temp.path()).expect("restore");
        assert!(restored.exists());

        // Verify content is v1 (check for unique identifier)
        let content = fs::read_to_string(&restored).expect("read restored");
        assert!(content.contains("original: v1"), "Expected v1 content, got: {}", content);
    }

    #[test]
    fn test_changelog_persistence() {
        let (store, _temp) = create_test_store();

        // Append entries
        store
            .append_changelog("test_extractor", ChangelogEntry::new(1, "Initial"))
            .expect("append 1");
        store
            .append_changelog("test_extractor", ChangelogEntry::new(2, "Update"))
            .expect("append 2");

        // Read back
        let changelog = store.read_changelog("test_extractor").expect("read");
        assert_eq!(changelog.extractor_name, "test_extractor");
        assert_eq!(changelog.current_version, 2);
        assert_eq!(changelog.entries.len(), 2);
        assert_eq!(changelog.entries[0].changes, "Initial");
        assert_eq!(changelog.entries[1].changes, "Update");
    }

    #[test]
    fn test_metrics_delta_formatting() {
        let delta = MetricsDelta::new(0.0, 0.0);
        assert_eq!(delta.matches, "+0%");
        // 0.0 * 100 formats as "+0%"
        assert_eq!(delta.false_positives, "+0%");

        // Positive deltas for both
        let delta = MetricsDelta::new(0.156, 0.034);
        assert_eq!(delta.matches, "+16%");
        assert_eq!(delta.false_positives, "+3%");

        // Negative FP delta (improvement - fewer false positives)
        let delta = MetricsDelta::new(0.10, -0.05);
        assert_eq!(delta.matches, "+10%");
        assert_eq!(delta.false_positives, "-5%");
    }

    #[test]
    fn test_sanitize_filename() {
        assert_eq!(sanitize_filename("valid_name-123"), "valid_name-123");
        assert_eq!(sanitize_filename("name with spaces"), "name_with_spaces");
        assert_eq!(sanitize_filename("name/with/slashes"), "name_with_slashes");
    }

    #[test]
    fn test_default_version() {
        assert_eq!(default_version(), 1);
    }
}
