/*
 * Copyright(c) 2024-2026 China Telecom Cloud Technologies Co., Ltd. All rights
 * reserved. ctscat is licensed under Mulan PSL v2. You can use this software
 * according to the terms and conditions of the Mulan PSL V2. You may obtain a
 * copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2.
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
 * KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  See the Mulan PSL v2 for
 * more details.
 */

//! Git 仓库客户端
//!
//! 用于本地 Git 仓库操作,支持 clone、fetch 和 commit 差异对比

use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use git2::{Repository, Sort};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tracing::{debug, info};

/// Git Commit 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitCommit {
    /// Commit SHA
    pub sha: String,
    /// Commit 消息
    pub message: String,
    /// 作者名
    pub author: String,
    /// 作者邮箱
    pub author_email: String,
    /// Commit 时间
    pub committed_at: DateTime<Utc>,
    /// 文件变动数
    pub files_changed: usize,
}

/// Commit 对比结果
#[derive(Debug, Clone)]
pub struct CommitDiff {
    /// L1 领先 commits(L1 中有但 L2 中没有)
    pub l1_ahead: Vec<GitCommit>,
    /// L2 领先 commits(L2 中有但 L1 中没有)
    pub l2_ahead: Vec<GitCommit>,
}

/// Git 仓库客户端
pub struct GitRepositoryClient {
    repo_path: PathBuf,
}

impl GitRepositoryClient {
    /// 创建新的 Git 客户端
    pub fn new(repo_path: impl AsRef<Path>) -> Result<Self> {
        let repo_path = repo_path.as_ref().to_path_buf();

        // 验证仓库是否存在
        if !repo_path.exists() {
            return Err(anyhow!("仓库路径不存在: {}", repo_path.display()));
        }

        Ok(Self { repo_path })
    }

    /// 克隆或更新仓库
    pub fn clone_or_pull(url: &str, target_path: impl AsRef<Path>, branch: &str) -> Result<Self> {
        let target_path = target_path.as_ref();

        if target_path.exists() {
            // 仓库已存在,执行 pull
            info!(url = url, path = ?target_path, "更新现有仓库");
            let repo = Repository::open(target_path).context("打开现有仓库失败")?;

            Self::fetch_and_checkout(&repo, branch)?;
        } else {
            // 仓库不存在,执行 clone
            info!(url = url, path = ?target_path, "克隆新仓库");
            Repository::clone(url, target_path).context("克隆仓库失败")?;

            let repo = Repository::open(target_path).context("打开新克隆的仓库失败")?;

            Self::fetch_and_checkout(&repo, branch)?;
        }

        Self::new(target_path)
    }

    /// 获取指定分支的 commits
    pub fn get_commits(&self, branch: &str) -> Result<Vec<GitCommit>> {
        let repo = Repository::open(&self.repo_path).context("打开仓库失败")?;

        // 获取分支引用
        let branch_name = format!("refs/heads/{}", branch);
        let obj = repo
            .revparse_single(&branch_name)
            .or_else(|_| repo.revparse_single(&format!("remotes/origin/{}", branch)))
            .context(format!("找不到分支: {}", branch))?;

        // 创建遍历器
        let mut revwalk = repo.revwalk().context("创建 revwalk 失败")?;
        revwalk.push(obj.id()).context("设置 revwalk 起点失败")?;

        revwalk
            .set_sorting(Sort::TIME | Sort::REVERSE)
            .context("设置排序失败")?;

        let mut commits = Vec::new();

        for oid in revwalk {
            let oid = oid.context("获取 OID 失败")?;
            let commit = repo.find_commit(oid).context("查找 commit 失败")?;

            let files_changed = Self::count_files_changed(&repo, &commit)?;

            commits.push(GitCommit {
                sha: commit.id().to_string(),
                message: commit
                    .message()
                    .unwrap_or("")
                    .lines()
                    .next()
                    .unwrap_or("")
                    .to_string(),
                author: commit.author().name().unwrap_or("Unknown").to_string(),
                author_email: commit.author().email().unwrap_or("").to_string(),
                committed_at: DateTime::<Utc>::from_timestamp(commit.time().seconds(), 0)
                    .unwrap_or_else(Utc::now),
                files_changed,
            });
        }

        debug!(branch = branch, count = commits.len(), "获取 commits 完成");

        Ok(commits)
    }

    /// 对比两个分支的 commits
    pub fn compare_branches(&self, l1_branch: &str, l2_branch: &str) -> Result<CommitDiff> {
        let l1_commits = self
            .get_commits(l1_branch)
            .context("获取 L1 branch commits 失败")?;
        let l2_commits = self
            .get_commits(l2_branch)
            .context("获取 L2 branch commits 失败")?;

        let diff = Self::compute_diff(&l1_commits, &l2_commits);

        info!(
            l1_branch = l1_branch,
            l2_branch = l2_branch,
            l1_ahead = diff.l1_ahead.len(),
            l2_ahead = diff.l2_ahead.len(),
            "分支对比完成"
        );

        Ok(diff)
    }

