use std::collections::{BTreeMap, BTreeSet};

use crate::git;
use crate::git::Oid;
use crate::git::fmt::Qualified;
use crate::git::raw::ErrorExt as _;
use crate::prelude::Did;

use super::{FoundObjects, GraphAheadBehind, MergeBase, Object};

/// Find objects for the canonical computation.
///
/// Typically implemented by a Git repository.
pub trait FindObjects {
    /// Find the objects for the given [`Qualified`] reference name, for each
    /// [`Did`]'s namespace.
    ///
    /// The resulting [`FoundObjects`] includes all objects that were found, the
    /// references that were missing, and the objects that were missing (if the
    /// reference was found).
    fn find_objects<'a, 'b, I>(
        &self,
        refname: &Qualified<'a>,
        dids: I,
    ) -> Result<FoundObjects, FindObjectsError>
    where
        I: Iterator<Item = &'b Did>;
}

/// Error produced by the [`FindObjects::find_objects`] method.
#[derive(Debug, thiserror::Error)]
pub enum FindObjectsError {
    #[error(transparent)]
    InvalidObjectType(#[from] InvalidObjectType),
    #[error(transparent)]
    MissingObject(#[from] MissingObject),
    #[error("failed to find object {oid} due to: {source}")]
    FindObject {
        oid: Oid,
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error("failed to find reference {refname} due to: {source}")]
    FindReference {
        refname: git::fmt::Namespaced<'static>,
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    #[error("failed to find objects")]
    Other {
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
}

impl FindObjectsError {
    pub fn find_object<E>(oid: Oid, err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        Self::FindObject {
            oid,
            source: Box::new(err),
        }
    }

    pub fn find_reference<E>(refname: git::fmt::Namespaced<'static>, err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        Self::FindReference {
            refname,
            source: Box::new(err),
        }
    }

    pub fn missing_object<E>(did: Did, oid: Oid, err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        MissingObject {
            did,
            commit: oid,
            source: Box::new(err),
        }
        .into()
    }

    pub fn invalid_object_type(did: Did, oid: Oid, kind: Option<String>) -> Self {
        InvalidObjectType { did, oid, kind }.into()
    }

    pub fn other<E>(err: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        Self::Other {
            source: Box::new(err),
        }
    }
}

#[derive(Debug, thiserror::Error)]
#[error("the object {oid} for {did} is of unexpected type {kind:?}")]
pub struct InvalidObjectType {
    did: Did,
    oid: Oid,
    kind: Option<String>,
}

#[derive(Debug, thiserror::Error)]
#[error("the commit {commit} for {did} is missing")]
pub struct MissingObject {
    did: Did,
    commit: Oid,
    source: Box<dyn std::error::Error + Send + Sync + 'static>,
}

/// Find the merge base of two commits.
///
/// Typically implemented by a Git repository.
pub trait FindMergeBase {
    /// Produce the [`MergeBase`] of commits `a` and `b`.
    fn merge_base(&self, a: Oid, b: Oid) -> Result<MergeBase, MergeBaseError>;
}

#[derive(Debug, thiserror::Error)]
#[error("failed to find merge base for {a} and {b} due to: {source}")]
pub struct MergeBaseError {
    a: Oid,
    b: Oid,
    source: Box<dyn std::error::Error + Send + Sync + 'static>,
}

impl MergeBaseError {
    pub fn new<E>(a: Oid, b: Oid, source: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        Self {
            a,
            b,
            source: Box::new(source),
        }
    }
}

/// Calculate the ancestry of two commits.
///
/// Typically implemented by a Git repository.
pub trait Ancestry {
    /// Produce the [`GraphAheadBehind`] of `commit` and `upstream`.
    ///
    /// The result should provide how many commits are ahead and behind when
    /// comparing the `commit` and `upstream`.
    fn graph_ahead_behind(
        &self,
        commit: Oid,
        upstream: Oid,
    ) -> Result<GraphAheadBehind, GraphDescendant>;
}

#[derive(Debug, thiserror::Error)]
#[error("failed to check if {commit} is an ancestor of {upstream} due to: {source}")]
pub struct GraphDescendant {
    commit: Oid,
    upstream: Oid,
    source: Box<dyn std::error::Error + Send + Sync + 'static>,
}

// ===========================================
// `git2` implementations of the above effects
// ===========================================

impl FindMergeBase for git::raw::Repository {
    fn merge_base(&self, a: Oid, b: Oid) -> Result<MergeBase, MergeBaseError> {
        self.merge_base(a.into(), b.into())
            .map_err(|err| MergeBaseError {
                a,
                b,
                source: Box::new(err),
            })
            .map(|base| MergeBase {
                a,
                b,
                base: base.into(),
            })
    }
}

impl Ancestry for git::raw::Repository {
    fn graph_ahead_behind(
        &self,
        commit: Oid,
        upstream: Oid,
    ) -> Result<GraphAheadBehind, GraphDescendant> {
        self.graph_ahead_behind(commit.into(), upstream.into())
            .map_err(|err| GraphDescendant {
                commit,
                upstream,
                source: Box::new(err),
            })
            .map(|(ahead, behind)| GraphAheadBehind { ahead, behind })
    }
}

impl FindObjects for git::raw::Repository {
    fn find_objects<'a, 'b, I>(
        &self,
        refname: &Qualified,
        dids: I,
    ) -> Result<FoundObjects, FindObjectsError>
    where
        I: Iterator<Item = &'b Did>,
    {
        let mut objects = BTreeMap::new();
        let mut missing_refs = BTreeSet::new();
        let mut missing_objects = BTreeMap::new();
        for did in dids {
            let name = &refname.with_namespace(did.as_key().into());
            let reference = match self.find_reference(name.as_str()) {
                Ok(reference) => reference,
                Err(e) if e.is_not_found() => {
                    missing_refs.insert(name.to_owned());
                    continue;
                }
                Err(e) => {
                    return Err(FindObjectsError::find_reference(name.to_owned(), e));
                }
            };
            let Some(oid) = reference.target().map(Oid::from) else {
                log::warn!(target: "radicle", "Missing target for reference `{name}`");
                continue;
            };
            let object = match self.find_object(oid.into(), None) {
                Ok(object) => Object::new(&object).ok_or_else(|| {
                    FindObjectsError::invalid_object_type(
                        *did,
                        oid,
                        object.kind().map(|kind| kind.to_string()),
                    )
                }),
                Err(err) if err.is_not_found() => {
                    missing_objects.insert(*did, oid);
                    continue;
                }
                Err(err) => Err(FindObjectsError::find_object(oid, err)),
            };
            objects.insert(*did, object?);
        }
        Ok(FoundObjects {
            objects,
            missing_refs,
            missing_objects,
        })
    }
}
