use super::active_model::DeriveActiveModel;
use super::attributes::compound_attr;
use super::util::{extract_compound_entity, field_not_ignored_compound, is_compound_field};
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{Attribute, Data, Expr, Fields, Type};

pub fn expand_derive_active_model_ex(
    ident: &Ident,
    data: &Data,
    attrs: &[Attribute],
) -> syn::Result<TokenStream> {
    let mut compact = false;
    let mut model_fields = Vec::new();
    let mut ignored_model_fields = Vec::new();
    let mut field_types: Vec<Type> = Vec::new();
    let mut scalar_fields = Vec::new();
    let mut compound_fields = Vec::new();
    let mut belongs_to_fields = Vec::new();
    let mut has_one_fields = Vec::new();
    let mut has_many_fields = Vec::new();
    let mut has_many_via_fields = Vec::new();

    attrs
        .iter()
        .filter(|attr| attr.path().is_ident("sea_orm"))
        .try_for_each(|attr| {
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("compact_model") {
                    compact = true;
                } else {
                    // Reads the value expression to advance the parse stream.
                    let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
                }
                Ok(())
            })
        })?;

    if let Data::Struct(item_struct) = &data {
        if let Fields::Named(fields) = &item_struct.fields {
            for field in fields.named.iter() {
                if let Some(ident) = &field.ident {
                    if field_not_ignored_compound(field) {
                        let field_type = &field.ty;
                        let field_type = quote! { #field_type }
                            .to_string() // e.g.: "Option < String >"
                            .replace(' ', ""); // Remove spaces

                        let ty = if is_compound_field(&field_type) {
                            compound_fields.push(ident);
                            let entity_path = extract_compound_entity(&field_type);

                            if !compact {
                                let compound_attrs =
                                    compound_attr::SeaOrm::from_attributes(&field.attrs)?;
                                if compound_attrs.relation_enum.is_none() {
                                    if compound_attrs.belongs_to.is_some() {
                                        belongs_to_fields.push(ident.clone());
                                    } else if compound_attrs.has_one.is_some() {
                                        has_one_fields.push(ident.clone());
                                    } else if compound_attrs.has_many.is_some()
                                        && compound_attrs.via.is_none()
                                    {
                                        has_many_fields.push(ident.clone());
                                    } else if compound_attrs.has_many.is_some()
                                        && compound_attrs.via.is_some()
                                    {
                                        has_many_via_fields.push((
                                            ident.clone(),
                                            compound_attrs.via.as_ref().unwrap().value(),
                                        ));
                                    }
                                }
                            }

                            if field_type.starts_with("HasOne<") {
                                syn::parse_str(&format!("HasOneModel < {entity_path} >"))?
                            } else {
                                syn::parse_str(&format!("HasManyModel < {entity_path} >"))?
                            }
                        } else {
                            scalar_fields.push(ident);
                            syn::parse_str(&format!("sea_orm::ActiveValue < {field_type} >"))?
                        };
                        model_fields.push(ident);
                        field_types.push(ty);
                    } else {
                        ignored_model_fields.push(ident);
                    }
                }
            }
        }
    }

    let active_model_trait_methods =
        DeriveActiveModel::new(ident, data)?.impl_active_model_trait_methods();

    let active_model_action = expand_active_model_action(
        &belongs_to_fields,
        &has_one_fields,
        &has_many_fields,
        &has_many_via_fields,
    );

    let active_model_setters = expand_active_model_setters(data)?;

    let mut is_changed_expr = quote!(false);

    for field in scalar_fields.iter() {
        is_changed_expr.extend(quote!(|| self.#field.is_set()));
    }
    for field in compound_fields.iter() {
        is_changed_expr.extend(quote!(|| self.#field.is_changed()));
    }

    Ok(quote! {
        #[doc = " Generated by sea-orm-macros"]
        #[derive(Clone, Debug, PartialEq)]
        pub struct ActiveModelEx {
            #(
                #[doc = " Generated by sea-orm-macros"]
                pub #model_fields: #field_types
            ),*
        }

        impl ActiveModel {
            #[doc = " Generated by sea-orm-macros"]
            pub fn into_ex(self) -> ActiveModelEx {
                self.into()
            }
        }

        #[automatically_derived]
        impl sea_orm::ActiveModelTrait for ActiveModelEx {
            type Entity = Entity;

            #active_model_trait_methods

            /// Returns true if any field is set or changed. This is recursive.
            fn is_changed(&self) -> bool {
                #is_changed_expr
            }

            fn default() -> Self {
                <ActiveModel as sea_orm::ActiveModelBehavior>::new().into()
            }
        }

        impl ActiveModelEx {
            #[doc = " Generated by sea-orm-macros"]
            pub fn new() -> Self {
                <Self as sea_orm::ActiveModelTrait>::default()
            }

            #active_model_action

            #active_model_setters
        }

        #[automatically_derived]
        impl std::default::Default for ActiveModelEx {
            fn default() -> Self {
                <Self as sea_orm::ActiveModelTrait>::default()
            }
        }

        #[automatically_derived]
        impl std::convert::From<ActiveModel> for ActiveModelEx {
            fn from(m: ActiveModel) -> Self {
                Self {
                    #(#scalar_fields: m.#scalar_fields,)*
                    #(#compound_fields: Default::default(),)*
                }
            }
        }

        #[automatically_derived]
        impl std::convert::From<ActiveModelEx> for ActiveModel {
            fn from(m: ActiveModelEx) -> Self {
                Self {
                    #(#scalar_fields: m.#scalar_fields,)*
                }
            }
        }

        #[automatically_derived]
        impl std::convert::From<ModelEx> for ActiveModelEx {
            fn from(m: ModelEx) -> Self {
                Self {
                    #(#scalar_fields: sea_orm::ActiveValue::Unchanged(m.#scalar_fields),)*
                    #(#compound_fields: m.#compound_fields.into_active_model(),)*
                }
            }
        }

        #[automatically_derived]
        impl std::convert::TryFrom<ActiveModelEx> for ModelEx {
            type Error = sea_orm::DbErr;
            fn try_from(a: ActiveModelEx) -> Result<Self, sea_orm::DbErr> {
                #(if a.#scalar_fields.is_not_set() {
                    return Err(sea_orm::DbErr::AttrNotSet(stringify!(#scalar_fields).to_owned()));
                })*
                Ok(
                    Self {
                        #(#scalar_fields: a.#scalar_fields.unwrap(),)*
                        #(#compound_fields: a.#compound_fields.try_into_model()?,)*
                        #(#ignored_model_fields: Default::default(),)*
                    }
                )
            }
        }

        #[automatically_derived]
        impl sea_orm::IntoActiveModel<ActiveModelEx> for ModelEx {
            fn into_active_model(self) -> ActiveModelEx {
                self.into()
            }
        }

        #[automatically_derived]
        impl sea_orm::TryIntoModel<ModelEx> for ActiveModelEx {
            fn try_into_model(self) -> Result<ModelEx, sea_orm::DbErr> {
                self.try_into()
            }
        }

        impl Model {
            #[doc = " Generated by sea-orm-macros"]
            pub async fn cascade_delete<'a, C>(self, db: &'a C) -> Result<sea_orm::DeleteResult, sea_orm::DbErr>
            where
                C: sea_orm::TransactionTrait,
            {
                self.into_ex().delete(db).await
            }
        }

        impl ModelEx {
            #[doc = " Generated by sea-orm-macros"]
            pub async fn delete<'a, C>(self, db: &'a C) -> Result<sea_orm::DeleteResult, sea_orm::DbErr>
            where
                C: sea_orm::TransactionTrait,
            {
                let active_model: ActiveModelEx = self.into();
                active_model.delete(db).await
            }
        }

        impl ActiveModel {
            #[doc = " Generated by sea-orm-macros"]
            pub fn builder() -> ActiveModelEx {
                ActiveModelEx::new()
            }
        }
    })
}

fn expand_active_model_action(
    belongs_to: &[Ident],
    has_one: &[Ident],
    has_many: &[Ident],
    has_many_via: &[(Ident, String)],
) -> TokenStream {
    let mut belongs_to_action = TokenStream::new();
    let mut belongs_to_after_action = TokenStream::new();
    let mut has_one_before_action = TokenStream::new();
    let mut has_one_action = TokenStream::new();
    let mut has_one_delete = TokenStream::new();
    let mut has_many_before_action = TokenStream::new();
    let mut has_many_action = TokenStream::new();
    let mut has_many_delete = TokenStream::new();
    let mut has_many_via_before_action = TokenStream::new();
    let mut has_many_via_action = TokenStream::new();
    let mut has_many_via_delete = TokenStream::new();

    for field in belongs_to {
        belongs_to_action.extend(quote! {
            let #field = if let Some(model) = self.#field.take() {
                if model.is_update() {
                    // has primary key
                    self.set_parent_key(&model)?;
                    if model.is_changed() {
                        let model = Box::pin(model.action(action, db)).await?;
                        Some(model)
                    } else {
                        Some(model)
                    }
                } else {
                    // new model
                    let model = Box::pin(model.action(action, db)).await?;
                    self.set_parent_key(&model)?;
                    Some(model)
                }
            } else {
                None
            };
        });

        belongs_to_after_action.extend(quote! {
            if let Some(#field) = #field {
                model.#field = HasOneModel::set(#field);
            }
        });
    }

    let delete_associated_model = quote! {
        let mut item = item.into_active_model();
        if item.clear_parent_key::<Entity>()? {
            item.update(db).await?;
        } else {
            deleted.merge(item.into_ex().delete(db).await?); // deep delete
        }
    };

    for field in has_one {
        has_one_before_action.extend(quote! {
            let #field = self.#field.take();
        });

        has_one_action.extend(quote! {
            if let Some(mut #field) = #field {
                if #field.is_changed() {
                    #field.set_parent_key(&model)?;
                    model.#field = HasOneModel::set(Box::pin(#field.action(action, db)).await?);
                } else {
                    model.#field = HasOneModel::set(#field);
                }
            }
        });

        has_one_delete.extend(quote! {
            if let Some(item) = self.find_related_of(self.#field.empty_slice()).one(db).await? {
                #delete_associated_model
            }
        });
    }

    for field in has_many {
        has_many_before_action.extend(quote! {
            let #field = self.#field.take();
        });

        has_many_action.extend(quote! {
            if #field.is_replace() {
                for item in model.find_related_of(#field.as_slice()).all(db).await? {
                    if !#field.find(&item) {
                        #delete_associated_model
                    }
                }
            }
            model.#field = #field.empty_holder();
            for mut #field in #field.into_vec() {
                if #field.is_changed() {
                    #field.set_parent_key(&model)?;
                    model.#field.push(Box::pin(#field.action(action, db)).await?);
                } else {
                    model.#field.push(#field);
                }
            }
        });

        has_many_delete.extend(quote! {
            for item in self.find_related_of(self.#field.as_slice()).all(db).await? {
                #delete_associated_model
            }
        });
    }

    for (field, via_entity) in has_many_via {
        let mut via_entity = via_entity.as_str();
        if let Some((prefix, _)) = via_entity.split_once("::") {
            via_entity = prefix;
        }

        let related_entity: TokenStream = format!("super::{via_entity}::Entity").parse().unwrap();

        has_many_via_before_action.extend(quote! {
            let #field = self.#field.take();
        });

        has_many_via_action.extend(quote! {
            model.#field = #field.empty_holder();
            for item in #field.into_vec() {
                if item.is_update() {
                    // has primary key
                    if item.is_changed() {
                        model.#field.push(Box::pin(item.action(action, db)).await?);
                    } else {
                        model.#field.push(item);
                    }
                } else {
                    // new model
                    model.#field.push(Box::pin(item.action(action, db)).await?);
                }
            }
            model.establish_links(
                #related_entity,
                model.#field.as_slice(),
                model.#field.is_replace(),
                db
            ).await?;
        });

        has_many_via_delete.extend(quote! {
            let mut to_delete = Vec::new();
            for item in Entity::find_related_rev::<#related_entity>()
                .belongs_to_active_model(&self)
                .all(db)
                .await?
            {
                to_delete.push(item.get_primary_key_value());
            }
            if !to_delete.is_empty() {
                deleted.merge(
                    #related_entity::delete_many()
                        .filter_by_value_tuples(&to_delete)
                        .exec(db)
                        .await?
                );
            }
        });
    }

    quote! {
        #[doc = " Generated by sea-orm-macros"]
        pub async fn insert<'a, C>(self, db: &'a C) -> Result<ModelEx, sea_orm::DbErr>
        where
            C: sea_orm::TransactionTrait,
        {
            let active_model = self.action(sea_orm::ActiveModelAction::Insert, db).await?;
            active_model.try_into()
        }

        #[doc = " Generated by sea-orm-macros"]
        pub async fn update<'a, C>(self, db: &'a C) -> Result<ModelEx, sea_orm::DbErr>
        where
            C: sea_orm::TransactionTrait,
        {
            let active_model = self.action(sea_orm::ActiveModelAction::Update, db).await?;
            active_model.try_into()
        }

        #[doc = " Generated by sea-orm-macros"]
        pub async fn save<'a, C>(self, db: &'a C) -> Result<Self, sea_orm::DbErr>
        where
            C: sea_orm::TransactionTrait,
        {
            self.action(sea_orm::ActiveModelAction::Save, db).await
        }

        #[doc = " Generated by sea-orm-macros"]
        pub async fn delete<'a, C>(self, db: &'a C) -> Result<sea_orm::DeleteResult, sea_orm::DbErr>
        where
            C: sea_orm::TransactionTrait,
        {
            use sea_orm::{IntoActiveModel, TransactionSession};

            let txn = db.begin().await?;
            let db = &txn;
            let mut deleted = sea_orm::DeleteResult::empty();

            #has_one_delete
            #has_many_delete
            #has_many_via_delete

            let model: ActiveModel = self.into();
            deleted.merge(model.delete(db).await?);

            txn.commit().await?;

            Ok(deleted)
        }

        #[doc = " Generated by sea-orm-macros"]
        pub async fn action<'a, C>(mut self, action: sea_orm::ActiveModelAction, db: &'a C) -> Result<Self, sea_orm::DbErr>
        where
            C: sea_orm::TransactionTrait,
        {
            use sea_orm::{HasOneModel, HasManyModel, IntoActiveModel, TransactionSession};
            let txn = db.begin().await?;
            let db = &txn;
            let mut deleted = sea_orm::DeleteResult::empty();

            #belongs_to_action
            #has_one_before_action
            #has_many_before_action
            #has_many_via_before_action

            let model: ActiveModel = self.into();

            let mut model: Self = if model.is_changed() {
                match action {
                    sea_orm::ActiveModelAction::Insert => model.insert(db).await,
                    sea_orm::ActiveModelAction::Update => model.update(db).await,
                    sea_orm::ActiveModelAction::Save => if !model.is_update() {
                        model.insert(db).await
                    } else {
                        model.update(db).await
                    },
                }?.into_ex().into()
            } else {
                model.into()
            };

            #belongs_to_after_action
            #has_one_action
            #has_many_action
            #has_many_via_action

            txn.commit().await?;

            Ok(model)
        }
    }
}

