use super::*;
use crate::{
    JoinType, Paginator, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, QueryTrait, Related,
    SelectC, SelectFive, SelectFour, SelectFourMany, Topology, TopologyStar,
    combine::{SelectD, prepare_select_col},
};

impl<E, F, G, H, TOP> SelectFour<E, F, G, H, TOP>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
    TOP: Topology,
{
    pub(crate) fn new(query: SelectStatement) -> Self {
        Self::new_without_prepare(query).prepare_select()
    }

    pub(crate) fn new_without_prepare(query: SelectStatement) -> Self {
        Self {
            query,
            entity: PhantomData,
        }
    }

    fn prepare_select(mut self) -> Self {
        prepare_select_col::<H, _, _>(&mut self, SelectD);
        self
    }

    /// Left Join with a Related Entity and select all Entities.
    pub fn find_also<T, I>(self, _: T, _: I) -> SelectFive<E, F, G, H, I, TopologyStar>
    where
        I: EntityTrait,
        T: EntityTrait + Related<I>,
    {
        SelectFive::new(
            self.join_join(JoinType::LeftJoin, T::to(), T::via())
                .into_query(),
        )
    }
}

macro_rules! impl_query_trait {
    ( $trait: ident ) => {
        impl<E, F, G, H, TOP> $trait for SelectFour<E, F, G, H, TOP>
        where
            E: EntityTrait,
            F: EntityTrait,
            G: EntityTrait,
            H: EntityTrait,
            TOP: Topology,
        {
            type QueryStatement = SelectStatement;

            fn query(&mut self) -> &mut SelectStatement {
                &mut self.query
            }
        }
    };
}

impl<E, F, G, H, TOP> QueryTrait for SelectFour<E, F, G, H, TOP>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
    TOP: Topology,
{
    type QueryStatement = SelectStatement;
    fn query(&mut self) -> &mut SelectStatement {
        &mut self.query
    }
    fn as_query(&self) -> &SelectStatement {
        &self.query
    }
    fn into_query(self) -> SelectStatement {
        self.query
    }
}

impl_query_trait!(QuerySelect);
impl_query_trait!(QueryFilter);
impl_query_trait!(QueryOrder);

impl<M, N, O, P> SelectorTrait for SelectFourModel<M, N, O, P>
where
    M: FromQueryResult + Sized,
    N: FromQueryResult + Sized,
    O: FromQueryResult + Sized,
    P: FromQueryResult + Sized,
{
    type Item = (M, Option<N>, Option<O>, Option<P>);

    fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr> {
        Ok((
            M::from_query_result(&res, SelectA.as_str())?,
            N::from_query_result_optional(&res, SelectB.as_str())?,
            O::from_query_result_optional(&res, SelectC.as_str())?,
            P::from_query_result_optional(&res, SelectD.as_str())?,
        ))
    }
}

