//! Commit domain entity
//!
//! This module provides structures for working with git commits and parsing
//! [Conventional Commits](https://www.conventionalcommits.org/) format.
//!
//! # Conventional Commits Format
//!
//! The basic format is: `<type>[optional scope]: <description>`
//!
//! Common types:
//! - `feat`: New feature
//! - `fix`: Bug fix
//! - `docs`: Documentation changes
//! - `style`: Code style changes
//! - `refactor`: Code refactoring
//! - `perf`: Performance improvements
//! - `test`: Adding tests
//! - `build`: Build system changes
//! - `ci`: CI configuration changes
//! - `chore`: Maintenance tasks
//!
//! # Examples
//!
//! ```
//! use governor_core::domain::commit::{Commit, CommitType};
//!
//! let commit = Commit::new(
//!     "abc123def".to_string(),
//!     "feat(api): add user authentication".to_string(),
//!     "Alice".to_string(),
//!     "alice@example.com".to_string(),
//!     chrono::Utc::now(),
//! );
//!
//! assert_eq!(commit.commit_type, Some(CommitType::Feat));
//! assert_eq!(commit.scope, Some("api".to_string()));
//! ```

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

/// A git commit
///
/// Represents a git commit with parsed conventional commit metadata.
///
/// # Examples
///
/// ```
/// use governor_core::domain::commit::Commit;
/// use chrono::Utc;
///
/// let commit = Commit::new(
///     "abc123def".to_string(),
///     "feat: add new feature".to_string(),
///     "Alice".to_string(),
///     "alice@example.com".to_string(),
///     Utc::now(),
/// );
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Commit {
    /// Full commit hash
    pub hash: String,
    /// Short commit hash (7 characters)
    pub short_hash: String,
    /// Commit message
    pub message: String,
    /// Commit author
    pub author: String,
    /// Commit author email
    pub author_email: String,
    /// Commit date
    pub date: DateTime<Utc>,
    /// Files changed in this commit
    pub files_changed: Vec<String>,
    /// Conventional commit type (if parsed)
    pub commit_type: Option<CommitType>,
    /// Conventional commit scope (if present)
    pub scope: Option<String>,
    /// Whether this is a breaking change
    pub breaking: bool,
    /// Commit body
    pub body: Option<String>,
}

impl Commit {
    /// Create a new commit
    #[must_use]
    pub fn new(
        hash: String,
        message: String,
        author: String,
        author_email: String,
        date: DateTime<Utc>,
    ) -> Self {
        let short_hash = hash.chars().take(7).collect();
        let (commit_type, scope, breaking) = Self::parse_conventional(&message);

        Self {
            hash,
            short_hash,
            message,
            author,
            author_email,
            date,
            files_changed: Vec::new(),
            commit_type,
            scope,
            breaking,
            body: None,
        }
    }

    /// Parse a conventional commit message
    fn parse_conventional(message: &str) -> (Option<CommitType>, Option<String>, bool) {
        let parts: Vec<&str> = message.splitn(2, ':').collect();
        if parts.len() < 2 {
            return (None, None, false);
        }

        let prefix = parts[0].trim();

        // Extract type and scope from prefix like "type(scope)" or "type!"
        let type_str = prefix.trim_end_matches('!');

        // Parse scope from type(scope) format
        let scope = type_str.find(')').and_then(|end| {
            type_str.find('(').and_then(|start| {
                let scope_str = &type_str[start + 1..end];
                if scope_str.is_empty() {
                    None
                } else {
                    Some(scope_str.to_string())
                }
            })
        });

        // Get the base type without scope
        let base_type = type_str
            .find('(')
            .map_or(type_str, |start| &type_str[..start]);

        let breaking = prefix.ends_with('!');
        let commit_type = CommitType::parse_from_str(base_type);

        // Check for BREAKING CHANGE in body
        let has_breaking_body = message
            .lines()
            .skip(1)
            .any(|line| line.to_uppercase().contains("BREAKING CHANGE"));

        (commit_type, scope, breaking || has_breaking_body)
    }

    /// Get the commit type
    #[must_use]
    pub const fn commit_type(&self) -> Option<CommitType> {
        self.commit_type
    }

    /// Check if this is a conventional commit
    #[must_use]
    pub const fn is_conventional(&self) -> bool {
        self.commit_type.is_some()
    }

