//! The gix-backed implementation of [`GitRepo`].

use std::path::{Path, PathBuf};

use crate::diff_impl::NameStatusPairs;
use crate::error::GitError;
use crate::repo::GitRepo;
use crate::types::{
    CommitInfo, DiffEntry, EntryMode, GitOid, IndexEntry, RefEdit, RefName, StatusEntry, TreeEdit,
    TreeEntry, WorktreeInfo,
};

/// A [`GitRepo`] implementation backed by [gix](https://github.com/GitoxideLabs/gitoxide).
///
/// Construct via [`GixRepo::open`] or [`GixRepo::open_at`].
pub struct GixRepo {
    pub(crate) repo: gix::Repository,
    pub(crate) workdir: Option<PathBuf>,
    /// Override `.gitattributes` entries for LFS pattern matching.
    /// Set by merge callers when the merge itself modifies `.gitattributes`,
    /// so `write_blob_with_path` uses the incoming attrs instead of HEAD's.
    #[cfg(feature = "lfs")]
    pub(crate) pending_gitattributes: Option<Vec<(String, Vec<u8>)>>,
}

impl GixRepo {
    /// Open the git repository at or above `path`.
    ///
    /// # Errors
    /// Returns an error if no repository can be discovered from `path`.
    pub fn open(path: &Path) -> Result<Self, GitError> {
        let repo = gix::open(path).map_err(|e| GitError::BackendError {
            message: e.to_string(),
        })?;
        let workdir = repo.workdir().map(std::path::Path::to_path_buf);
        Ok(Self {
            repo,
            workdir,
            #[cfg(feature = "lfs")]
            pending_gitattributes: None,
        })
    }

    /// Path to this repository's git directory (`.git` or its common dir for worktrees).
    ///
    /// For a worktree, this returns the per-worktree `.git` dir, not the common
    /// git dir. LFS storage lives under the common dir; use [`Self::common_dir`].
    pub fn git_dir(&self) -> &Path {
        self.repo.git_dir()
    }

    /// Path to this repository's working directory, or `None` for a bare repo.
    ///
    /// Mirrors `gix::Repository::workdir`. Used by callers (e.g. brownfield
    /// init) that need to know whether a path opens as a non-bare worktree.
    pub fn workdir(&self) -> Option<&Path> {
        self.workdir.as_deref()
    }

    /// Path to the common git directory (shared across all worktrees).
    ///
    /// For a worktree, this is the upstream `repo.git/` rather than
    /// `repo.git/worktrees/<name>/`. For a non-worktree repo, it equals
    /// [`Self::git_dir`].
    pub fn common_dir(&self) -> &Path {
        self.repo.common_dir()
    }

    /// Open a git repository at exactly `path` (no parent discovery).
    ///
    /// # Errors
    /// Returns an error if `path` is not a git repository.
    pub fn open_at(path: &Path) -> Result<Self, GitError> {
        let repo = gix::open_opts(path, gix::open::Options::isolated()).map_err(|e| {
            GitError::BackendError {
                message: e.to_string(),
            }
        })?;
        let workdir = repo.workdir().map(std::path::Path::to_path_buf);
        Ok(Self {
            repo,
            workdir,
            #[cfg(feature = "lfs")]
            pending_gitattributes: None,
        })
    }

    /// Set override `.gitattributes` entries for LFS pattern matching.
    ///
    /// Call this before passing the repo to `build_merge_commit` when the
    /// merge itself modifies `.gitattributes`. Each entry is
    /// `(dir_prefix, file_contents)` — same format as
    /// [`maw_lfs::AttrsMatcher::from_entries`].
    ///
    /// `write_blob_with_path` will use these instead of reading from HEAD.
    #[cfg(feature = "lfs")]
    pub fn set_pending_gitattributes(&mut self, entries: Vec<(String, Vec<u8>)>) {
        self.pending_gitattributes = Some(entries);
    }

    /// Clear the override set by [`set_pending_gitattributes`].
    #[cfg(feature = "lfs")]
    pub fn clear_pending_gitattributes(&mut self) {
        self.pending_gitattributes = None;
    }

