//! Changelog domain entity

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

use super::commit::Commit;
use super::version::SemanticVersion;

/// A changelog for a crate or workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Changelog {
    /// Entries in the changelog
    pub entries: Vec<ChangelogEntry>,
    /// The version this changelog is for
    pub version: SemanticVersion,
    /// Release date
    pub released_at: Option<DateTime<Utc>>,
}

impl Changelog {
    /// Create a new changelog
    #[must_use]
    pub const fn new(version: SemanticVersion) -> Self {
        Self {
            entries: Vec::new(),
            version,
            released_at: None,
        }
    }

    /// Add an entry to the changelog
    pub fn add_entry(&mut self, entry: ChangelogEntry) {
        self.entries.push(entry);
    }

    /// Get entries by section
    #[must_use]
    pub fn entries_by_section(&self, section: ChangelogSection) -> Vec<&ChangelogEntry> {
        self.entries
            .iter()
            .filter(|e| e.section == section)
            .collect()
    }

    /// Get breaking changes
    #[must_use]
    pub fn breaking_changes(&self) -> Vec<&ChangelogEntry> {
        self.entries_by_section(ChangelogSection::Breaking)
    }

    /// Get features
    #[must_use]
    pub fn features(&self) -> Vec<&ChangelogEntry> {
        self.entries_by_section(ChangelogSection::Added)
    }

    /// Get fixes
    #[must_use]
    pub fn fixes(&self) -> Vec<&ChangelogEntry> {
        self.entries_by_section(ChangelogSection::Fixed)
    }

    /// Format as keep-a-changelog markdown
    #[must_use]
    pub fn format_keep_a_changelog(&self) -> String {
        use std::fmt::Write;

        let mut output = String::new();
        writeln!(output, "## [{}]", self.version).unwrap();
        if let Some(date) = &self.released_at {
            writeln!(output, " - {}", date.format("%Y-%m-%d")).unwrap();
        }
        writeln!(output).unwrap();

        for section in ChangelogSection::all() {
            let entries = self.entries_by_section(section);
            if !entries.is_empty() {
                writeln!(output, "\n### {}\n", section.title()).unwrap();
                for entry in entries {
                    output.push_str(&entry.format_markdown());
                }
            }
        }

        output
    }

    /// Format as JSON
    ///
    /// # Errors
    ///
    /// Returns an error if JSON serialization fails
    pub fn format_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(self)
    }
}

/// A section in the changelog
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangelogSection {
    /// Breaking changes
    Breaking,
    /// Added features
    Added,
    /// Fixed bugs
    Fixed,
    /// Changed things
    Changed,
    /// Deprecated features
    Deprecated,
    /// Removed features
    Removed,
    /// Security fixes
    Security,
}

impl ChangelogSection {
    /// Get all sections in order
    #[must_use]
    pub fn all() -> Vec<Self> {
        vec![
            Self::Breaking,
            Self::Added,
            Self::Fixed,
            Self::Changed,
            Self::Deprecated,
            Self::Removed,
            Self::Security,
        ]
    }

    /// Get the title for this section
    #[must_use]
    pub const fn title(self) -> &'static str {
        match self {
            Self::Breaking => "Breaking Changes",
            Self::Added => "Added",
            Self::Fixed => "Fixed",
            Self::Changed => "Changed",
            Self::Deprecated => "Deprecated",
            Self::Removed => "Removed",
            Self::Security => "Security",
        }
    }

    /// Get the order for this section
    #[must_use]
    pub const fn order(self) -> usize {
        match self {
            Self::Breaking => 0,
            Self::Added => 1,
            Self::Fixed => 2,
            Self::Changed => 3,
            Self::Deprecated => 4,
            Self::Removed => 5,
            Self::Security => 6,
        }
    }

    /// Create from commit type
    #[must_use]
    pub fn from_commit_type(commit_type: &str, breaking: bool) -> Option<Self> {
        if breaking {
            return Some(Self::Breaking);
        }
        match commit_type.to_lowercase().as_str() {
            "feat" => Some(Self::Added),
            "fix" => Some(Self::Fixed),
            "refactor" | "perf" => Some(Self::Changed),
            "deprecated" => Some(Self::Deprecated),
            "removed" => Some(Self::Removed),
            "security" => Some(Self::Security),
            _ => None,
        }
    }
}

/// A changelog entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogEntry {
    /// The section this entry belongs to
    pub section: ChangelogSection,
    /// The entry text
    pub message: String,
    /// Scope (if applicable)
    pub scope: Option<String>,
    /// Commit hash (if applicable)
    pub commit_hash: Option<String>,
    /// Affected crates (for workspace changelogs)
    pub affected_crates: Vec<String>,
}

impl ChangelogEntry {
    /// Create a new changelog entry
    #[must_use]
    pub const fn new(section: ChangelogSection, message: String) -> Self {
        Self {
            section,
            message,
            scope: None,
            commit_hash: None,
            affected_crates: Vec::new(),
        }
    }

    /// Create from a commit
    #[must_use]
    pub fn from_commit(commit: &Commit) -> Option<Self> {
        let commit_type = commit.commit_type?;
        let section =
            ChangelogSection::from_commit_type(&commit_type.to_string(), commit.breaking)?;

        let mut entry = Self::new(section, commit.short_message().to_string());
        entry.scope.clone_from(&commit.scope);
        entry.commit_hash = Some(commit.short_hash.clone());
        Some(entry)
    }