    /// Check if this commit affects a specific scope
    #[must_use]
    pub fn affects_scope(&self, scope: &str) -> bool {
        self.scope.as_deref() == Some(scope)
    }

    /// Check if this commit is of a specific type
    #[must_use]
    pub fn is_type(&self, commit_type: CommitType) -> bool {
        self.commit_type == Some(commit_type)
    }

    /// Get a shortened message (first line)
    #[must_use]
    pub fn short_message(&self) -> &str {
        self.message.lines().next().unwrap_or(&self.message)
    }
}

/// Conventional commit types
///
/// According to the [Conventional Commits](https://www.conventionalcommits.org/) specification.
///
/// Each type maps to a specific semantic versioning impact:
/// - `feat`: Minor version bump (new functionality)
/// - `fix`: Patch version bump (bug fixes)
/// - `perf`: Patch version bump (performance improvements)
/// - `refactor`: Major version bump if breaking, otherwise omitted
/// - Others: Omitted from changelog
///
/// # Examples
///
/// ```
/// use governor_core::domain::commit::CommitType;
///
/// assert_eq!(CommitType::parse_from_str("feat"), Some(CommitType::Feat));
/// assert_eq!(CommitType::parse_from_str("invalid"), None);
/// assert!(CommitType::Feat.is_feature());
/// assert!(CommitType::Fix.is_fix());
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CommitType {
    /// New feature (minor version bump)
    Feat,
    /// Bug fix (patch version bump)
    Fix,
    /// Documentation changes (no version bump)
    Docs,
    /// Code style changes (no version bump)
    Style,
    /// Code refactoring (major if breaking, else no version bump)
    Refactor,
    /// Performance improvements (patch version bump)
    Perf,
    /// Adding tests (no version bump)
    Test,
    /// Build system changes (no version bump)
    Build,
    /// CI configuration changes (no version bump)
    Ci,
    /// Chore/maintenance (no version bump)
    Chore,
    /// Revert a previous commit
    Revert,
}

impl CommitType {
    /// Parse from string
    #[must_use]
    pub fn parse_from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "feat" => Some(Self::Feat),
            "fix" => Some(Self::Fix),
            "docs" => Some(Self::Docs),
            "style" => Some(Self::Style),
            "refactor" => Some(Self::Refactor),
            "perf" => Some(Self::Perf),
            "test" => Some(Self::Test),
            "build" => Some(Self::Build),
            "ci" => Some(Self::Ci),
            "chore" => Some(Self::Chore),
            "revert" => Some(Self::Revert),
            _ => None,
        }
    }

    /// Check if this type triggers a major version bump
    #[must_use]
    pub const fn is_breaking(self) -> bool {
        matches!(self, Self::Feat | Self::Fix | Self::Perf | Self::Refactor)
    }

    /// Check if this type triggers a minor version bump
    #[must_use]
    pub const fn is_feature(self) -> bool {
        matches!(self, Self::Feat)
    }

    /// Check if this type triggers a patch version bump
    #[must_use]
    pub const fn is_fix(self) -> bool {
        matches!(self, Self::Fix | Self::Perf)
    }

    /// Check if this type should be included in changelog
    #[must_use]
    pub const fn is_changeloggable(self) -> bool {
        matches!(self, Self::Feat | Self::Fix | Self::Perf | Self::Refactor)
    }
}

impl std::fmt::Display for CommitType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Feat => write!(f, "feat"),
            Self::Fix => write!(f, "fix"),
            Self::Docs => write!(f, "docs"),
            Self::Style => write!(f, "style"),
            Self::Refactor => write!(f, "refactor"),
            Self::Perf => write!(f, "perf"),
            Self::Test => write!(f, "test"),
            Self::Build => write!(f, "build"),
            Self::Ci => write!(f, "ci"),
            Self::Chore => write!(f, "chore"),
            Self::Revert => write!(f, "revert"),
        }
    }
}

/// A collection of commits
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitHistory {
    /// All commits in the history
    pub commits: Vec<Commit>,
    /// The starting tag/ref
    pub since: Option<String>,
    /// The ending ref
    pub until: Option<String>,
}