    /// Resolve a slash-separated `path` inside the tree at `tree_or_commit_oid`.
    ///
    /// Returns `(mode, oid)` for the entry, or `None` if the path is missing.
    /// Accepts a commit OID (whose tree is resolved automatically) or a tree
    /// OID directly.
    ///
    /// Replaces: `git ls-tree -z <tree-or-commit> -- <path>`.
    ///
    /// # Errors
    /// Returns a `GitError` if the backend operation fails.
    pub fn find_entry_at_path(
        &self,
        tree_or_commit_oid: GitOid,
        path: &str,
    ) -> Result<Option<(EntryMode, GitOid)>, GitError> {
        crate::objects_impl::find_entry_at_path(self, tree_or_commit_oid, path)
    }

    /// Read a blob located at `path` inside the tree at `tree_or_commit_oid`.
    ///
    /// Returns `(mode, oid, content)`, or `None` if the path is missing or
    /// names a non-blob entry. Accepts a commit OID or a tree OID directly.
    ///
    /// Replaces: `git show <oid>:<path>` (combined ls-tree + cat-file blob).
    ///
    /// # Errors
    /// Returns a `GitError` if the backend operation fails.
    pub fn read_blob_at_path(
        &self,
        tree_or_commit_oid: GitOid,
        path: &str,
    ) -> Result<Option<(EntryMode, GitOid, Vec<u8>)>, GitError> {
        crate::objects_impl::read_blob_at_path(self, tree_or_commit_oid, path)
    }

    /// Recursively list every blob / symlink reachable from `tree_or_commit_oid`.
    ///
    /// Returns one [`BlobEntry`] per file (or symlink), in tree-walk order.
    /// Submodules (gitlinks) are skipped. Use [`Self::walk_tree_blobs`] when
    /// you also need blob contents.
    ///
    /// Replaces: `git ls-tree -r --name-only -z <oid>`.
    ///
    /// # Errors
    /// Returns a `GitError` if the backend operation fails.
    pub fn walk_tree_blob_paths(
        &self,
        tree_or_commit_oid: GitOid,
    ) -> Result<Vec<crate::BlobEntry>, GitError> {
        crate::objects_impl::walk_tree_blob_paths(self, tree_or_commit_oid)
    }

    /// Recursively visit every blob reachable from `tree_or_commit_oid`,
    /// invoking `visit(entry, content)` with each blob's metadata and raw
    /// bytes.
    ///
    /// Useful for content-search workloads (replaces `git grep -z -n <oid>`).
    /// Submodules (gitlinks) are skipped.
    ///
    /// # Errors
    /// Returns a `GitError` if any backend operation or visitor invocation
    /// fails.
    pub fn walk_tree_blobs<F>(&self, tree_or_commit_oid: GitOid, visit: F) -> Result<(), GitError>
    where
        F: FnMut(&crate::BlobEntry, &[u8]) -> Result<(), GitError>,
    {
        crate::objects_impl::walk_tree_blobs(self, tree_or_commit_oid, visit)
    }

    /// Initialize a brand-new (non-bare) git repository at `directory`.
    ///
    /// Creates intermediate parent directories as needed. Fails if
    /// `directory/.git` already exists.
    ///
    /// Replaces: `git init <directory>`.
    ///
    /// # Errors
    /// Returns a `GitError` if the directory cannot be created or gix cannot
    /// initialize the repository.
    pub fn init_repo(directory: &Path) -> Result<Self, GitError> {
        crate::init_impl::init_repo(directory)
    }

    /// Return the short branch name HEAD points at, or `None` when detached.
    ///
    /// Replaces: `git symbolic-ref --short HEAD` (with `Ok(None)` mapping to
    /// the "HEAD is detached" exit-code path).
    ///
    /// # Errors
    /// Returns a `GitError` only if reading HEAD fails for a reason other
    /// than the ref being detached or missing.
    pub fn init_head_branch(&self) -> Result<Option<String>, GitError> {
        crate::init_impl::init_head_branch(self)
    }

    /// Point HEAD symbolically at `refs/heads/<branch>`.
    ///
    /// Atomic (temp file + rename). Does not require the branch ref to
    /// already exist — mirroring `git symbolic-ref` semantics.
    ///
    /// Replaces: `git symbolic-ref HEAD refs/heads/<branch>`.
    ///
    /// # Errors
    /// Returns a `GitError` if writing the HEAD file fails.
    pub fn init_set_head_to_branch(&self, branch: &str) -> Result<(), GitError> {
        crate::init_impl::init_set_head_to_branch(self, branch)
    }