    /// Set the scope
    #[must_use]
    pub fn with_scope(mut self, scope: String) -> Self {
        self.scope = Some(scope);
        self
    }

    /// Set the commit hash
    #[must_use]
    pub fn with_commit_hash(mut self, hash: String) -> Self {
        self.commit_hash = Some(hash);
        self
    }

    /// Add an affected crate
    #[must_use]
    pub fn with_affected_crate(mut self, krate: String) -> Self {
        self.affected_crates.push(krate);
        self
    }

    /// Format as markdown
    #[must_use]
    pub fn format_markdown(&self) -> String {
        use std::fmt::Write;

        let mut output = String::from("- ");

        if let Some(scope) = &self.scope {
            write!(output, "**{scope}**: ").unwrap();
        }

        output.push_str(&self.message);

        if let Some(hash) = &self.commit_hash {
            write!(output, " ({hash})").unwrap();
            if !self.affected_crates.is_empty() {
                write!(output, " in {}", self.affected_crates.join(", ")).unwrap();
            }
        }

        writeln!(output).unwrap();
        output
    }
}

/// Changelog configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogConfig {
    /// Changelog format
    pub format: ChangelogFormat,
    /// Path to changelog file
    pub path: String,
    /// Whether to append to existing changelog
    pub incremental: bool,
    /// Section configuration
    pub sections: Vec<ChangelogSectionConfig>,
    /// Commit types to exclude
    pub exclude_types: Vec<String>,
    /// Scopes to exclude
    pub exclude_scopes: Vec<String>,
}

impl Default for ChangelogConfig {
    fn default() -> Self {
        Self {
            format: ChangelogFormat::KeepAChangelog,
            path: "CHANGELOG.md".to_string(),
            incremental: true,
            sections: ChangelogSectionConfig::default_all(),
            exclude_types: vec![
                "docs".to_string(),
                "test".to_string(),
                "chore".to_string(),
                "style".to_string(),
                "ci".to_string(),
                "build".to_string(),
            ],
            exclude_scopes: vec!["internal".to_string(), "deps".to_string()],
        }
    }
}

/// Changelog format
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChangelogFormat {
    /// Keep a Changelog format
    KeepAChangelog,
    /// GitHub Releases format
    GitHubReleases,
    /// JSON format
    Json,
}

/// Configuration for a changelog section
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangelogSectionConfig {
    /// Section type
    pub section: ChangelogSection,
    /// Title to display
    pub title: String,
    /// Order in the changelog
    pub order: usize,
    /// Commit types that go in this section
    pub commit_types: Vec<String>,
}

impl ChangelogSectionConfig {
    /// Get default section configurations
    #[must_use]
    pub fn default_all() -> Vec<Self> {
        vec![
            Self {
                section: ChangelogSection::Breaking,
                title: "Breaking Changes".to_string(),
                order: 0,
                commit_types: vec!["feat!".to_string()],
            },
            Self {
                section: ChangelogSection::Added,
                title: "Added".to_string(),
                order: 1,
                commit_types: vec!["feat".to_string()],
            },
            Self {
                section: ChangelogSection::Fixed,
                title: "Fixed".to_string(),
                order: 2,
                commit_types: vec!["fix".to_string()],
            },
            Self {
                section: ChangelogSection::Changed,
                title: "Changed".to_string(),
                order: 3,
                commit_types: vec!["refactor".to_string(), "perf".to_string()],
            },
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::commit::Commit;

    #[test]
    fn test_changelog_entry_from_commit() {
        let commit = Commit::new(
            "abc123".to_string(),
            "feat: add new feature".to_string(),
            "Author".to_string(),
            "author@example.com".to_string(),
            Utc::now(),
        );
        let entry = ChangelogEntry::from_commit(&commit);
        assert!(entry.is_some());
        let entry = entry.unwrap();
        assert_eq!(entry.section, ChangelogSection::Added);
        assert_eq!(entry.message, "feat: add new feature");
    }

    #[test]
    fn test_changelog_format() {
        let mut changelog = Changelog::new(SemanticVersion::parse("1.0.0").unwrap());
        changelog.released_at = Some(Utc::now());

        let mut entry =
            ChangelogEntry::new(ChangelogSection::Added, "Add cool feature".to_string());
        entry.scope = Some("api".to_string());
        entry.commit_hash = Some("abc123".to_string());
        changelog.add_entry(entry);

        let formatted = changelog.format_keep_a_changelog();
        assert!(formatted.contains("## [1.0.0]"));
        assert!(formatted.contains("### Added"));
        assert!(formatted.contains("**api**"));
    }

    #[test]
    fn test_changelog_sections() {
        let section = ChangelogSection::from_commit_type("feat", false);
        assert_eq!(section, Some(ChangelogSection::Added));

        let section = ChangelogSection::from_commit_type("feat", true);
        assert_eq!(section, Some(ChangelogSection::Breaking));

        let section = ChangelogSection::from_commit_type("fix", false);
        assert_eq!(section, Some(ChangelogSection::Fixed));
    }
}
