use super::{CommitInfo, GitRepo};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::fs;
use std::path::Path;

pub const CHANGELOG_HEADER: &str = r#"# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

"#;

/// Changelog generator
pub struct ChangelogGenerator {
    format: ChangelogFormat,
    include_hashes: bool,
    include_authors: bool,
    group_by_type: bool,
    custom_categories: Vec<ChangelogCategory>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangelogFormat {
    KeepAChangelog,
    GitHubReleases,
    Custom,
}

#[derive(Debug, Clone)]
pub struct ChangelogCategory {
    pub title: String,
    pub types: Vec<String>,
}

impl ChangelogGenerator {
    /// Create new changelog generator
    pub fn new() -> Self {
        Self {
            format: ChangelogFormat::KeepAChangelog,
            include_hashes: false,
            include_authors: false,
            group_by_type: true,
            custom_categories: vec![],
        }
    }

    /// Set format
    pub fn format(mut self, format: ChangelogFormat) -> Self {
        self.format = format;
        self
    }

    /// Include commit hashes
    pub fn include_hashes(mut self, include: bool) -> Self {
        self.include_hashes = include;
        self
    }

    /// Include authors
    pub fn include_authors(mut self, include: bool) -> Self {
        self.include_authors = include;
        self
    }

    /// Group by type
    pub fn group_by_type(mut self, group: bool) -> Self {
        self.group_by_type = group;
        self
    }

    /// Add custom category
    pub fn add_category(mut self, title: impl Into<String>, types: Vec<String>) -> Self {
        self.custom_categories.push(ChangelogCategory {
            title: title.into(),
            types,
        });
        self
    }

    /// Generate changelog for version
    pub fn generate(
        &self,
        version: &str,
        date: DateTime<Utc>,
        commits: &[CommitInfo],
    ) -> Result<String> {
        match self.format {
            ChangelogFormat::KeepAChangelog => {
                self.generate_keep_a_changelog(version, date, commits)
            }
            ChangelogFormat::GitHubReleases => {
                self.generate_github_releases(version, date, commits)
            }
            ChangelogFormat::Custom => {
                self.generate_custom(version, date, commits)
            }
        }
    }

    /// Generate changelog entry and prepend to file
    pub fn generate_and_prepend(
        &self,
        changelog_path: &Path,
        version: &str,
        date: DateTime<Utc>,
        commits: &[CommitInfo],
    ) -> Result<()> {
        let entry = self.generate(version, date, commits)?;
        
        let existing = if changelog_path.exists() {
            fs::read_to_string(changelog_path)?
        } else {
            String::new()
        };
        
        let new_content = if existing.is_empty() {
            format!("{}{}", CHANGELOG_HEADER, entry)
        } else if existing.starts_with(CHANGELOG_HEADER) {
            format!("{}{}", CHANGELOG_HEADER, entry)
        } else if existing.starts_with("# Changelog") {
            let lines: Vec<&str> = existing.lines().collect();
            let mut header_end = 0;
            
            for (i, line) in lines.iter().enumerate() {
                if i == 0 && line.starts_with('#') {
                    header_end = i + 1;
                } else if line.trim().is_empty() {
                    header_end = i + 1;
                } else {
                    break;
                }
            }
        
            let header = lines[..header_end].join("\n");
            let rest = lines[header_end..].join("\n");
            
            format!("{}\n{}\n{}", header, entry, rest)
        } else {
            format!("{}{}", CHANGELOG_HEADER, entry)
        };
        
        fs::write(changelog_path, new_content)
            .with_context(|| format!("Failed to write changelog: {:?}", changelog_path))?;
        
        Ok(())
    }