    /// Create an empty-tree commit, optionally updating `refs/heads/<branch>`.
    ///
    /// Bootstraps a fresh repository with an `--allow-empty` commit. When
    /// `branch` is `Some`, the named branch is created / fast-forwarded to
    /// the new commit OID.
    ///
    /// Replaces: `git commit --allow-empty -m "..."` after `git init`.
    ///
    /// # Errors
    /// Returns a `GitError` if writing the tree, the commit, or updating
    /// the branch ref fails (e.g., no author/committer identity).
    pub fn init_create_empty_commit(
        &self,
        message: &str,
        branch: Option<&str>,
    ) -> Result<GitOid, GitError> {
        crate::init_impl::init_create_empty_commit(self, message, branch)
    }

    /// Load a `.gitattributes` matcher from the tree at the given commit.
    ///
    /// Use this to look up merge drivers (`merge=union`, `merge=binary`,
    /// etc.) for files during a merge. Typically called with the merge-base
    /// epoch commit so the resulting matcher reflects the `.gitattributes`
    /// state *at the merge base* — matching git's semantics for merge driver
    /// selection.
    ///
    /// Returns `None` on any error (invalid OID, missing commit, parse error)
    /// so callers can degrade to default-text-merge behavior without aborting.
    #[cfg(feature = "lfs")]
    pub fn load_gitattributes_at_commit(&self, commit_hex: &str) -> Option<maw_lfs::AttrsMatcher> {
        let oid = gix::ObjectId::from_hex(commit_hex.as_bytes()).ok()?;
        let commit = self.repo.find_commit(oid).ok()?;
        let tree = commit.tree().ok()?;
        maw_lfs::AttrsMatcher::from_gix_tree(&self.repo, &tree).ok()
    }
}

impl GitRepo for GixRepo {
    // === Refs ===
    fn read_ref(&self, name: &RefName) -> Result<Option<GitOid>, GitError> {
        crate::refs_impl::read_ref(self, name)
    }

    fn write_ref(&self, name: &RefName, oid: GitOid, log_message: &str) -> Result<(), GitError> {
        crate::refs_impl::write_ref(self, name, oid, log_message)
    }

    fn delete_ref(&self, name: &RefName) -> Result<(), GitError> {
        crate::refs_impl::delete_ref(self, name)
    }

    fn atomic_ref_update(&self, edits: &[RefEdit]) -> Result<(), GitError> {
        crate::refs_impl::atomic_ref_update(self, edits)
    }

    fn list_refs(&self, prefix: &str) -> Result<Vec<(RefName, GitOid)>, GitError> {
        crate::refs_impl::list_refs(self, prefix)
    }

    // === Rev-parse ===
    fn rev_parse(&self, spec: &str) -> Result<GitOid, GitError> {
        crate::refs_impl::rev_parse(self, spec)
    }

    fn rev_parse_opt(&self, spec: &str) -> Result<Option<GitOid>, GitError> {
        crate::refs_impl::rev_parse_opt(self, spec)
    }

    // === Object read ===
    fn read_blob(&self, oid: GitOid) -> Result<Vec<u8>, GitError> {
        crate::objects_impl::read_blob(self, oid)
    }

    fn read_tree(&self, oid: GitOid) -> Result<Vec<TreeEntry>, GitError> {
        crate::objects_impl::read_tree(self, oid)
    }

    fn read_commit(&self, oid: GitOid) -> Result<CommitInfo, GitError> {
        crate::objects_impl::read_commit(self, oid)
    }

    // === Object write ===
    fn write_blob(&self, data: &[u8]) -> Result<GitOid, GitError> {
        crate::objects_impl::write_blob(self, data)
    }

    #[cfg(feature = "lfs")]
    fn write_blob_with_path(&self, data: &[u8], rel_path: &str) -> Result<GitOid, GitError> {
        crate::lfs_clean::write_blob_with_path(self, data, rel_path)
    }

    fn write_tree(&self, entries: &[TreeEntry]) -> Result<GitOid, GitError> {
        crate::objects_impl::write_tree(self, entries)
    }

