use crate::error::Result;
use crate::integrations::git::{CommitInfo, GitRepo};
use crate::steps::{Step, StepContext, StepOutput};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use std::fs;
use std::path::PathBuf;

pub struct ChangelogStep {
    version: String,
    previous_tag: Option<String>,
}

impl ChangelogStep {
    pub fn new(version: String, previous_tag: Option<String>) -> Self {
        Self {
            version,
            previous_tag,
        }
    }

    fn format_changelog(
        version: &str,
        commits: &[CommitInfo],
        previous_tag: Option<&str>,
    ) -> String {
        let mut output = String::new();
        let now: DateTime<Utc> = Utc::now();

        output.push_str(&format!(
            "## [{}] - {}\n\n",
            version,
            now.format("%Y-%m-%d")
        ));

        if commits.is_empty() {
            output.push_str("No changes recorded.\n\n");
            return output;
        }

        let mut features = Vec::new();
        let mut fixes = Vec::new();
        let mut other = Vec::new();

        for commit in commits {
            let msg = commit.message.lines().next().unwrap_or("").trim();
            if msg.is_empty() {
                continue;
            }

            if msg.starts_with("feat:") || msg.starts_with("feature:") {
                features.push(
                    msg.trim_start_matches("feat:")
                        .trim_start_matches("feature:")
                        .trim(),
                );
            } else if msg.starts_with("fix:") {
                fixes.push(msg.trim_start_matches("fix:").trim());
            } else {
                other.push(msg);
            }
        }

        if !features.is_empty() {
            output.push_str("### Features\n");
            for feature in features {
                output.push_str(&format!("- {}\n", feature));
            }
            output.push('\n');
        }

        if !fixes.is_empty() {
            output.push_str("### Bug Fixes\n");
            for fix in fixes {
                output.push_str(&format!("- {}\n", fix));
            }
            output.push('\n');
        }

        if !other.is_empty() {
            output.push_str("### Other Changes\n");
            for change in other {
                output.push_str(&format!("- {}\n", change));
            }
            output.push('\n');
        }

        if let Some(prev) = previous_tag {
            output.push_str(&format!("**Full Changelog**: {}...{}\n\n", prev, version));
        }

        output
    }

    fn get_changelog_path(&self) -> Result<PathBuf> {
        let repo = GitRepo::open()?;
        Ok(repo.root_path().join("CHANGELOG.md"))
    }

    fn prepend_to_changelog(&self, path: &PathBuf, new_content: &str) -> Result<()> {
        let existing = if path.exists() {
            fs::read_to_string(path)?
        } else {
            "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n".to_string()
        };

        // Insert the new release section before the first existing release
        // heading so the file header (title + description) stays intact.
        // Splitting at the first blank line instead would drop the new
        // section between the title and its description paragraph.
        let updated = if existing.starts_with("## [") {
            format!("{}{}", new_content, existing)
        } else if let Some(pos) = existing.find("\n## [") {
            let (header, rest) = existing.split_at(pos + 1);
            format!("{}{}{}", header, new_content, rest)
        } else {
            // No release sections yet: append after the header.
            format!("{}\n\n{}", existing.trim_end(), new_content)
        };

        fs::write(path, updated)?;

        Ok(())
    }

    fn categorize_commits(commits: &[String]) -> (usize, usize, usize) {
        let mut features = 0;
        let mut fixes = 0;
        let mut other = 0;

        for commit in commits {
            let msg = commit.trim();
            if msg.is_empty() {
                continue;
            }
            if msg.starts_with("feat:") || msg.starts_with("feature:") {
                features += 1;
            } else if msg.starts_with("fix:") {
                fixes += 1;
            } else {
                other += 1;
            }
        }

        (features, fixes, other)
    }
}

#[async_trait]
impl Step for ChangelogStep {
    fn name(&self) -> &str {
        "changelog"
    }

    fn description(&self) -> &str {
        "Generate changelog from commits"
    }

    async fn validate(&self, _ctx: &StepContext) -> Result<()> {
        GitRepo::open()?;
        Ok(())
    }