    fn generate_keep_a_changelog(
        &self,
        version: &str,
        date: DateTime<Utc>,
        commits: &[CommitInfo],
    ) -> Result<String> {
        let date_str = date.format("%Y-%m-%d").to_string();
        let mut output = format!("## [{}] - {}\n\n", version, date_str);
        
        if self.group_by_type {
            let _grouped = self.group_commits(commits);
            
            // Standard categories
            let categories = vec![
                ("Added", vec!["feat"]),
                ("Changed", vec!["refactor", "perf"]),
                ("Deprecated", vec![]),
                ("Removed", vec!["remove"]),
                ("Fixed", vec!["fix"]),
                ("Security", vec!["security"]),
            ];
            
            for (title, types) in &categories {
                let items: Vec<&CommitInfo> = commits
                    .iter()
                    .filter(|c| {
                        if let Some(ref t) = c.commit_type() {
                            types.contains(&t.as_str())
                        } else {
                            false
                        }
                    })
                    .collect();
                
                if !items.is_empty() {
                    output.push_str(&format!("### {}\n\n", title));
                    for commit in items {
                        output.push_str(&self.format_commit(commit));
                        output.push('\n');
                    }
                    output.push('\n');
                }
            }
            
            // Other changes
            let categorized: Vec<String> = categories
                .iter()
                .flat_map(|(_, types)| types.iter().map(|s| s.to_string()))
                .collect();
            
            let other: Vec<&CommitInfo> = commits
                .iter()
                .filter(|c| {
                    if let Some(ref t) = c.commit_type() {
                        !categorized.contains(t)
                    } else {
                        true
                    }
                })
                .collect();
            
            if !other.is_empty() {
                output.push_str("### Other\n\n");
                for commit in other {
                    output.push_str(&self.format_commit(commit));
                    output.push('\n');
                }
                output.push('\n');
            }
        } else {
            for commit in commits {
                output.push_str(&self.format_commit(commit));
                output.push('\n');
            }
        }
        
        Ok(output)
    }

    fn generate_github_releases(
        &self,
        _version: &str,
        _date: DateTime<Utc>,
        commits: &[CommitInfo],
    ) -> Result<String> {
        let mut output = "## What's Changed\n\n".to_string();
        
        // Group by type
        let mut features = vec![];
        let mut fixes = vec![];
        let mut docs = vec![];
        let mut other = vec![];
        let mut breaking = vec![];
        
        for commit in commits {
            if commit.message.contains("BREAKING CHANGE") {
                breaking.push(commit);
            }
            
            if let Some(ref t) = commit.commit_type() {
                match t.as_str() {
                    "feat" => features.push(commit),
                    "fix" => fixes.push(commit),
                    "docs" => docs.push(commit),
                    _ => other.push(commit),
                }
            } else {
                other.push(commit);
            }
        }
        
        if !breaking.is_empty() {
            output.push_str("### ⚠ Breaking Changes\n\n");
            for commit in breaking {
                output.push_str(&self.format_commit_github(commit));
            }
            output.push('\n');
        }
        
        if !features.is_empty() {
            output.push_str("### 🚀 Features\n\n");
            for commit in features {
                output.push_str(&self.format_commit_github(commit));
            }
            output.push('\n');
        }
        
        if !fixes.is_empty() {
            output.push_str("### 🐛 Bug Fixes\n\n");
            for commit in fixes {
                output.push_str(&self.format_commit_github(commit));
            }
            output.push('\n');
        }
        
        if !docs.is_empty() {
            output.push_str("### 📚 Documentation\n\n");
            for commit in docs {
                output.push_str(&self.format_commit_github(commit));
            }
            output.push('\n');
        }
        
        if !other.is_empty() {
            output.push_str("### Other Changes\n\n");
            for commit in other {
                output.push_str(&self.format_commit_github(commit));
            }
        }
        
        Ok(output)
    }

    fn generate_custom(
        &self,
        version: &str,
        date: DateTime<Utc>,
        commits: &[CommitInfo],
    ) -> Result<String> {
        // Use custom categories if defined
        if !self.custom_categories.is_empty() {
            let date_str = date.format("%Y-%m-%d").to_string();
            let mut output = format!("## [{}] - {}\n\n", version, date_str);
            
            for category in &self.custom_categories {
                let items: Vec<&CommitInfo> = commits
                    .iter()
                    .filter(|c| {
                        if let Some(ref t) = c.commit_type() {
                            category.types.contains(t)
                        } else {
                            false
                        }
                    })
                    .collect();
                
                if !items.is_empty() {
                    output.push_str(&format!("### {}\n\n", category.title));
                    for commit in items {
                        output.push_str(&self.format_commit(commit));
                        output.push('\n');
                    }
                    output.push('\n');
                }
            }
            
            Ok(output)
        } else {
            // Fall back to keep-a-changelog
            self.generate_keep_a_changelog(version, date, commits)
        }
    }