    fn create_commit(
        &self,
        tree: GitOid,
        parents: &[GitOid],
        message: &str,
        update_ref: Option<&RefName>,
    ) -> Result<GitOid, GitError> {
        crate::objects_impl::create_commit(self, tree, parents, message, update_ref)
    }

    // === Tree editing ===
    fn edit_tree(&self, base: GitOid, edits: &[TreeEdit]) -> Result<GitOid, GitError> {
        crate::objects_impl::edit_tree(self, base, edits)
    }

    // === Index ===
    fn read_index(&self) -> Result<Vec<IndexEntry>, GitError> {
        crate::checkout_impl::read_index(self)
    }

    fn write_index(&self, entries: &[IndexEntry]) -> Result<(), GitError> {
        crate::checkout_impl::write_index(self, entries)
    }

    // === Checkout ===
    fn checkout_tree(&self, oid: GitOid, workdir: &Path) -> Result<(), GitError> {
        crate::checkout_impl::checkout_tree(self, oid, workdir)
    }

    // === Status ===
    fn is_dirty(&self) -> Result<bool, GitError> {
        crate::status_impl::is_dirty(self)
    }

    fn status(&self) -> Result<Vec<StatusEntry>, GitError> {
        crate::status_impl::status(self)
    }

    fn status_head_to_worktree(&self) -> Result<Vec<StatusEntry>, GitError> {
        crate::status_impl::status_head_to_worktree(self)
    }

    fn status_tracked_only(&self) -> Result<Vec<StatusEntry>, GitError> {
        crate::status_impl::status_tracked_only(self)
    }

    fn count_dirty_tracked(&self) -> Result<usize, GitError> {
        crate::status_impl::count_dirty_tracked(self)
    }

    fn list_untracked(&self) -> Result<Vec<String>, GitError> {
        crate::status_impl::list_untracked(self)
    }

    fn read_file_at_commit(
        &self,
        commit_spec: &str,
        rel_path: &Path,
    ) -> Result<Option<Vec<u8>>, GitError> {
        crate::objects_impl::read_file_at_commit(self, commit_spec, rel_path)
    }

    // === Diff ===
    fn diff_trees(&self, old: Option<GitOid>, new: GitOid) -> Result<Vec<DiffEntry>, GitError> {
        crate::diff_impl::diff_trees(self, old, new)
    }

    fn diff_trees_with_renames(
        &self,
        old: Option<GitOid>,
        new: GitOid,
        similarity_pct: u32,
    ) -> Result<Vec<DiffEntry>, GitError> {
        crate::diff_impl::diff_trees_with_renames(self, old, new, similarity_pct)
    }

    fn diff_name_status_pairs(&self, base: GitOid) -> Result<NameStatusPairs, GitError> {
        crate::diff_impl::diff_name_status_pairs(self, base)
    }

    // === Worktrees ===
    fn worktree_add(&self, name: &str, target: GitOid, path: &Path) -> Result<(), GitError> {
        crate::worktree_impl::worktree_add(self, name, target, path)
    }

    fn worktree_remove(&self, name: &str) -> Result<(), GitError> {
        crate::worktree_impl::worktree_remove(self, name)
    }

    fn worktree_list(&self) -> Result<Vec<WorktreeInfo>, GitError> {
        crate::worktree_impl::worktree_list(self)
    }

    fn worktree_prune(&self) -> Result<(), GitError> {
        crate::worktree_impl::worktree_prune(self)
    }

    // === Stash ===
    fn stash_create(&self) -> Result<Option<GitOid>, GitError> {
        crate::stash_impl::stash_create(self)
    }

    fn stash_apply(&self, oid: GitOid) -> Result<(), GitError> {
        crate::stash_impl::stash_apply(self, oid)
    }

    fn worktree_state_commit(&self, message: &str) -> Result<Option<GitOid>, GitError> {
        crate::stash_impl::worktree_state_commit(self, message)
    }

    fn unstage_all(&self) -> Result<(), GitError> {
        crate::index_impl::unstage_all(self)
    }

    // === Push ===
    fn push_branch(
        &self,
        remote: &str,
        local_ref: &str,
        remote_ref: &str,
        force: bool,
    ) -> Result<(), GitError> {
        crate::push_impl::push_branch(self, remote, local_ref, remote_ref, force)
    }

