use serde::{Deserialize, Serialize};

use crate::{
    bare::GitBare,
    cmd::{commit::CommitMeta, oid::ObjectId},
    errors::{GitError, GitResult},
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CommitWalkSort {
    None,
    Topological,
    Time,
    Reverse,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitWalkParams {
    pub start_oids: Vec<ObjectId>,
    pub hide_oids: Vec<ObjectId>,
    pub limit: Option<usize>,
    pub skip: usize,
    pub first_parent: bool,
    pub sort: CommitWalkSort,
    pub branch: Option<String>,
}

impl Default for CommitWalkParams {
    fn default() -> Self {
        Self {
            start_oids: Vec::new(),
            hide_oids: Vec::new(),
            limit: None,
            skip: 0,
            first_parent: false,
            sort: CommitWalkSort::Time,
            branch: None,
        }
    }
}

impl GitBare {
    pub fn commit_walk(
        &self,
        params: CommitWalkParams,
    ) -> GitResult<Vec<CommitMeta>> {
        let repo = self.gix_repo()?;
        let tips: Vec<gix::hash::ObjectId> = if let Some(ref branch_name) =
            params.branch
        {
            if branch_name.is_empty() {
                vec![repo.head_id()?.detach()]
            } else {
                let ref_name = format!("refs/heads/{branch_name}");
                match repo.try_find_reference(&ref_name)? {
                    Some(reference) => {
                        let target = reference.target();
                        let target_id = target.try_id().ok_or_else(|| {
                            GitError::Internal(format!(
                                "branch '{branch_name}' has no direct target"
                            ))
                        })?;
                        vec![target_id.to_owned()]
                    }
                    None => {
                        let remote_ref = format!("refs/remotes/{branch_name}");
                        match repo.try_find_reference(&remote_ref)? {
                            Some(reference) => {
                                let target = reference.target();
                                let target_id = target.try_id().ok_or_else(|| {
                                    GitError::Internal(format!("remote branch '{branch_name}' has no direct target"))
                                })?;
                                vec![target_id.to_owned()]
                            }
                            None => {
                                return Err(GitError::RefNotFound(
                                    branch_name.clone(),
                                ));
                            }
                        }
                    }
                }
            }
        } else if params.start_oids.is_empty() {
            vec![repo.head_id()?.detach()]
        } else {
            params
                .start_oids
                .iter()
                .map(|oid| oid.try_into())
                .collect::<Result<Vec<_>, _>>()?
        };
        let hide: Vec<gix::hash::ObjectId> = params
            .hide_oids
            .iter()
            .map(|oid| oid.try_into())
            .collect::<Result<Vec<_>, _>>()?;
        let mut platform = repo.rev_walk(tips);
        match params.sort {
            CommitWalkSort::None => {}
            CommitWalkSort::Topological => {
                platform = platform
                    .sorting(gix::revision::walk::Sorting::BreadthFirst);
            }
            CommitWalkSort::Time => {
                platform = platform.sorting(gix::revision::walk::Sorting::ByCommitTime(
                    gix::traverse::commit::simple::CommitTimeOrder::NewestFirst,
                ));
            }
            CommitWalkSort::Reverse => {
                platform = platform.sorting(gix::revision::walk::Sorting::ByCommitTime(
                    gix::traverse::commit::simple::CommitTimeOrder::OldestFirst,
                ));
            }
        }
        if params.first_parent {
            platform = platform.first_parent_only();
        }
        if !hide.is_empty() {
            platform = platform.with_boundary(hide);
        }

        let walk = platform.all()?;

        let mut commits = Vec::new();
        let mut count = 0;
        let skip = params.skip;

        for info in walk {
            if count < skip {
                count += 1;
                continue;
            }

            let info = info?;
            let oid = ObjectId::new(info.id().detach().to_hex().to_string());
            let commit = self.commit_info(oid)?;
            commits.push(commit);
            if let Some(limit) = params.limit {
                if commits.len() >= limit {
                    break;
                }
            }

            count += 1;
        }
        if matches!(params.sort, CommitWalkSort::Reverse) {
            commits.reverse();
        }

        Ok(commits)
    }
}