    /// 对比两个 commit 列表
    pub fn compute_diff(l1_commits: &[GitCommit], l2_commits: &[GitCommit]) -> CommitDiff {
        // 构建 L2 commit SHA 的集合用于快速查询
        let l2_shas: std::collections::HashSet<_> =
            l2_commits.iter().map(|c| c.sha.as_str()).collect();

        // 找出 L1 中有但 L2 中没有的 commits
        let l1_ahead: Vec<GitCommit> = l1_commits
            .iter()
            .filter(|c| !l2_shas.contains(c.sha.as_str()))
            .cloned()
            .collect();

        // 构建 L1 commit SHA 的集合用于快速查询
        let l1_shas: std::collections::HashSet<_> =
            l1_commits.iter().map(|c| c.sha.as_str()).collect();

        // 找出 L2 中有但 L1 中没有的 commits
        let l2_ahead: Vec<GitCommit> = l2_commits
            .iter()
            .filter(|c| !l1_shas.contains(c.sha.as_str()))
            .cloned()
            .collect();

        CommitDiff { l1_ahead, l2_ahead }
    }

    /// 私有辅助方法:获取和检出分支
    fn fetch_and_checkout(repo: &Repository, branch: &str) -> Result<()> {
        // 如果是远程仓库,先 fetch
        if let Ok(mut remote) = repo.find_remote("origin") {
            remote
                .fetch(&[branch], None, None)
                .context("Fetch 分支失败")?;
        }

        // 检出分支
        let obj = repo
            .revparse_single(&format!("refs/heads/{}", branch))
            .or_else(|_| repo.revparse_single(&format!("remotes/origin/{}", branch)))
            .context(format!("找不到分支: {}", branch))?;

        repo.set_head_detached(obj.id()).context("检出分支失败")?;

        Ok(())
    }

