//! Branch create/delete/rename operations.

use git2::BranchType;

use crate::branch::types::BranchInfo;
use crate::commit::types::CommitOid;
use crate::ref_utils::validate_ref_name;
use crate::{GitDomain, GitError, GitResult};

impl GitDomain {
    pub fn branch_create(&self, name: &str, oid: &CommitOid, force: bool) -> GitResult<BranchInfo> {
        validate_ref_name(name)?;

        let target = oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(oid.to_string()))?;

        let commit = self
            .repo()
            .find_commit(target)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let branch = self.repo.branch(name, &commit, force).map_err(|e| {
            if e.code() == git2::ErrorCode::Exists && !force {
                GitError::BranchExists(name.to_string())
            } else {
                GitError::Internal(e.to_string())
            }
        })?;

        let full_name = branch.get().name().unwrap_or("").to_string();

        Ok(BranchInfo {
            name: full_name,
            oid: CommitOid::from_git2(target),
            is_head: false,
            is_remote: false,
            is_current: false,
            upstream: None,
        })
    }

    pub fn branch_create_from_head(&self, name: &str, force: bool) -> GitResult<BranchInfo> {
        let head_oid = self
            .repo()
            .head()
            .ok()
            .and_then(|r| r.target())
            .ok_or_else(|| GitError::Internal("HEAD is not attached".to_string()))?;

        self.branch_create(name, &CommitOid::from_git2(head_oid), force)
    }

    pub fn branch_delete(&self, name: &str) -> GitResult<()> {
        let full_name = if name.starts_with("refs/heads/") {
            name.to_string()
        } else {
            format!("refs/heads/{}", name)
        };

        let mut branch = self
            .repo()
            .find_branch(&full_name, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        branch
            .delete()
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn branch_delete_remote(&self, name: &str) -> GitResult<()> {
        let full_name = format!("refs/remotes/{}", name);

        let mut branch = self
            .repo()
            .find_branch(&full_name, BranchType::Remote)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        branch
            .delete()
            .map_err(|e| GitError::Internal(e.to_string()))
    }

    pub fn branch_rename(&self, old_name: &str, new_name: &str) -> GitResult<BranchInfo> {
        validate_ref_name(new_name)?;

        let old_full = if old_name.starts_with("refs/heads/") {
            old_name.to_string()
        } else {
            format!("refs/heads/{}", old_name)
        };

        let mut branch = self
            .repo()
            .find_branch(&old_full, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?;

        let target = branch
            .get()
            .target()
            .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?;

        branch.rename(new_name, false).map_err(|e| {
            if e.code() == git2::ErrorCode::Exists {
                GitError::BranchExists(new_name.to_string())
            } else {
                GitError::Internal(e.to_string())
            }
        })?;

        Ok(BranchInfo {
            name: format!("refs/heads/{}", new_name),
            oid: CommitOid::from_git2(target),
            is_head: false,
            is_remote: false,
            is_current: false,
            upstream: None,
        })
    }

    pub fn branch_move(&self, name: &str, new_name: &str, force: bool) -> GitResult<BranchInfo> {
        validate_ref_name(new_name)?;

        let full_name = if name.starts_with("refs/heads/") {
            name.to_string()
        } else {
            format!("refs/heads/{}", name)
        };

        let mut branch = self
            .repo()
            .find_branch(&full_name, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        let target = branch
            .get()
            .target()
            .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?;

        let commit = self
            .repo()
            .find_commit(target)
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Delete the old branch first. If deletion fails, we fail atomically.
        branch
            .delete()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        // Create the new branch pointing to the same commit.
        self.repo().branch(new_name, &commit, force).map_err(|e| {
            if e.code() == git2::ErrorCode::Exists && !force {
                GitError::BranchExists(new_name.to_string())
            } else {
                GitError::Internal(e.to_string())
            }
        })?;

        Ok(BranchInfo {
            name: format!("refs/heads/{}", new_name),
            oid: CommitOid::from_git2(target),
            is_head: false,
            is_remote: false,
            is_current: false,
            upstream: None,
        })
    }

    pub fn branch_set_upstream(&self, name: &str, upstream: Option<&str>) -> GitResult<()> {
        let full_name = if name.starts_with("refs/heads/") {
            name.to_string()
        } else {
            format!("refs/heads/{}", name)
        };

        let mut branch = self
            .repo()
            .find_branch(&full_name, BranchType::Local)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        match upstream {
            Some(u) => {
                let upstream_name = if u.starts_with("refs/remotes/") || u.contains('/') {
                    u.to_string()
                } else {
                    format!("refs/remotes/{}", u)
                };

                branch
                    .set_upstream(Some(&upstream_name))
                    .map_err(|e| GitError::Internal(e.to_string()))
            }
            None => branch
                .set_upstream(None)
                .map_err(|e| GitError::Internal(e.to_string())),
        }
    }
}