    fn format_commit(&self, commit: &CommitInfo) -> String {
        let mut line = format!("- {}", commit.subject());
        
        if self.include_hashes {
            line.push_str(&format!(" ({})", &commit.short_id));
        }
        
        if self.include_authors {
            line.push_str(&format!(" - @{}", commit.author));
        }
        
        line
    }

    fn format_commit_github(&self, commit: &CommitInfo) -> String {
        format!("- {} by @{} in {}\n", commit.subject(), commit.author, &commit.short_id)
    }

    fn group_commits<'a>(&self, commits: &'a [CommitInfo]) -> HashMap<String, Vec<&'a CommitInfo>> {
        let mut groups: HashMap<String, Vec<&'a CommitInfo>> = HashMap::new();
        
        for commit in commits {
            let commit_type = commit.commit_type().unwrap_or_else(|| "other".to_string());
            groups.entry(commit_type).or_default().push(commit);
        }
        
        groups
    }
}

impl Default for ChangelogGenerator {
    fn default() -> Self {
        Self::new()
    }
}

/// Read existing changelog
pub fn read_changelog(path: &Path) -> Result<String> {
    fs::read_to_string(path)
        .with_context(|| format!("Failed to read changelog: {:?}", path))
}

/// Initialize new changelog file
pub fn init_changelog(path: &Path) -> Result<()> {
    if path.exists() {
        anyhow::bail!("Changelog already exists at {:?}", path);
    }
    
    fs::write(path, CHANGELOG_HEADER)
        .with_context(|| format!("Failed to create changelog: {:?}", path))?;
    
    Ok(())
}

/// Generate changelog from git history
pub fn generate_from_history(
    repo: &GitRepo,
    from_tag: Option<&str>,
    to_ref: Option<&str>,
) -> Result<Vec<CommitInfo>> {
    let to_ref = to_ref.unwrap_or("HEAD");
    
    if let Some(from) = from_tag {
        repo.get_commits_between(from, to_ref)
    } else {
        // Get last 50 commits if no tag specified
        repo.get_commits(50)
    }
}

/// Update version links in changelog
pub fn update_version_links(
    changelog: &str,
    version: &str,
    compare_url: &str,
) -> String {
    // Add version link at the end of changelog
    format!("{}\n[{}]: {}\n", changelog, version, compare_url)
}

/// Parse changelog to extract versions
pub fn parse_versions(changelog: &str) -> Vec<(String, String)> {
    let mut versions = vec![];
    
    for line in changelog.lines() {
        if line.starts_with("## [")
            && let Some(start) = line.find('[')
                && let Some(end) = line.find(']') {
                    let version = &line[start + 1..end];
                    if version != "Unreleased"
                        && let Some(date_start) = line.find(" - ") {
                            let date = &line[date_start + 3..].trim();
                            versions.push((version.to_string(), date.to_string()));
                        }
                }
    }
    
    versions
}

/// Get unreleased changes
pub fn get_unreleased_changes(repo: &GitRepo) -> Result<Vec<CommitInfo>> {
    let tags = repo.get_tags()?;
    
    if let Some(latest_tag) = tags.first() {
        repo.get_commits_between(&latest_tag.name, "HEAD")
    } else {
        repo.get_commits(50)
    }
}

/// Changelog entry for a specific version
pub struct ChangelogEntry {
    pub version: String,
    pub date: DateTime<Utc>,
    pub commits: Vec<CommitInfo>,
}

impl ChangelogEntry {
    /// Create new entry
    pub fn new(version: impl Into<String>, commits: Vec<CommitInfo>) -> Self {
        Self {
            version: version.into(),
            date: Utc::now(),
            commits,
        }
    }

    /// Set date
    pub fn with_date(mut self, date: DateTime<Utc>) -> Self {
        self.date = date;
        self
    }
}
