use crate::OutputFormat;

pub struct Options {
    pub format: OutputFormat,
    pub file_favor: Option<gix::merge::tree::FileFavor>,
    pub tree_favor: Option<gix::merge::tree::TreeFavor>,
    pub in_memory: bool,
    pub debug: bool,
    pub message: Option<String>,
    pub update_head: bool,
}

pub(super) mod function {

    use std::collections::BTreeSet;

    use anyhow::{Context, anyhow, bail};
    use gix::{
        bstr::{BString, ByteSlice},
        merge::tree::TreatAsUnresolved,
        prelude::Write,
    };

    use super::Options;
    use crate::OutputFormat;

    pub fn tree(
        mut repo: gix::Repository,
        out: &mut dyn std::io::Write,
        err: &mut dyn std::io::Write,
        base: BString,
        ours: BString,
        theirs: BString,
        Options {
            format,
            file_favor,
            tree_favor,
            in_memory,
            debug,
            message,
            update_head,
        }: Options,
    ) -> anyhow::Result<()> {
        if format != OutputFormat::Human {
            bail!("JSON output isn't implemented yet");
        }
        if update_head && in_memory {
            bail!("`--update-head` cannot be used with `--in-memory` - cannot set head to nothing");
        }
        if update_head && message.is_none() {
            bail!("`--update-head` requires `--message`");
        }
        repo.object_cache_size_if_unset(repo.compute_object_cache_size_for_tree_diffs(&**repo.index_or_empty()?));
        if in_memory || message.is_some() {
            repo.objects.enable_object_memory();
        }
        let (base_ref, base_id) = refname_and_tree(&repo, base)?;
        let (ours_ref, ours_id) = refname_and_tree(&repo, ours)?;
        let (theirs_ref, theirs_id) = refname_and_tree(&repo, theirs)?;

        let options = repo
            .tree_merge_options()?
            .with_file_favor(file_favor)
            .with_tree_favor(tree_favor);
        let base_id_str = base_id.to_string();
        let ours_id_str = ours_id.to_string();
        let theirs_id_str = theirs_id.to_string();
        let labels = gix::merge::blob::builtin_driver::text::Labels {
            ancestor: base_ref
                .as_ref()
                .map_or(base_id_str.as_str().into(), |n| n.as_bstr())
                .into(),
            current: ours_ref
                .as_ref()
                .map_or(ours_id_str.as_str().into(), |n| n.as_bstr())
                .into(),
            other: theirs_ref
                .as_ref()
                .map_or(theirs_id_str.as_str().into(), |n| n.as_bstr())
                .into(),
        };
        let res = repo.merge_trees(base_id, ours_id, theirs_id, labels, options)?;
        let has_conflicts = !res.conflicts.is_empty();
        let has_unresolved_conflicts = res.has_unresolved_conflicts(TreatAsUnresolved::default());
        if message.is_some() && has_unresolved_conflicts {
            write_unresolved_conflict_paths(err, &res.conflicts)?;
            if debug {
                writeln!(err, "{:#?}", res.conflicts)?;
            }
            bail!("Tree conflicted, refusing to write commit");
        }

        let tree_id = {
            let _span = gix::trace::detail!("Writing merged tree");
            let mut written = 0;
            let tree_id = res
                .tree
                .detach()
                .write(|tree| {
                    written += 1;
                    repo.write(tree)
                })
                .map_err(|err| anyhow!("{err}"))?;
            writeln!(out, "{tree_id} (wrote {written} trees)")?;
            tree_id
        };

        let conflicts = res.conflicts;
        if message.is_some() && !in_memory {
            persist_in_memory_objects(&mut repo)?;
        }

        if let Some(message) = message {
            let head_id = repo.head_id()?;
            let commit_id = if update_head {
                let commit_id = repo.commit("HEAD", message, tree_id, Some(head_id))?;
                let mut index = repo.index_from_tree(&tree_id)?;
                index.write(Default::default())?;
                commit_id
            } else {
                repo.new_commit(message, tree_id, Some(head_id))?.id()
            };
            writeln!(out, "{commit_id} (commit)")?;
            return Ok(());
        }

        if debug {
            writeln!(err, "{conflicts:#?}")?;
        }
        if has_conflicts {
            writeln!(err, "{} possibly resolved conflicts", conflicts.len())?;
        }
        if has_unresolved_conflicts {
            bail!("Tree conflicted")
        }
        Ok(())
    }

    fn persist_in_memory_objects(repo: &mut gix::Repository) -> anyhow::Result<()> {
        let objects = repo.objects.take_object_memory().expect("always write in memory first");
        for (_id, (kind, data)) in objects.iter() {
            repo.write_buf(*kind, data).map_err(|err| anyhow!("{err}"))?;
        }
        Ok(())
    }

    fn write_unresolved_conflict_paths(
        err: &mut dyn std::io::Write,
        conflicts: &[gix::merge::tree::Conflict],
    ) -> std::io::Result<()> {
        let how = TreatAsUnresolved::default();
        let mut paths = BTreeSet::new();
        for conflict in conflicts.iter().filter(|conflict| conflict.is_unresolved(how)) {
            let (ours, theirs) = conflict.changes_in_resolution();
            for path in [
                ours.source_location(),
                ours.location(),
                theirs.source_location(),
                theirs.location(),
            ] {
                if !path.is_empty() {
                    paths.insert(path);
                }
            }
        }
        for path in paths {
            err.write_all(path.as_ref())?;
            err.write_all(b"\n")?;
        }
        Ok(())
    }

    fn refname_and_tree(
        repo: &gix::Repository,
        revspec: BString,
    ) -> anyhow::Result<(Option<BString>, gix::hash::ObjectId)> {
        let spec = repo.rev_parse(revspec.as_bstr())?;
        let tree_id = spec
            .single()
            .context("Expected revspec to expand to a single rev only")?
            .object()?
            .peel_to_tree()?
            .id;
        let refname = spec.first_reference().map(|r| r.name.shorten().as_bstr().to_owned());
        Ok((refname, tree_id))
    }
}
