use serde::{Deserialize, Serialize};

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

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchListItem {
    pub name: String,
    pub oid: ObjectId,
    pub is_head: bool,
    pub is_remote: bool,
    pub is_current: bool,
    pub upstream: Option<String>,
}

impl GitBare {
    pub fn branch_list_all(&self) -> GitResult<Vec<BranchListItem>> {
        let repo = self.gix_repo()?;
        let head_name = repo.head_name()?.map(|n| n.shorten().to_string());

        let mut items = Vec::new();
        let platform = repo.references()?;
        let local_iter = platform.local_branches()?;
        for ref_result in local_iter {
            let reference = ref_result?;
            let name = reference.name().shorten().to_string();
            let is_head = head_name.as_ref() == Some(&name);
            let is_current = is_head;

            let target = reference.target();
            let target_id = target.try_id().ok_or_else(|| {
                GitError::Internal(
                    "local branch has no direct target".to_string(),
                )
            })?;
            let oid = ObjectId::new(target_id.to_hex().to_string());
            let upstream = reference
                .remote_tracking_ref_name(gix::remote::Direction::Fetch)
                .and_then(|r| r.ok())
                .map(|n| n.shorten().to_string());

            items.push(BranchListItem {
                name,
                oid,
                is_head,
                is_remote: false,
                is_current,
                upstream,
            });
        }
        let remote_iter = platform.remote_branches()?;
        for ref_result in remote_iter {
            let reference = ref_result?;
            let name = reference.name().shorten().to_string();
            let is_head = head_name.as_ref() == Some(&name);
            let is_current = is_head;

            let target = reference.target();
            let target_id = target.try_id().ok_or_else(|| {
                GitError::Internal(
                    "remote branch has no direct target".to_string(),
                )
            })?;
            let oid = ObjectId::new(target_id.to_hex().to_string());

            items.push(BranchListItem {
                name,
                oid,
                is_head,
                is_remote: true,
                is_current,
                upstream: None,
            });
        }

        Ok(items)
    }

    pub fn branch_info(&self, branch: String) -> GitResult<BranchListItem> {
        let repo = self.gix_repo()?;
        let head_name = repo.head_name()?.map(|n| n.shorten().to_string());
        let local_ref_str = format!("refs/heads/{branch}");
        let local_ref = repo.try_find_reference(local_ref_str.as_str())?;
        if let Some(reference) = local_ref {
            let name = reference.name().shorten().to_string();
            let is_head = head_name.as_ref() == Some(&name);
            let target = reference.target();
            let target_id = target.try_id().ok_or_else(|| {
                GitError::Internal("branch has no direct target".to_string())
            })?;
            let oid = ObjectId::new(target_id.to_hex().to_string());

            let upstream = reference
                .remote_tracking_ref_name(gix::remote::Direction::Fetch)
                .and_then(|r| r.ok())
                .map(|n| n.shorten().to_string());

            return Ok(BranchListItem {
                name,
                oid,
                is_head,
                is_remote: false,
                is_current: is_head,
                upstream,
            });
        }
        let remote_ref_str = format!("refs/remotes/{branch}");
        let remote_ref = repo.try_find_reference(remote_ref_str.as_str())?;
        if let Some(reference) = remote_ref {
            let name = reference.name().shorten().to_string();
            let is_head = head_name.as_ref() == Some(&name);
            let target = reference.target();
            let target_id = target.try_id().ok_or_else(|| {
                GitError::Internal("branch has no direct target".to_string())
            })?;
            let oid = ObjectId::new(target_id.to_hex().to_string());

            return Ok(BranchListItem {
                name,
                oid,
                is_head,
                is_remote: true,
                is_current: is_head,
                upstream: None,
            });
        }

        Err(GitError::RefNotFound(branch))
    }
}
