//! YAML writer for promoted extractors.
//!
//! Writes validated extractors to YAML files in `.aphoria/extractors/learned/`.
//! Supports versioning with automatic archiving and changelog tracking.

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

use chrono::Utc;
use serde::Serialize;
use tracing::{debug, info};

use super::types::PromotionMetadata;
use super::version::{ChangelogEntry, VersionStore};
use crate::extractors::{DeclarativeExtractorDef, DeclarativeValue};
use crate::learning::LearnedPattern;

// Note: DeclarativeClaimDef was removed as it's now defined inline within DeclarativeExtractorDef
use crate::AphoriaError;

/// YAML-serializable extractor definition.
///
/// This is a separate struct from `DeclarativeExtractorDef` to control
/// the YAML output format and include promotion metadata.
#[derive(Debug, Serialize)]
struct YamlExtractor {
    /// Unique name for this extractor.
    name: String,

    /// Human-readable description.
    description: String,

    /// Version number of this extractor.
    version: u32,

    /// Previous version number (if this is an update).
    #[serde(skip_serializing_if = "Option::is_none")]
    previous_version: Option<u32>,

    /// Languages this extractor applies to.
    languages: Vec<String>,

    /// Regex pattern to match.
    pattern: String,

    /// Claim configuration.
    claim: YamlClaim,

    /// Confidence score (0.0-1.0).
    confidence: f32,

    /// Promotion metadata for traceability.
    metadata: PromotionMetadata,

    /// Inline changelog (last 3 entries, newest first).
    #[serde(skip_serializing_if = "Vec::is_empty")]
    changelog: Vec<ChangelogEntry>,
}

/// YAML-serializable claim definition.
#[derive(Debug, Serialize)]
struct YamlClaim {
    /// Subject path.
    subject: String,

    /// Predicate.
    predicate: String,

    /// Value specification.
    #[serde(flatten)]
    value: YamlValue,
}

/// YAML-serializable value specification.
#[derive(Debug, Serialize)]
#[serde(untagged)]
enum YamlValue {
    /// Use matched text as value.
    MatchedText { value_from_match: bool },
    /// Boolean value.
    BoolValue { value: bool },
    /// Text value.
    TextValue { value: String },
}

impl From<&DeclarativeValue> for YamlValue {
    fn from(value: &DeclarativeValue) -> Self {
        match value {
            DeclarativeValue::MatchedText { value_from_match } => {
                YamlValue::MatchedText { value_from_match: *value_from_match }
            }
            DeclarativeValue::Boolean { value } => YamlValue::BoolValue { value: *value },
            DeclarativeValue::Text { value } => YamlValue::TextValue { value: value.clone() },
        }
    }
}

/// Writes promoted extractors to YAML files.
pub struct YamlWriter {
    /// Output directory for YAML files.
    output_dir: PathBuf,
}

impl YamlWriter {
    /// Create a new YAML writer with the specified output directory.
    ///
    /// Creates the directory if it doesn't exist.
    pub fn new(output_dir: impl Into<PathBuf>) -> Result<Self, AphoriaError> {
        let output_dir = output_dir.into();

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

        Ok(Self { output_dir })
    }

    /// Get the default output directory.
    pub fn default_output_dir() -> PathBuf {
        PathBuf::from(".aphoria/extractors/learned")
    }

    /// Write an extractor to a YAML file.
    ///
    /// The filename is derived from the extractor name.
    pub fn write(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
    ) -> Result<PathBuf, AphoriaError> {
        self.write_internal(extractor, pattern, false)
    }

    /// Write an autonomously promoted extractor to a YAML file.
    ///
    /// Includes "AUTO-PROMOTED" header and "Approved by: autonomous" metadata.
    pub fn write_autonomous(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
    ) -> Result<PathBuf, AphoriaError> {
        self.write_internal(extractor, pattern, true)
    }