    async fn execute(&self, _ctx: &StepContext) -> Result<StepOutput> {
        let repo = GitRepo::open()?;

        let commits = if let Some(ref prev_tag) = self.previous_tag {
            repo.get_commits_between(prev_tag, "HEAD")?
        } else {
            // First release: no previous tag, so include the full history.
            repo.get_commits_up_to("HEAD")?
        };

        let changelog_content =
            Self::format_changelog(&self.version, &commits, self.previous_tag.as_deref());

        let path = self.get_changelog_path()?;
        self.prepend_to_changelog(&path, &changelog_content)?;

        Ok(StepOutput::ok(format!(
            "Generated changelog with {} commits",
            commits.len()
        )))
    }

    async fn dry_run(&self, _ctx: &StepContext) -> Result<StepOutput> {
        let repo = GitRepo::open()?;

        let commits = if let Some(ref prev_tag) = self.previous_tag {
            repo.get_commits_between(prev_tag, "HEAD")?
        } else {
            // First release: no previous tag, so include the full history.
            repo.get_commits_up_to("HEAD")?
        };

        // Generate preview
        let preview_content =
            Self::format_changelog(&self.version, &commits, self.previous_tag.as_deref());

        // Count categorized commits
        let commit_messages: Vec<String> = commits
            .iter()
            .map(|c| c.message.lines().next().unwrap_or("").to_string())
            .collect();
        let (features, fixes, other) = Self::categorize_commits(&commit_messages);

        let details = crate::steps::DryRunDetails {
            file_changes: vec![crate::steps::FileChange {
                path: "CHANGELOG.md".to_string(),
                operation: crate::steps::FileOperation::Modify,
                diff: None,
            }],
            docker_preview: None,
            notes: vec![
                format!("Found {} new commits", commits.len()),
                format!(
                    "  - {} features, {} fixes, {} other",
                    features, fixes, other
                ),
                if commits.is_empty() {
                    "⚠ No commits since last tag - changelog will be empty".to_string()
                } else {
                    format!(
                        "Preview:\n{}",
                        preview_content
                            .lines()
                            .take(20)
                            .collect::<Vec<_>>()
                            .join("\n")
                    )
                },
            ],
        };

        Ok(StepOutput::ok(format!(
            "Would generate changelog with {} commits",
            commits.len()
        ))
        .with_dry_run_details(details))
    }

    async fn rollback(&self, _ctx: &StepContext) -> Result<()> {
        let repo = GitRepo::open()?;
        let path = self.get_changelog_path()?;
        let rel_path = path.strip_prefix(repo.root_path()).map_err(|_| {
            crate::error::ApiForgeError::Config("Invalid changelog path".to_string())
        })?;
        repo.checkout_file(rel_path)?;
        tracing::info!("Rolled back changelog changes");
        Ok(())
    }
}

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

    #[test]
    fn test_prepend_keeps_header_intact_and_orders_sections() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("CHANGELOG.md");
        std::fs::write(
            &path,
            "# Changelog\n\nAll notable changes documented here.\n\n## [1.0.0] - 2026-01-01\n\n- old\n",
        )
        .unwrap();

        let step = ChangelogStep::new("1.1.0".to_string(), Some("v1.0.0".to_string()));
        step.prepend_to_changelog(&path, "## [1.1.0] - 2026-06-10\n\n- new\n\n")
            .unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        let header_pos = content.find("All notable").unwrap();
        let new_pos = content.find("## [1.1.0]").unwrap();
        let old_pos = content.find("## [1.0.0]").unwrap();
        assert!(header_pos < new_pos, "header must stay above new section");
        assert!(new_pos < old_pos, "newest section must come first");
    }

    #[test]
    fn test_prepend_creates_new_file_with_header_then_section() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("CHANGELOG.md");

        let step = ChangelogStep::new("1.0.0".to_string(), None);
        step.prepend_to_changelog(&path, "## [1.0.0] - 2026-06-10\n\n- first\n\n")
            .unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.starts_with("# Changelog"));
        assert!(
            content.find("documented in this file").unwrap() < content.find("## [1.0.0]").unwrap(),
            "description paragraph must precede release sections"
        );
    }

    #[test]
    fn test_prepend_handles_file_starting_with_release_section() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("CHANGELOG.md");
        std::fs::write(&path, "## [1.0.0] - 2026-01-01\n\n- old\n").unwrap();

        let step = ChangelogStep::new("1.1.0".to_string(), Some("v1.0.0".to_string()));
        step.prepend_to_changelog(&path, "## [1.1.0] - 2026-06-10\n\n- new\n\n")
            .unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.starts_with("## [1.1.0]"));
    }
}