    fn push_tag(&self, remote: &str, tag: &str) -> Result<(), GitError> {
        crate::push_impl::push_tag(self, remote, tag)
    }

    // === Config ===
    fn read_config(&self, key: &str) -> Result<Option<String>, GitError> {
        crate::config_impl::read_config(self, key)
    }

    fn write_config(&self, key: &str, value: &str) -> Result<(), GitError> {
        crate::config_impl::write_config(self, key, value)
    }

    // === Ancestry ===
    fn is_ancestor(&self, ancestor: GitOid, descendant: GitOid) -> Result<bool, GitError> {
        crate::refs_impl::is_ancestor(self, ancestor, descendant)
    }

    fn merge_base(&self, a: GitOid, b: GitOid) -> Result<Option<GitOid>, GitError> {
        crate::refs_impl::merge_base(self, a, b)
    }

    fn walk_commits(
        &self,
        from: GitOid,
        to: GitOid,
        reverse: bool,
    ) -> Result<Vec<GitOid>, GitError> {
        crate::rev_walk_impl::walk_commits(self, from, to, reverse)
    }

    fn count_commits_between(&self, from: GitOid, to: GitOid) -> Result<u32, GitError> {
        crate::rev_walk_impl::count_commits_between(self, from, to)
    }

    // === HEAD ===
    fn head_is_detached(&self) -> Result<bool, GitError> {
        let head = self.repo.head().map_err(|e| GitError::BackendError {
            message: format!("read HEAD: {e}"),
        })?;
        Ok(head.is_detached())
    }

    fn set_head(&self, oid: GitOid) -> Result<(), GitError> {
        crate::checkout_impl::set_head(self, oid)
    }
}

impl GixRepo {
    // --- Convenience compound operations ---

    /// Detach HEAD at `oid` and update the worktree to match.
    ///
    /// Equivalent to `git checkout --detach <oid>` but fully native:
    /// `checkout_tree` materialises the commit's tree into `workdir`, then
    /// `set_head` writes the detached HEAD (+ reflog entry, bn-20sa).
    ///
    /// # Errors
    /// Returns a `GitError` if either step fails. If `checkout_tree` fails,
    /// `set_head` is NOT called — HEAD and worktree are left unchanged.
    pub fn checkout_detach(&self, oid: GitOid, workdir: &Path) -> Result<(), GitError> {
        crate::checkout_impl::checkout_detach(self, oid, workdir)
    }

    /// Point HEAD at `<branch>` and update the worktree to match.
    ///
    /// Equivalent to `git checkout <branch>` but fully native:
    /// `checkout_tree` materialises `oid`'s tree into `workdir`, then
    /// `set_head_to_branch` writes `ref: refs/heads/<branch>` to HEAD
    /// atomically (+ reflog entry, bn-20sa).
    ///
    /// # Errors
    /// Returns a `GitError` if either step fails. If `checkout_tree` fails,
    /// HEAD is NOT written — it stays at its previous value.
    pub fn checkout_to_branch(
        &self,
        oid: GitOid,
        workdir: &Path,
        branch: &str,
    ) -> Result<(), GitError> {
        crate::checkout_impl::checkout_to_branch(self, oid, workdir, branch)
    }

    /// Force-checkout: materialize `oid`'s tree, clobbering tracked
    /// modifications, then set detached HEAD.
    ///
    /// Equivalent to `git checkout --force <oid>`. The worktree is ALWAYS
    /// updated — use only on rollback / force-restore paths where the intent
    /// is to discard the current working tree. Tracked modifications are
    /// overwritten; untracked files are preserved (bn-29x0 semantics of
    /// `checkout_tree`).
    ///
    /// # Errors
    /// Returns a `GitError` if either `checkout_tree` or `set_head` fails.
    pub fn checkout_force(&self, oid: GitOid, workdir: &Path) -> Result<(), GitError> {
        // checkout_tree uses overwrite_existing = true, matching --force semantics.
        crate::checkout_impl::checkout_tree(self, oid, workdir)?;
        crate::checkout_impl::set_head(self, oid)?;
        Ok(())
    }
}
