#[cfg(feature = "seaography")]
mod private {
    use heck::ToLowerCamelCase;
    use proc_macro_crate::{FoundCrate, crate_name};
    use proc_macro2::{Ident, Span, TokenStream};
    use quote::{quote, quote_spanned};

    use crate::derives::attributes::related_attr;

    enum Error {
        InputNotEnum,
        InvalidEntityPath,
        Syn(syn::Error),
    }

    struct DeriveRelatedEntity {
        entity_ident: TokenStream,
        ident: syn::Ident,
        variants: syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
    }

    impl DeriveRelatedEntity {
        fn new(input: syn::DeriveInput) -> Result<Self, Error> {
            let sea_attr = related_attr::SeaOrm::try_from_attributes(&input.attrs)
                .map_err(Error::Syn)?
                .unwrap_or_default();

            let ident = input.ident;
            let entity_ident = match sea_attr.entity.as_ref().map(Self::parse_lit_string) {
                Some(entity_ident) => entity_ident.map_err(|_| Error::InvalidEntityPath)?,
                None => quote! { Entity },
            };

            let variants = match input.data {
                syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
                _ => return Err(Error::InputNotEnum),
            };

            Ok(DeriveRelatedEntity {
                entity_ident,
                ident,
                variants,
            })
        }

        fn expand(&self) -> syn::Result<TokenStream> {
            let ident = &self.ident;
            let entity_ident = &self.entity_ident;

            let mut get_relation_impl = Vec::new();
            let mut get_relation_name_impl = Vec::new();
            let mut get_related_entity_filter_impl = Vec::new();

            for variant in &self.variants {
                let attr = related_attr::SeaOrm::from_attributes(&variant.attrs)?;

                let enum_name = &variant.ident;

                let target_entity = attr
                    .entity
                    .as_ref()
                    .map(Self::parse_lit_string)
                    .ok_or_else(|| {
                        syn::Error::new_spanned(variant, "Missing value for 'entity'")
                    })??;

                let def = match attr.def {
                    Some(def) => Some(Self::parse_lit_string(&def).map_err(|_| {
                        syn::Error::new_spanned(variant, "Missing value for 'def'")
                    })?),
                    None => None,
                };

                let name = enum_name.to_string().to_lower_camel_case();

                get_relation_impl.push(if let Some(def) = &def {
                    quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) }
                } else {
                    quote! { Self::#enum_name => via_builder.get_relation::<#entity_ident, #target_entity>(#name) }
                });
                get_relation_name_impl.push(if let Some(def) = &def {
                    quote! { Self::#enum_name => builder.get_relation_name::<#entity_ident, #target_entity>(#name, #def) }
                } else {
                    quote! { Self::#enum_name => via_builder.get_relation_name::<#entity_ident, #target_entity>(#name) }
                });
                get_related_entity_filter_impl.push(if let Some(def) = &def {
                    quote! { Self::#enum_name => builder.get_relation::<#entity_ident, #target_entity>(#name, #def) }
                } else {
                    quote! { Self::#enum_name => builder.get_relation_via::<#entity_ident, #target_entity>(#name) }
                });
            }

            // Get the path of the `async-graphql` on the application's Cargo.toml
            let async_graphql_crate = match crate_name("async-graphql") {
                // if found, use application's `async-graphql`
                Ok(FoundCrate::Name(name)) => {
                    let ident = Ident::new(&name, Span::call_site());
                    quote! { #ident }
                }
                Ok(FoundCrate::Itself) => quote! { async_graphql },
                // if not, then use the `async-graphql` re-exported by `seaography`
                Err(_) => quote! { seaography::async_graphql },
            };

            Ok(quote! {
                impl seaography::RelationBuilder for #ident {
                    fn get_relation(&self, context: & 'static seaography::BuilderContext) -> #async_graphql_crate::dynamic::Field {
                        let builder = seaography::EntityObjectRelationBuilder { context };
                        let via_builder = seaography::EntityObjectViaRelationBuilder { context };
                        match self {
                            #(#get_relation_impl,)*
                            _ => panic!("No relations for this entity"),
                        }
                    }
                    fn get_relation_name(&self, context: & 'static seaography::BuilderContext) -> String {
                        let builder = seaography::EntityObjectRelationBuilder { context };
                        let via_builder = seaography::EntityObjectViaRelationBuilder { context };
                        match self {
                            #(#get_relation_name_impl,)*
                            _ => panic!("No relations for this entity"),
                        }
                    }
                    fn get_related_entity_filter(&self, context: & 'static seaography::BuilderContext) -> seaography::RelatedEntityFilterField {
                        let builder = seaography::RelatedEntityFilterBuilder { context };
                        match self {
                            #(#get_related_entity_filter_impl,)*
                            _ => panic!("No relations for this entity"),
                        }
                    }
                }
            })
        }

        fn parse_lit_string(lit: &syn::Lit) -> syn::Result<TokenStream> {
            match lit {
                syn::Lit::Str(lit_str) => lit_str
                    .value()
                    .parse()
                    .map_err(|_| syn::Error::new_spanned(lit, "attribute not valid")),
                _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")),
            }
        }
    }

    /// Method to derive a Related enumeration
    pub fn expand_derive_related_entity(input: syn::DeriveInput) -> syn::Result<TokenStream> {
        let ident_span = input.ident.span();

        match DeriveRelatedEntity::new(input) {
            Ok(model) => model.expand(),
            Err(Error::InputNotEnum) => Ok(quote_spanned! {
                ident_span => compile_error!("you can only derive DeriveRelation on enums");
            }),
            Err(Error::InvalidEntityPath) => Ok(quote_spanned! {
                ident_span => compile_error!("invalid attribute value for 'entity'");
            }),
            Err(Error::Syn(err)) => Err(err),
        }
    }
}

#[cfg(not(feature = "seaography"))]
mod private {
    use proc_macro2::TokenStream;

    pub fn expand_derive_related_entity(_: syn::DeriveInput) -> syn::Result<TokenStream> {
        Ok(TokenStream::new())
    }
}

pub use private::*;