    /// Internal write implementation.
    fn write_internal(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
        auto_promoted: bool,
    ) -> Result<PathBuf, AphoriaError> {
        let yaml_extractor = self.to_yaml_extractor(extractor, pattern, auto_promoted);

        // Generate filename from extractor name
        let filename = format!("{}.yaml", sanitize_filename(&extractor.name));
        let path = self.output_dir.join(&filename);

        // Generate YAML content with header comment
        let yaml_content = self.generate_yaml(&yaml_extractor, pattern, auto_promoted)?;

        // Write to file
        fs::write(&path, yaml_content).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to write YAML to {}: {}", path.display(), e))
        })?;

        info!(
            path = %path.display(),
            name = %extractor.name,
            auto_promoted = auto_promoted,
            "Wrote promoted extractor"
        );

        Ok(path)
    }

    /// Convert an extractor and pattern to the YAML format.
    fn to_yaml_extractor(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
        auto_promoted: bool,
    ) -> YamlExtractor {
        let metadata = if auto_promoted {
            PromotionMetadata::from_autonomous(pattern)
        } else {
            PromotionMetadata::from_pattern(pattern)
        };

        YamlExtractor {
            name: extractor.name.clone(),
            description: extractor.description.clone(),
            version: 1,
            previous_version: None,
            languages: extractor.languages.clone(),
            pattern: extractor.pattern.clone(),
            claim: YamlClaim {
                subject: extractor.claim.subject.clone(),
                predicate: extractor.claim.predicate.clone(),
                value: (&extractor.claim.value).into(),
            },
            confidence: extractor.confidence,
            metadata,
            changelog: Vec::new(),
        }
    }

    /// Convert an extractor and pattern to the YAML format with versioning.
    fn to_yaml_extractor_versioned(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
        auto_promoted: bool,
        version: u32,
        previous_version: Option<u32>,
        changelog: Vec<ChangelogEntry>,
    ) -> YamlExtractor {
        let metadata = if auto_promoted {
            PromotionMetadata::from_autonomous_versioned(pattern, version)
        } else {
            PromotionMetadata::from_pattern_versioned(pattern, version)
        };

        YamlExtractor {
            name: extractor.name.clone(),
            description: extractor.description.clone(),
            version,
            previous_version,
            languages: extractor.languages.clone(),
            pattern: extractor.pattern.clone(),
            claim: YamlClaim {
                subject: extractor.claim.subject.clone(),
                predicate: extractor.claim.predicate.clone(),
                value: (&extractor.claim.value).into(),
            },
            confidence: extractor.confidence,
            metadata,
            changelog,
        }
    }

    /// Generate YAML content with header comment.
    fn generate_yaml(
        &self,
        extractor: &YamlExtractor,
        pattern: &LearnedPattern,
        auto_promoted: bool,
    ) -> Result<String, AphoriaError> {
        let yaml_body = serde_yaml::to_string(extractor)
            .map_err(|e| AphoriaError::Promotion(format!("Failed to serialize YAML: {}", e)))?;

        let auto_promoted_line =
            if auto_promoted { "# AUTO-PROMOTED: true (no human review)\n" } else { "" };

        let approved_by = if auto_promoted { "autonomous" } else { "human" };

        // Include version info in header
        let version_line = if let Some(prev) = extractor.previous_version {
            format!("# Version: {} (previous: {})\n", extractor.version, prev)
        } else {
            format!("# Version: {}\n", extractor.version)
        };

        let header = format!(
            "# Generated from learned pattern. Review before editing.\n\
             {}\
             # Pattern ID: {}\n\
             {}\
             # Learned from: {} projects, {} occurrences\n\
             # Confidence: {:.2}\n\
             # Promoted: {} UTC\n\
             # Approved by: {}\n\
             \n",
            auto_promoted_line,
            pattern.id,
            version_line,
            pattern.project_count(),
            pattern.occurrences,
            pattern.avg_confidence,
            Utc::now().format("%Y-%m-%d %H:%M:%S"),
            approved_by
        );

        Ok(format!("{}{}", header, yaml_body))
    }

    /// Write an extractor with automatic versioning.
    ///
    /// Archives the current version (if any) before writing the new one.
    /// Updates the changelog with the provided changes description.
    ///
    /// Returns the path to the written file and the new version number.
    pub fn write_versioned(
        &self,
        extractor: &DeclarativeExtractorDef,
        pattern: &LearnedPattern,
        changes: &str,
        auto_promoted: bool,
    ) -> Result<(PathBuf, u32), AphoriaError> {
        let version_store = VersionStore::new(&self.output_dir)?;
        let extractor_name = &extractor.name;

        // Determine version number and archive existing if present
        let path = self.path_for(extractor_name);
        let (version, previous_version) = if path.exists() {
            let prev = version_store.archive_current(extractor_name, &path)?;
            (version_store.next_version(extractor_name)?, Some(prev))
        } else {
            (1, None)
        };

        // Get changelog for inline display (last 3 entries)
        let changelog =
            version_store.read_changelog(extractor_name).map(|c| c.recent(3)).unwrap_or_default();

        // Build versioned YAML extractor
        let yaml_extractor = self.to_yaml_extractor_versioned(
            extractor,
            pattern,
            auto_promoted,
            version,
            previous_version,
            changelog,
        );

        // Generate YAML content
        let yaml_content = self.generate_yaml(&yaml_extractor, pattern, auto_promoted)?;

        // Write to file
        fs::write(&path, yaml_content).map_err(|e| {
            AphoriaError::Promotion(format!("Failed to write YAML to {}: {}", path.display(), e))
        })?;

        // Record in changelog
        version_store.append_changelog(extractor_name, ChangelogEntry::new(version, changes))?;

        info!(
            path = %path.display(),
            name = %extractor.name,
            version = version,
            auto_promoted = auto_promoted,
            "Wrote versioned extractor"
        );

        Ok((path, version))
    }

    /// Get the path where an extractor would be written.
    fn path_for(&self, extractor_name: &str) -> PathBuf {
        let filename = format!("{}.yaml", sanitize_filename(extractor_name));
        self.output_dir.join(&filename)
    }

    /// List existing YAML files in the output directory.
    pub fn list_existing(&self) -> Result<Vec<PathBuf>, AphoriaError> {
        if !self.output_dir.exists() {
            return Ok(vec![]);
        }

        let entries = fs::read_dir(&self.output_dir).map_err(|e| {
            AphoriaError::Promotion(format!(
                "Failed to read output directory {}: {}",
                self.output_dir.display(),
                e
            ))
        })?;

        let mut files = Vec::new();
        for entry in entries {
            let entry = entry.map_err(|e| {
                AphoriaError::Promotion(format!("Failed to read directory entry: {}", e))
            })?;
            let path = entry.path();
            if path.extension().is_some_and(|ext| ext == "yaml" || ext == "yml") {
                files.push(path);
            }
        }

        Ok(files)
    }

    /// Check if an extractor with the given name already exists.
    pub fn exists(&self, name: &str) -> bool {
        let filename = format!("{}.yaml", sanitize_filename(name));
        self.output_dir.join(&filename).exists()
    }

    /// Get the output directory path.
    pub fn output_dir(&self) -> &Path {
        &self.output_dir
    }
}

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

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

    fn create_test_pattern() -> LearnedPattern {
        let mut pattern = LearnedPattern::new(
            "const TLS_MIN = \"1.0\"",
            "const TLS_MIN = <string:version>",
            ClaimTemplate::new("tls/min_version", "version", ValueType::Text, "TLS version"),
            Language::Rust,
            "project1",
            0.9,
        );
        // Add more projects
        pattern.record_observation("project2", 0.85, Utc::now());
        pattern.record_observation("project3", 0.88, Utc::now());
        pattern
    }

    fn create_test_extractor() -> DeclarativeExtractorDef {
        DeclarativeExtractorDef {
            name: "learned_tls_min_version".to_string(),
            description: "TLS minimum version set to deprecated value".to_string(),
            languages: vec!["rust".to_string(), "go".to_string()],
            pattern: r#"(?i)tls_?min_?(version)?\s*[:=]\s*["\']?(?P<value>1\.[01])["\']?"#
                .to_string(),
            claim: DeclarativeClaimDef {
                subject: "tls/min_version".to_string(),
                predicate: "version".to_string(),
                value: DeclarativeValue::MatchedText { value_from_match: true },
            },
            confidence: 0.91,
            source: None,
        }
    }

    #[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");
        assert_eq!(sanitize_filename("name.with.dots"), "name_with_dots");
    }

    #[test]
    fn test_yaml_writer_creation() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");
        assert!(writer.output_dir().exists());
    }

    #[test]
    fn test_yaml_writer_creates_directory() {
        let temp = TempDir::new().expect("temp dir");
        let new_dir = temp.path().join("nested").join("dir");

        let writer = YamlWriter::new(&new_dir).expect("create writer");
        assert!(new_dir.exists());
        assert!(writer.output_dir().exists());
    }

    #[test]
    fn test_write_extractor() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        let path = writer.write(&extractor, &pattern).expect("write");

        assert!(path.exists());
        assert_eq!(path.file_name().and_then(|n| n.to_str()), Some("learned_tls_min_version.yaml"));

        let content = fs::read_to_string(&path).expect("read");
        assert!(content.contains("# Generated from learned pattern"));
        assert!(content.contains(&format!("# Pattern ID: {}", pattern.id)));
        assert!(content.contains("name: learned_tls_min_version"));
        assert!(content.contains("tls/min_version"));
        assert!(content.contains("# Approved by: human"));
        // Should NOT be auto-promoted
        assert!(!content.contains("AUTO-PROMOTED"));
    }

    #[test]
    fn test_write_autonomous_extractor() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        let path = writer.write_autonomous(&extractor, &pattern).expect("write");

        assert!(path.exists());

        let content = fs::read_to_string(&path).expect("read");
        assert!(content.contains("# AUTO-PROMOTED: true"));
        assert!(content.contains("# Approved by: autonomous"));
        assert!(content.contains("auto_promoted: true"));
    }

    #[test]
    fn test_list_existing() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        // Initially empty
        let files = writer.list_existing().expect("list");
        assert!(files.is_empty());

        // Write one file
        let pattern = create_test_pattern();
        let extractor = create_test_extractor();
        writer.write(&extractor, &pattern).expect("write");

        // Now should have one file
        let files = writer.list_existing().expect("list");
        assert_eq!(files.len(), 1);
    }

    #[test]
    fn test_exists() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        assert!(!writer.exists("learned_tls_min_version"));

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();
        writer.write(&extractor, &pattern).expect("write");

        assert!(writer.exists("learned_tls_min_version"));
    }

    #[test]
    fn test_yaml_value_conversion() {
        let matched = DeclarativeValue::MatchedText { value_from_match: true };
        let yaml_matched: YamlValue = (&matched).into();
        assert!(matches!(yaml_matched, YamlValue::MatchedText { value_from_match: true }));

        let bool_val = DeclarativeValue::Boolean { value: false };
        let yaml_bool: YamlValue = (&bool_val).into();
        assert!(matches!(yaml_bool, YamlValue::BoolValue { value: false }));

        let text_val = DeclarativeValue::Text { value: "test".to_string() };
        let yaml_text: YamlValue = (&text_val).into();
        assert!(matches!(yaml_text, YamlValue::TextValue { value } if value == "test"));
    }

    #[test]
    fn test_default_output_dir() {
        let default = YamlWriter::default_output_dir();
        assert_eq!(default.to_str(), Some(".aphoria/extractors/learned"));
    }

    #[test]
    fn test_write_versioned_first_version() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        let (path, version) = writer
            .write_versioned(&extractor, &pattern, "Initial promotion", false)
            .expect("write versioned");

        assert!(path.exists());
        assert_eq!(version, 1);

        let content = fs::read_to_string(&path).expect("read");
        assert!(content.contains("version: 1"));
        assert!(content.contains("# Version: 1"));
        assert!(!content.contains("previous_version"));
    }

    #[test]
    fn test_write_versioned_creates_new_version() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        // Write v1
        let (_, v1) = writer
            .write_versioned(&extractor, &pattern, "Initial promotion", false)
            .expect("write v1");
        assert_eq!(v1, 1);

        // Write v2
        let (path, v2) = writer
            .write_versioned(&extractor, &pattern, "Added YAML support", false)
            .expect("write v2");
        assert_eq!(v2, 2);

        let content = fs::read_to_string(&path).expect("read");
        assert!(content.contains("version: 2"));
        assert!(content.contains("previous_version: 1"));
        assert!(content.contains("# Version: 2 (previous: 1)"));

        // Check v1 was archived
        let archive_path = temp.path().join(".versions/learned_tls_min_version/v1.yaml");
        assert!(archive_path.exists());
    }

    #[test]
    fn test_write_versioned_updates_changelog() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        // Write v1
        writer.write_versioned(&extractor, &pattern, "Initial promotion", false).expect("write v1");

        // Write v2
        writer
            .write_versioned(&extractor, &pattern, "Added YAML support", false)
            .expect("write v2");

        // Check changelog exists and has both entries
        let changelog_path = temp.path().join(".versions/learned_tls_min_version/changelog.json");
        assert!(changelog_path.exists());

        let content = fs::read_to_string(&changelog_path).expect("read changelog");
        assert!(content.contains("Initial promotion"));
        assert!(content.contains("Added YAML support"));
    }

    #[test]
    fn test_write_versioned_autonomous() {
        let temp = TempDir::new().expect("temp dir");
        let writer = YamlWriter::new(temp.path()).expect("create writer");

        let pattern = create_test_pattern();
        let extractor = create_test_extractor();

        let (path, version) = writer
            .write_versioned(&extractor, &pattern, "Auto-promoted", true)
            .expect("write versioned autonomous");

        assert!(path.exists());
        assert_eq!(version, 1);

        let content = fs::read_to_string(&path).expect("read");
        assert!(content.contains("auto_promoted: true"));
        assert!(content.contains("# AUTO-PROMOTED: true"));
    }
}