    /// 私有辅助方法:计算 commit 中变动的文件数
    fn count_files_changed(repo: &Repository, commit: &git2::Commit) -> Result<usize> {
        if commit.parent_count() == 0 {
            // 首个 commit,直接计算树中的文件数
            let tree = commit.tree().context("获取树失败")?;
            Ok(tree.len())
        } else {
            // 对比与父 commit 的差异
            let parent = commit.parent(0).context("获取父 commit 失败")?;
            let parent_tree = parent.tree().context("获取父树失败")?;
            let commit_tree = commit.tree().context("获取 commit 树失败")?;

            let diff = repo
                .diff_tree_to_tree(Some(&parent_tree), Some(&commit_tree), None)
                .context("生成 diff 失败")?;

            Ok(diff.deltas().len())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use git2::{Oid, Repository, Signature};
    use tempfile::TempDir;

    fn create_commit(repo: &Repository, message: &str, parent: Option<Oid>) -> Oid {
        let signature = Signature::now("Test User", "test@example.com").unwrap();
        let tree_id = {
            let mut index = repo.index().unwrap();
            index.write_tree().unwrap()
        };
        let tree = repo.find_tree(tree_id).unwrap();
        let parents = if let Some(p) = parent {
            vec![repo.find_commit(p).unwrap()]
        } else {
            vec![]
        };
        let parents_refs: Vec<&git2::Commit> = parents.iter().collect();

        repo.commit(
            Some("HEAD"),
            &signature,
            &signature,
            message,
            &tree,
            &parents_refs,
        )
        .unwrap()
    }

    #[test]
    fn test_new_invalid_path() {
        let result = GitRepositoryClient::new("/invalid/path");
        assert!(result.is_err());
    }

    #[test]
    fn test_compute_diff() {
        let now = Utc::now();
        let c1 = GitCommit {
            sha: "sha1".to_string(),
            message: "m1".to_string(),
            author: "a".to_string(),
            author_email: "e".to_string(),
            committed_at: now,
            files_changed: 1,
        };
        let c2 = GitCommit {
            sha: "sha2".to_string(),
            message: "m2".to_string(),
            author: "a".to_string(),
            author_email: "e".to_string(),
            committed_at: now,
            files_changed: 1,
        };
        let c3 = GitCommit {
            sha: "sha3".to_string(),
            message: "m3".to_string(),
            author: "a".to_string(),
            author_email: "e".to_string(),
            committed_at: now,
            files_changed: 1,
        };

        // L1: [c1, c2], L2: [c1] => L1 ahead [c2], L2 ahead []
        let diff =
            GitRepositoryClient::compute_diff(&[c1.clone(), c2.clone()], std::slice::from_ref(&c1));
        assert_eq!(diff.l1_ahead.len(), 1);
        assert_eq!(diff.l1_ahead[0].sha, "sha2");
        assert_eq!(diff.l2_ahead.len(), 0);

        // L1: [c1], L2: [c1, c3] => L1 ahead [], L2 ahead [c3]
        let diff =
            GitRepositoryClient::compute_diff(std::slice::from_ref(&c1), &[c1.clone(), c3.clone()]);
        assert_eq!(diff.l1_ahead.len(), 0);
        assert_eq!(diff.l2_ahead.len(), 1);
        assert_eq!(diff.l2_ahead[0].sha, "sha3");
    }

    #[test]
    fn test_get_commits() {
        let temp_dir = TempDir::new().unwrap();
        let repo = Repository::init(temp_dir.path()).unwrap();

        // Create commits
        let oid1 = create_commit(&repo, "First commit", None);
        let oid2 = create_commit(&repo, "Second commit", Some(oid1));

        let client = GitRepositoryClient::new(temp_dir.path()).unwrap();
        let commits = client.get_commits("master").unwrap();

        assert_eq!(commits.len(), 2);
        // git2 revwalk with TIME | REVERSE usually yields oldest first if timestamps same,
        // but here we rely on topological order or verify existence.
        // Actually implementation uses Sort::TIME | Sort::REVERSE which means REVERSE(TIME) i.e. oldest first?
        // Let's check implementation:
        // revwalk.set_sorting(Sort::TIME | Sort::REVERSE)
        // libgit2 doc: TIME sorts by time (newest first). REVERSE reverses the order.
        // So it should be oldest first.

        // However, in our test, timestamps might be identical.
        // Let's just check content.
        let shas: Vec<String> = commits.iter().map(|c| c.sha.clone()).collect();
        assert!(shas.contains(&oid1.to_string()));
        assert!(shas.contains(&oid2.to_string()));
    }

    #[test]
    fn test_compute_diff_no_diff() {
        let commit1 = GitCommit {
            sha: "abc123".to_string(),
            message: "test".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let l1 = vec![commit1.clone()];
        let l2 = vec![commit1];

        let diff = GitRepositoryClient::compute_diff(&l1, &l2);
        assert_eq!(diff.l1_ahead.len(), 0);
        assert_eq!(diff.l2_ahead.len(), 0);
    }

    #[test]
    fn test_compute_diff_l1_ahead() {
        let commit1 = GitCommit {
            sha: "abc123".to_string(),
            message: "first".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let commit2 = GitCommit {
            sha: "def456".to_string(),
            message: "second".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let l1 = vec![commit1.clone(), commit2.clone()];
        let l2 = vec![commit1];

        let diff = GitRepositoryClient::compute_diff(&l1, &l2);
        assert_eq!(diff.l1_ahead.len(), 1);
        assert_eq!(diff.l2_ahead.len(), 0);
        assert_eq!(diff.l1_ahead[0].sha, "def456");
    }

    #[test]
    fn test_compute_diff_l2_ahead() {
        let commit1 = GitCommit {
            sha: "abc123".to_string(),
            message: "first".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let commit2 = GitCommit {
            sha: "def456".to_string(),
            message: "second".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let l1 = vec![commit1.clone()];
        let l2 = vec![commit1, commit2.clone()];

        let diff = GitRepositoryClient::compute_diff(&l1, &l2);
        assert_eq!(diff.l1_ahead.len(), 0);
        assert_eq!(diff.l2_ahead.len(), 1);
        assert_eq!(diff.l2_ahead[0].sha, "def456");
    }

    #[test]
    fn test_compute_diff_both_different() {
        let commit1 = GitCommit {
            sha: "aaa".to_string(),
            message: "first".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let commit2 = GitCommit {
            sha: "bbb".to_string(),
            message: "second".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let commit3 = GitCommit {
            sha: "ccc".to_string(),
            message: "third".to_string(),
            author: "test".to_string(),
            author_email: "test@example.com".to_string(),
            committed_at: Utc::now(),
            files_changed: 1,
        };

        let l1 = vec![commit1.clone(), commit2.clone()];
        let l2 = vec![commit1, commit3.clone()];

        let diff = GitRepositoryClient::compute_diff(&l1, &l2);
        assert_eq!(diff.l1_ahead.len(), 1);
        assert_eq!(diff.l2_ahead.len(), 1);
        assert_eq!(diff.l1_ahead[0].sha, "bbb");
        assert_eq!(diff.l2_ahead[0].sha, "ccc");
    }
}