impl CommitHistory {
    /// Create a new commit history
    #[must_use]
    pub const fn new(commits: Vec<Commit>) -> Self {
        Self {
            commits,
            since: None,
            until: None,
        }
    }

    /// Get commits by type
    #[must_use]
    pub fn by_type(&self, commit_type: CommitType) -> Vec<&Commit> {
        self.commits
            .iter()
            .filter(|c| c.commit_type == Some(commit_type))
            .collect()
    }

    /// Get breaking changes
    #[must_use]
    pub fn breaking_changes(&self) -> Vec<&Commit> {
        self.commits.iter().filter(|c| c.breaking).collect()
    }

    /// Get features
    #[must_use]
    pub fn features(&self) -> Vec<&Commit> {
        self.commits
            .iter()
            .filter(|c| c.commit_type == Some(CommitType::Feat))
            .collect()
    }

    /// Get fixes
    #[must_use]
    pub fn fixes(&self) -> Vec<&Commit> {
        self.commits
            .iter()
            .filter(|c| c.commit_type == Some(CommitType::Fix))
            .collect()
    }

    /// Get changeloggable commits
    #[must_use]
    pub fn changeloggable(&self) -> Vec<&Commit> {
        self.commits
            .iter()
            .filter(|c| c.commit_type.is_some_and(CommitType::is_changeloggable))
            .collect()
    }

    /// Count commits by type
    #[must_use]
    pub fn count_by_type(&self) -> std::collections::HashMap<CommitType, usize> {
        let mut counts = std::collections::HashMap::new();
        for commit in &self.commits {
            if let Some(ct) = commit.commit_type {
                *counts.entry(ct).or_insert(0) += 1;
            }
        }
        counts
    }

    /// Check if history is empty
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.commits.is_empty()
    }

    /// Get the number of commits
    #[must_use]
    pub const fn len(&self) -> usize {
        self.commits.len()
    }
}

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

    #[test]
    fn test_conventional_commit_parsing() {
        let commit = Commit::new(
            "abc123".to_string(),
            "feat: add new feature".to_string(),
            "Author".to_string(),
            "author@example.com".to_string(),
            Utc::now(),
        );
        assert_eq!(commit.commit_type, Some(CommitType::Feat));
        assert!(!commit.breaking);
        assert!(commit.is_conventional());
    }

    #[test]
    fn test_breaking_commit_parsing() {
        let commit = Commit::new(
            "abc123".to_string(),
            "feat!: breaking API change".to_string(),
            "Author".to_string(),
            "author@example.com".to_string(),
            Utc::now(),
        );
        assert_eq!(commit.commit_type, Some(CommitType::Feat));
        assert!(commit.breaking);
    }

    #[test]
    fn test_scope_parsing() {
        let commit = Commit::new(
            "abc123".to_string(),
            "feat(api): add new endpoint".to_string(),
            "Author".to_string(),
            "author@example.com".to_string(),
            Utc::now(),
        );
        assert_eq!(commit.commit_type, Some(CommitType::Feat));
        assert_eq!(commit.scope, Some("api".to_string()));
        assert!(commit.affects_scope("api"));
    }

    #[test]
    fn test_non_conventional_commit() {
        let commit = Commit::new(
            "abc123".to_string(),
            "Add some stuff".to_string(),
            "Author".to_string(),
            "author@example.com".to_string(),
            Utc::now(),
        );
        assert_eq!(commit.commit_type, None);
        assert!(!commit.is_conventional());
    }

    #[test]
    fn test_commit_history_filtering() {
        let commits = vec![
            Commit::new(
                "1".to_string(),
                "feat: feature 1".to_string(),
                "A".to_string(),
                "a@a.com".to_string(),
                Utc::now(),
            ),
            Commit::new(
                "2".to_string(),
                "fix: bug fix".to_string(),
                "A".to_string(),
                "a@a.com".to_string(),
                Utc::now(),
            ),
            Commit::new(
                "3".to_string(),
                "feat: feature 2".to_string(),
                "A".to_string(),
                "a@a.com".to_string(),
                Utc::now(),
            ),
        ];
        let history = CommitHistory::new(commits);
        assert_eq!(history.features().len(), 2);
        assert_eq!(history.fixes().len(), 1);
        assert_eq!(history.changeloggable().len(), 3);
    }
}
