//! Reference operations.

use crate::commit::types::CommitOid;
use crate::reference::types::RefInfo;
use crate::{GitDomain, GitError, GitResult};

/// Specifies what the reference update should be based on.
pub enum RefUpdateTarget {
    Oid(CommitOid),
}

/// Result of a reference update operation.
pub struct RefUpdateResult {
    pub name: String,
    pub old_oid: Option<CommitOid>,
    pub new_oid: Option<CommitOid>,
}

impl GitDomain {
    /// List all references matching a pattern (e.g. "refs/heads/*").
    pub fn ref_list(&self, pattern: Option<&str>) -> GitResult<Vec<RefInfo>> {
        let mut refs = Vec::new();
        let iter = self
            .repo()
            .references()
            .map_err(|e| GitError::Internal(e.to_string()))?;

        for result in iter {
            let r = result.map_err(|e| GitError::Internal(e.to_string()))?;
            let name = match r.name() {
                Some(n) => n.to_string(),
                None => continue,
            };

            if let Some(pat) = pattern {
                if !name_match(&name, pat) {
                    continue;
                }
            }

            let target = r.target().map(CommitOid::from_git2);
            let oid = r
                .peel_to_commit()
                .ok()
                .map(|c| CommitOid::from_git2(c.id()));

            let is_symbolic = r.kind() == Some(git2::ReferenceType::Symbolic);
            let is_branch = name.starts_with("refs/heads/");
            let is_remote = name.starts_with("refs/remotes/");
            let is_tag = name.starts_with("refs/tags/");
            let is_note = name.starts_with("refs/notes/");
            refs.push(RefInfo {
                name,
                oid,
                target,
                is_symbolic,
                is_branch,
                is_remote,
                is_tag,
                is_note,
            });
        }

        Ok(refs)
    }

    pub fn ref_get(&self, name: &str) -> GitResult<RefInfo> {
        let r = self
            .repo()
            .find_reference(name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        let target = r.target().map(CommitOid::from_git2);
        let oid = r
            .peel_to_commit()
            .ok()
            .map(|c| CommitOid::from_git2(c.id()));

        Ok(RefInfo {
            name: name.to_string(),
            oid,
            target,
            is_symbolic: r.kind() == Some(git2::ReferenceType::Symbolic),
            is_branch: name.starts_with("refs/heads/"),
            is_remote: name.starts_with("refs/remotes/"),
            is_tag: name.starts_with("refs/tags/"),
            is_note: name.starts_with("refs/notes/"),
        })
    }

    pub fn ref_create(
        &self,
        name: &str,
        oid: CommitOid,
        force: bool,
        message: Option<&str>,
    ) -> GitResult<RefUpdateResult> {
        let git_oid = oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(oid.to_string()))?;

        let old = self.repo().find_reference(name).ok();
        let old_oid = old
            .as_ref()
            .and_then(|r| r.target().map(CommitOid::from_git2));

        self.repo()
            .reference(name, git_oid, force, message.unwrap_or("create ref"))
            .map_err(|e| {
                if !force && e.code() == git2::ErrorCode::Exists {
                    GitError::BranchExists(name.to_string())
                } else {
                    GitError::Internal(e.to_string())
                }
            })?;

        Ok(RefUpdateResult {
            name: name.to_string(),
            old_oid,
            new_oid: Some(oid),
        })
    }

    pub fn ref_delete(&self, name: &str) -> GitResult<CommitOid> {
        let mut r = self
            .repo()
            .find_reference(name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        let target = r
            .target()
            .map(CommitOid::from_git2)
            .ok_or_else(|| GitError::Internal("ref has no target".to_string()))?;

        r.delete().map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(target)
    }

    /// Rename a reference. Fails if new name already exists unless `force` is true.
    pub fn ref_rename(&self, old_name: &str, new_name: &str, force: bool) -> GitResult<RefInfo> {
        let mut r = self
            .repo()
            .find_reference(old_name)
            .map_err(|_e| GitError::RefNotFound(old_name.to_string()))?;

        let target = r.target().map(CommitOid::from_git2);
        let oid = r
            .peel_to_commit()
            .ok()
            .map(|c| CommitOid::from_git2(c.id()));
        let ref_kind = r.kind(); // Capture kind before rename

        if !force && self.repo().find_reference(new_name).is_ok() {
            return Err(GitError::BranchExists(new_name.to_string()));
        }

        r.rename(new_name, force, "rename ref")
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(RefInfo {
            name: new_name.to_string(),
            oid,
            target,
            is_symbolic: ref_kind == Some(git2::ReferenceType::Symbolic),
            is_branch: new_name.starts_with("refs/heads/"),
            is_remote: new_name.starts_with("refs/remotes/"),
            is_tag: new_name.starts_with("refs/tags/"),
            is_note: new_name.starts_with("refs/notes/"),
        })
    }

    pub fn ref_update(
        &self,
        name: &str,
        new_oid: CommitOid,
        expected_oid: Option<CommitOid>,
        message: Option<&str>,
    ) -> GitResult<RefUpdateResult> {
        let old = self
            .repo()
            .find_reference(name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        let old_oid = old.target().map(CommitOid::from_git2);

        // CAS check
        if let Some(expected) = expected_oid {
            let git_expected = expected
                .to_oid()
                .map_err(|_| GitError::InvalidOid(expected.to_string()))?;
            if old.target() != Some(git_expected) {
                return Err(GitError::Internal(
                    "ref update failed: unexpected current value (CAS mismatch)".to_string(),
                ));
            }
        }

        let git_new_oid = new_oid
            .to_oid()
            .map_err(|_| GitError::InvalidOid(new_oid.to_string()))?;

        // Use reference_matching for CAS. Pass None as previous target only when
        // the ref has no target (symbolic ref with broken target) — fall back to
        // unconditional reference update in that case.
        match old.target() {
            Some(prev) => {
                self.repo()
                    .reference_matching(
                        name,
                        git_new_oid,
                        true,
                        prev,
                        message.unwrap_or("update ref"),
                    )
                    .map_err(|e| GitError::Internal(e.to_string()))?;
            }
            None => {
                self.repo()
                    .reference(name, git_new_oid, true, message.unwrap_or("update ref"))
                    .map_err(|e| GitError::Internal(e.to_string()))?;
            }
        }

        Ok(RefUpdateResult {
            name: name.to_string(),
            old_oid,
            new_oid: Some(new_oid),
        })
    }

    pub fn ref_exists(&self, name: &str) -> bool {
        self.repo().find_reference(name).is_ok()
    }

    /// Get the peeled (commit) OID of a reference.
    pub fn ref_target(&self, name: &str) -> GitResult<Option<CommitOid>> {
        let r = self
            .repo()
            .find_reference(name)
            .map_err(|_e| GitError::RefNotFound(name.to_string()))?;

        Ok(r.peel_to_commit()
            .ok()
            .map(|c| CommitOid::from_git2(c.id())))
    }
}

fn name_match(name: &str, pattern: &str) -> bool {
    if let Some(stripped) = pattern.strip_suffix("/**") {
        name.starts_with(stripped)
    } else if let Some(stripped) = pattern.strip_suffix("/*") {
        name.starts_with(stripped) && !name[stripped.len()..].contains('/')
    } else {
        name == pattern
    }
}
