use std::borrow::Cow;

use gix_error::{ErrorExt, Exn, OptionExt, ResultExt, bail, message};
use gix_hash::ObjectId;
use gix_index::entry::Stage;
use gix_revision::spec::parse::{
    delegate,
    delegate::{PeelTo, Traversal},
};

use crate::revision::spec::parse::delegate::peel;
use crate::{
    Object, Repository,
    bstr::{BStr, ByteSlice},
    ext::ObjectIdExt,
    revision::spec::parse::{Delegate, delegate::Replacements},
};

impl delegate::Navigate for Delegate<'_> {
    fn traverse(&mut self, kind: Traversal) -> Result<(), Exn> {
        self.unset_disambiguate_call();
        self.follow_refs_to_objects_if_needed_delay_errors();

        let mut replacements = Replacements::default();
        let mut errors = Vec::<(ObjectId, Exn)>::new();
        let objs = match self.objs[self.idx].as_mut() {
            Some(objs) => objs,
            None => {
                bail!(message("Tried to navigate the commit-graph without providing an anchor first").raise_erased())
            }
        };
        let repo = self.repo;

        for obj in objs.iter() {
            match kind {
                Traversal::NthParent(num) => {
                    match self
                        .repo
                        .find_object(*obj)
                        .or_erased()
                        .and_then(|obj| obj.peel_to_commit().or_erased())
                    {
                        Ok(commit) => match commit.parent_ids().nth(num.saturating_sub(1)) {
                            Some(id) => replacements.push((*obj, id.detach())),
                            None => errors.push((
                                *obj,
                                message!(
                                    "Commit {oid} has {available} parents and parent number {desired} is out of range",
                                    oid = commit.id().shorten_or_id(),
                                    desired = num,
                                    available = commit.parent_ids().count(),
                                )
                                .raise_erased(),
                            )),
                        },
                        Err(err) => errors.push((*obj, err)),
                    }
                }
                Traversal::NthAncestor(num) => {
                    let id = match peel(repo, obj, gix_object::Kind::Commit) {
                        Ok(id) => id.attach(repo),
                        Err(err) => {
                            errors.push((*obj, err));
                            continue;
                        }
                    };
                    match id
                        .ancestors()
                        .first_parent_only()
                        .all()
                        .expect("cannot fail without sorting")
                        .skip(num)
                        .find_map(Result::ok)
                    {
                        Some(commit) => replacements.push((*obj, commit.id)),
                        None => errors.push((
                            *obj,
                                message!("Commit {oid} has {available} ancestors along the first parent and ancestor number {num} is out of range",
                                    oid = id.shorten_or_id(),
                                    available = id
                                        .ancestors()
                                        .first_parent_only()
                                        .all()
                                        .expect("cannot fail without sorting")
                                        .skip(1)
                                        .count()
                                ).raise_erased()
                        )),
                    }
                }
            }
        }

        handle_errors_and_replacements(&mut self.delayed_errors, objs, errors, &mut replacements)
    }

    fn peel_until(&mut self, kind: PeelTo<'_>) -> Result<(), Exn> {
        self.unset_disambiguate_call();
        self.follow_refs_to_objects_if_needed_delay_errors();

        let mut replacements = Replacements::default();
        let mut errors = Vec::<(ObjectId, Exn)>::new();
        let objs = self.objs[self.idx]
            .as_mut()
            .ok_or_raise_erased(|| message!("Couldn't get object at internal index {idx}", idx = self.idx))?;
        let repo = self.repo;

        match kind {
            PeelTo::ValidObject => {
                for obj in objs.iter() {
                    if let Err(err) = repo.find_object(*obj) {
                        errors.push((*obj, err.raise_erased()));
                    }
                }
            }
            PeelTo::ObjectKind(kind) => {
                let peel = |obj| peel(repo, obj, kind);
                for obj in objs.iter() {
                    match peel(obj) {
                        Ok(replace) => replacements.push((*obj, replace)),
                        Err(err) => errors.push((*obj, err)),
                    }
                }
            }
            PeelTo::Path(path) => {
                let path = to_repo_relative_path(repo, path)?;
                let path = path.as_ref();
                let lookup_path = |obj: &ObjectId| {
                    let tree_id = peel(repo, obj, gix_object::Kind::Tree)?;
                    if path.is_empty() {
                        return Ok::<_, Exn>((tree_id, gix_object::tree::EntryKind::Tree.into()));
                    }
                    let mut tree = repo.find_object(tree_id).or_erased()?.into_tree();
                    let entry = tree
                        .peel_to_entry_by_path(gix_path::from_bstr(path))
                        .or_erased()?
                        .ok_or_raise_erased(|| {
                            message!(
                                "Could not find path {path:?} in tree {tree} of parent object {object}",
                                path = path,
                                object = obj.attach(repo).shorten_or_id(),
                                tree = tree_id.attach(repo).shorten_or_id(),
                            )
                        })?;
                    Ok((entry.object_id(), entry.mode()))
                };
                for obj in objs.iter() {
                    match lookup_path(obj) {
                        Ok((replace, mode)) => {
                            if !path.is_empty() {
                                // Technically this is letting the last one win, but so be it.
                                self.paths[self.idx] = Some((path.to_owned(), mode));
                            }
                            replacements.push((*obj, replace));
                        }
                        Err(err) => errors.push((*obj, err)),
                    }
                }
            }
            PeelTo::RecursiveTagObject => {
                for oid in objs.iter() {
                    match oid.attach(repo).object().and_then(Object::peel_tags_to_end) {
                        Ok(obj) => replacements.push((*oid, obj.id)),
                        Err(err) => errors.push((*oid, err.raise_erased())),
                    }
                }
            }
        }

        handle_errors_and_replacements(&mut self.delayed_errors, objs, errors, &mut replacements)
    }

    fn find(&mut self, regex: &BStr, negated: bool) -> Result<(), Exn> {
        self.unset_disambiguate_call();
        self.follow_refs_to_objects_if_needed_delay_errors();

        #[cfg(not(feature = "revparse-regex"))]
        let matches = |message: &BStr| -> bool { message.contains_str(regex) ^ negated };
        #[cfg(feature = "revparse-regex")]
        let matches = match regex::bytes::Regex::new(regex.to_str_lossy().as_ref()) {
            Ok(compiled) => {
                let needs_regex = regex::escape(compiled.as_str()) != regex;
                move |message: &BStr| -> bool {
                    if needs_regex {
                        compiled.is_match(message) ^ negated
                    } else {
                        message.contains_str(regex) ^ negated
                    }
                }
            }
            Err(err) => {
                bail!(err.raise_erased());
            }
        };

        match self.objs[self.idx].as_mut() {
            Some(objs) => {
                let repo = self.repo;
                let mut errors = Vec::<(ObjectId, Exn)>::new();
                let mut replacements = Replacements::default();
                for oid in objs.iter() {
                    let start = match peel(repo, oid, gix_object::Kind::Commit) {
                        Ok(id) => id.attach(repo),
                        Err(err) => {
                            errors.push((*oid, err));
                            continue;
                        }
                    };
                    match start
                        .ancestors()
                        .sorting(crate::revision::walk::Sorting::ByCommitTime(Default::default()))
                        .all()
                    {
                        Ok(iter) => {
                            let mut matched = false;
                            let mut count = 0;
                            let commits = iter.map(|res| {
                                res.map_err(|err| err.raise_erased()).and_then(|commit| {
                                    commit
                                        .id()
                                        .object()
                                        .map_err(|err| err.raise_erased())
                                        .map(Object::into_commit)
                                })
                            });
                            for commit in commits {
                                count += 1;
                                match commit {
                                    Ok(commit) => {
                                        if matches(commit.message_raw_sloppy()) {
                                            replacements.push((*oid, commit.id));
                                            matched = true;
                                            break;
                                        }
                                    }
                                    Err(err) => errors.push((*oid, err)),
                                }
                            }
                            if !matched {
                                errors.push((
                                    *oid,
                                    message!(
                                        "None of {commits_searched} commits from {oid} matched {kind} {regex:?}",
                                        regex = regex,
                                        commits_searched = count,
                                        oid = oid.attach(repo).shorten_or_id(),
                                        kind = if cfg!(feature = "revparse-regex") {
                                            "regex"
                                        } else {
                                            "text"
                                        }
                                    )
                                    .raise_erased(),
                                ));
                            }
                        }
                        Err(err) => errors.push((*oid, err.raise_erased())),
                    }
                }
                handle_errors_and_replacements(&mut self.delayed_errors, objs, errors, &mut replacements)
            }
            None => {
                let references = self.repo.references().or_erased()?;
                let references = references.all().or_erased()?;
                let iter = self
                    .repo
                    .rev_walk(
                        references
                            .peeled()
                            .or_raise_erased(|| message("Couldn't configure iterator for peeling"))?
                            .filter_map(Result::ok)
                            .filter(|r| r.id().header().ok().is_some_and(|obj| obj.kind().is_commit()))
                            .filter_map(|r| r.detach().peeled),
                    )
                    .sorting(crate::revision::walk::Sorting::ByCommitTime(Default::default()))
                    .all()
                    .or_erased()?;
                let mut matched = false;
                let mut count = 0;
                let commits = iter.map(|res| {
                    res.map_err(|err| err.raise_erased()).and_then(|commit| {
                        commit
                            .id()
                            .object()
                            .map_err(|err| err.raise_erased())
                            .map(Object::into_commit)
                    })
                });
                for commit in commits {
                    count += 1;
                    match commit {
                        Ok(commit) => {
                            if matches(commit.message_raw_sloppy()) {
                                let objs = self.objs[self.idx].get_or_insert_with(Vec::new);
                                if !objs.contains(&commit.id) {
                                    objs.push(commit.id);
                                }
                                matched = true;
                                break;
                            }
                        }
                        Err(err) => self.delayed_errors.push(err),
                    }
                }
                if matched {
                    Ok(())
                } else {
                    Err(message!(
                        "None of {commits_searched} commits reached from all references matched {kind} {regex:?}",
                        regex = regex,
                        commits_searched = count,
                        kind = if cfg!(feature = "revparse-regex") {
                            "regex"
                        } else {
                            "text"
                        }
                    )
                    .raise_erased())
                }
            }
        }
    }

    fn index_lookup(&mut self, path: &BStr, stage: u8) -> Result<(), Exn> {
        let stage = match stage {
            0 => Stage::Unconflicted,
            1 => Stage::Base,
            2 => Stage::Ours,
            3 => Stage::Theirs,
            _ => unreachable!(
                "BUG: driver will not pass invalid stages (and it uses integer to avoid gix-index as dependency)"
            ),
        };
        self.unset_disambiguate_call();
        let path = to_repo_relative_path(self.repo, path)?;
        let path = path.as_ref();
        let index = self.repo.index().or_erased()?;
        match index.entry_by_path_and_stage(path, stage) {
            Some(entry) => {
                let objs = self.objs[self.idx].get_or_insert_with(Vec::new);
                if !objs.contains(&entry.id) {
                    objs.push(entry.id);
                }

                self.paths[self.idx] = Some((
                    path.to_owned(),
                    entry
                        .mode
                        .to_tree_entry_mode()
                        .unwrap_or(gix_object::tree::EntryKind::Blob.into()),
                ));
                Ok(())
            }
            None => {
                let stage_hint = [Stage::Unconflicted, Stage::Base, Stage::Ours]
                    .iter()
                    .filter(|our_stage| **our_stage != stage)
                    .find_map(|stage| index.entry_index_by_path_and_stage(path, *stage).map(|_| *stage));
                let exists = self
                    .repo
                    .workdir()
                    .is_some_and(|root| root.join(gix_path::from_bstr(path)).exists());
                Err(message!(
                    "Path {path:?} did not exist in index at stage {desired_stage}{stage_hint}{exists}",
                    exists = if exists {
                        ". It exists on disk"
                    } else {
                        ". It does not exist on disk"
                    },
                    stage_hint = stage_hint
                        .map(|actual| format!(". It does exist at stage {}", actual as u8))
                        .unwrap_or_default(),
                    desired_stage = stage as u8,
                )
                .raise_erased())
            }
        }
    }
}