fn expand_active_model_setters(data: &Data) -> syn::Result<TokenStream> {
    let mut setters = TokenStream::new();

    if let Data::Struct(item_struct) = &data {
        if let Fields::Named(fields) = &item_struct.fields {
            for field in &fields.named {
                if let Some(ident) = &field.ident {
                    let field_type = &field.ty;
                    let field_type_str = quote! { #field_type }
                        .to_string() // e.g.: "Option < String >"
                        .replace(' ', ""); // Remove spaces

                    let mut ignore = false;

                    for attr in field.attrs.iter() {
                        if attr.path().is_ident("sea_orm") {
                            attr.parse_nested_meta(|meta| {
                                if meta.path.is_ident("ignore") {
                                    ignore = true;
                                } else {
                                    // Reads the value expression to advance the parse stream.
                                    let _: Option<Expr> = meta.value().and_then(|v| v.parse()).ok();
                                }

                                Ok(())
                            })?;
                        }
                    }

                    if ignore {
                        continue;
                    }

                    if is_compound_field(&field_type_str) {
                        let entity_path = extract_compound_entity(&field_type_str);
                        let active_model_type: Type = syn::parse_str(&format!(
                            "{}ActiveModelEx",
                            entity_path.trim_end_matches("Entity")
                        ))?;

                        if field_type_str.starts_with("HasOne<") {
                            let setter = format_ident!("set_{}", ident);

                            setters.extend(quote! {
                                #[doc = " Generated by sea-orm-macros"]
                                pub fn #setter(mut self, v: impl Into<#active_model_type>) -> Self {
                                    self.#ident.replace(v.into());
                                    self
                                }
                            });
                        } else {
                            let setter = format_ident!(
                                "add_{}",
                                pluralizer::pluralize(&ident.to_string(), 1, false)
                            );

                            setters.extend(quote! {
                                #[doc = " Generated by sea-orm-macros"]
                                pub fn #setter(mut self, v: impl Into<#active_model_type>) -> Self {
                                    self.#ident.push(v.into());
                                    self
                                }
                            });
                        }
                    } else {
                        let setter = format_ident!("set_{}", ident);

                        setters.extend(quote! {
                            #[doc = " Generated by sea-orm-macros"]
                            pub fn #setter(mut self, v: impl Into<#field_type>) -> Self {
                                self.#ident = sea_orm::Set(v.into());
                                self
                            }
                        });
                    }
                }
            }
        }
    }

    Ok(setters)
}