impl<E, F, G, H, TOP> SelectFour<E, F, G, H, TOP>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
    TOP: Topology,
{
    /// Perform a conversion into a [SelectFourModel]
    pub fn into_model<M, N, O, P>(self) -> Selector<SelectFourModel<M, N, O, P>>
    where
        M: FromQueryResult,
        N: FromQueryResult,
        O: FromQueryResult,
        P: FromQueryResult,
    {
        Selector {
            query: self.query,
            selector: PhantomData,
        }
    }

    /// Perform a conversion into a [SelectFourModel] with [PartialModel](PartialModelTrait)
    pub fn into_partial_model<M, N, O, P>(self) -> Selector<SelectFourModel<M, N, O, P>>
    where
        M: PartialModelTrait,
        N: PartialModelTrait,
        O: PartialModelTrait,
        P: PartialModelTrait,
    {
        let select = QuerySelect::select_only(self);
        let select = M::select_cols(select);
        let select = N::select_cols(select);
        let select = O::select_cols(select);
        let select = P::select_cols(select);
        select.into_model::<M, N, O, P>()
    }

    /// Convert the Models into JsonValue
    #[cfg(feature = "with-json")]
    pub fn into_json(
        self,
    ) -> Selector<SelectFourModel<JsonValue, JsonValue, JsonValue, JsonValue>> {
        Selector {
            query: self.query,
            selector: PhantomData,
        }
    }

    /// Get one Model from the Select query
    pub async fn one<C>(
        self,
        db: &C,
    ) -> Result<
        Option<(
            E::Model,
            Option<F::Model>,
            Option<G::Model>,
            Option<H::Model>,
        )>,
        DbErr,
    >
    where
        C: ConnectionTrait,
    {
        self.into_model().one(db).await
    }

    /// Get all Models from the Select query
    pub async fn all<C>(
        self,
        db: &C,
    ) -> Result<
        Vec<(
            E::Model,
            Option<F::Model>,
            Option<G::Model>,
            Option<H::Model>,
        )>,
        DbErr,
    >
    where
        C: ConnectionTrait,
    {
        self.into_model().all(db).await
    }

    /// Stream the results of a Select operation on a Model
    #[cfg(feature = "stream")]
    pub async fn stream<'a: 'b, 'b, C>(
        self,
        db: &'a C,
    ) -> Result<
        impl Stream<
            Item = Result<
                (
                    E::Model,
                    Option<F::Model>,
                    Option<G::Model>,
                    Option<H::Model>,
                ),
                DbErr,
            >,
        > + 'b,
        DbErr,
    >
    where
        C: ConnectionTrait + StreamTrait + Send,
    {
        self.into_model().stream(db).await
    }

    /// Stream the result of the operation with PartialModel
    #[cfg(feature = "stream")]
    pub async fn stream_partial_model<'a: 'b, 'b, C, M, N, O, P>(
        self,
        db: &'a C,
    ) -> Result<
        impl Stream<Item = Result<(M, Option<N>, Option<O>, Option<P>), DbErr>> + 'b + Send,
        DbErr,
    >
    where
        C: ConnectionTrait + StreamTrait + Send,
        M: PartialModelTrait + Send + 'b,
        N: PartialModelTrait + Send + 'b,
        O: PartialModelTrait + Send + 'b,
        P: PartialModelTrait + Send + 'b,
    {
        self.into_partial_model().stream(db).await
    }
}

impl<E, F, G, H, TOP> SelectFour<E, F, G, H, TOP>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
    TOP: Topology,
{
    /// Consolidate query result by first model. Only the star topology
    /// (E -> F, E -> G, E -> H) provides an `all()` impl.
    pub fn consolidate(self) -> SelectFourMany<E, F, G, H, TOP> {
        SelectFourMany {
            query: self.query,
            entity: self.entity,
        }
    }
}

impl<E, F, G, H, TOP> SelectFourMany<E, F, G, H, TOP>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
    TOP: Topology,
{
    /// Performs a conversion to [Selector]
    fn into_model<M, N, O, P>(self) -> Selector<SelectFourModel<M, N, O, P>>
    where
        M: FromQueryResult,
        N: FromQueryResult,
        O: FromQueryResult,
        P: FromQueryResult,
    {
        Selector {
            query: self.query,
            selector: PhantomData,
        }
    }
}

impl<E, F, G, H> SelectFourMany<E, F, G, H, TopologyStar>
where
    E: EntityTrait,
    F: EntityTrait,
    G: EntityTrait,
    H: EntityTrait,
{
    /// Execute query and consolidate rows by E
    pub async fn all<C>(
        self,
        db: &C,
    ) -> Result<Vec<(E::Model, Vec<F::Model>, Vec<G::Model>, Vec<H::Model>)>, DbErr>
    where
        C: ConnectionTrait,
    {
        let rows = self.into_model().all(db).await?;
        Ok(consolidate_query_result_quad_star::<E, F, G, H>(rows))
    }
}

impl<'db, C, EE, FF, GG, HH, E, F, G, H, TOP> PaginatorTrait<'db, C> for SelectFour<E, F, G, H, TOP>
where
    C: ConnectionTrait,
    E: EntityTrait<Model = EE>,
    F: EntityTrait<Model = FF>,
    G: EntityTrait<Model = GG>,
    H: EntityTrait<Model = HH>,
    EE: FromQueryResult + Sized + Send + Sync + 'db,
    FF: FromQueryResult + Sized + Send + Sync + 'db,
    GG: FromQueryResult + Sized + Send + Sync + 'db,
    HH: FromQueryResult + Sized + Send + Sync + 'db,
    TOP: Topology,
{
    type Selector = SelectFourModel<EE, FF, GG, HH>;

    fn paginate(self, db: &'db C, page_size: u64) -> Paginator<'db, C, Self::Selector> {
        self.into_model().paginate(db, page_size)
    }
}