/// Resolve `path` against the current working directory if it starts with `./` or `../`, and return it
/// unchanged otherwise, matching the path syntax described in `gitrevisions(7)`.
fn to_repo_relative_path<'a>(repo: &Repository, path: &'a BStr) -> Result<Cow<'a, BStr>, Exn> {
    if !(path.starts_with_str("./") || path.starts_with_str("../")) {
        return Ok(path.into());
    }
    repo.prefix()
        .or_erased()?
        .ok_or_raise_erased(|| message("Relative path syntax can't be used outside of a worktree"))?;
    repo.normalize_path(path).or_erased()
}

fn handle_errors_and_replacements(
    delayed_errors: &mut Vec<Exn>,
    objs: &mut Vec<ObjectId>,
    errors: Vec<(ObjectId, Exn)>,
    replacements: &mut Replacements,
) -> Result<(), Exn> {
    if errors.len() == objs.len() {
        delayed_errors.extend(errors.into_iter().map(|(_, err)| err));
        Err(delayed_errors
            .pop()
            .unwrap_or_else(|| message("BUG: Somehow there was no error but one was expected").raise_erased()))
    } else {
        for (obj, err) in errors {
            if let Some(pos) = objs.iter().position(|o| o == &obj) {
                objs.remove(pos);
            }
            delayed_errors.push(err);
        }
        for (find, replace) in replacements {
            if let Some(pos) = objs.iter().position(|o| o == find) {
                objs.remove(pos);
            }
            if !objs.contains(replace) {
                objs.push(*replace);
            }
        }
        Ok(())
    }
}
