use super::util::*;
use crate::model::{
    AutoImpl, ContractTrait, EndpointMutabilityMetadata, EndpointTypeMetadata, Method, MethodImpl,
    PublicRole,
};

fn generate_endpoint_snippet(
    m: &Method,
    endpoint_name: &str,
    only_owner: bool,
    only_admin: bool,
    mutability: EndpointMutabilityMetadata,
    endpoint_type: EndpointTypeMetadata,
    allow_multiple_var_args: bool,
) -> proc_macro2::TokenStream {
    let endpoint_docs = &m.docs;
    let rust_method_name = m.name.to_string();
    let title_tokens = m
        .title
        .as_ref()
        .map(|title| quote! { .with_title(#title) })
        .unwrap_or_default();
    let only_owner_tokens = if only_owner {
        quote! { .with_only_owner() }
    } else {
        quote! {}
    };
    let only_admin_tokens = if only_admin {
        quote! { .with_only_admin() }
    } else {
        quote! {}
    };
    let allow_multiple_var_args_tokens = if allow_multiple_var_args {
        quote! { .with_allow_multiple_var_args() }
    } else {
        quote! {}
    };

    let payable_in_tokens = m.payable_metadata().abi_strings();

    let input_snippets: Vec<proc_macro2::TokenStream> = m
        .method_args
        .iter()
        .filter_map(|arg| {
            if arg.metadata.payment.is_payment_arg() {
                None
            } else {
                let mut arg_type = arg.ty.clone();
                clear_all_type_lifetimes(&mut arg_type);
                let arg_name = &arg.pat;
                let arg_name_str = quote! { #arg_name }.to_string();
                Some(quote! {
                    endpoint_abi.add_input::<#arg_type>(#arg_name_str);
                    contract_abi.add_type_descriptions::<#arg_type>();
                })
            }
        })
        .collect();

    let output_names = &m.output_names;
    let output_snippet = match &m.return_type {
        syn::ReturnType::Default => quote! {},
        syn::ReturnType::Type(_, ty) => {
            let mut res_type = ty.clone();
            clear_all_type_lifetimes(&mut res_type);
            quote! {
                endpoint_abi.add_output::<#res_type>(&[ #(#output_names),* ]);
                contract_abi.add_type_descriptions::<#res_type>();
            }
        }
    };

    let label_names = &m.label_names;
    let mutability_tokens = mutability.to_tokens();
    let endpoint_type_tokens = endpoint_type.to_tokens();

    quote! {
        let mut endpoint_abi = multiversx_sc::abi::EndpointAbi::new(
            #endpoint_name,
            #rust_method_name,
            #mutability_tokens,
            #endpoint_type_tokens,
        )
        #(.with_docs(#endpoint_docs))*
        #title_tokens
        #only_owner_tokens
        #only_admin_tokens
        #(.with_label(#label_names))*
        #(.with_payable_token(#payable_in_tokens))*
        #allow_multiple_var_args_tokens
        ;

        #(#input_snippets)*
        #output_snippet
    }
}

fn generate_endpoint_snippets(contract: &ContractTrait) -> Vec<proc_macro2::TokenStream> {
    contract
        .methods
        .iter()
        .filter_map(|m| match &m.public_role {
            PublicRole::Init(_) => {
                let endpoint_def = generate_endpoint_snippet(
                    m,
                    "init",
                    false,
                    false,
                    EndpointMutabilityMetadata::Mutable,
                    EndpointTypeMetadata::Init,
                    m.is_allow_multiple_var_args(),
                );
                Some(quote! {
                    #endpoint_def
                    contract_abi.constructors.push(endpoint_abi);
                })
            }
            PublicRole::Upgrade(_) => {
                let endpoint_def = generate_endpoint_snippet(
                    m,
                    "upgrade",
                    false,
                    false,
                    EndpointMutabilityMetadata::Mutable,
                    EndpointTypeMetadata::Upgrade,
                    m.is_allow_multiple_var_args(),
                );
                Some(quote! {
                    #endpoint_def
                    contract_abi.upgrade_constructors.push(endpoint_abi);
                })
            }
            PublicRole::Endpoint(endpoint_metadata) => {
                let endpoint_def = generate_endpoint_snippet(
                    m,
                    &endpoint_metadata.public_name.to_string(),
                    endpoint_metadata.only_owner,
                    endpoint_metadata.only_admin,
                    endpoint_metadata.mutability.clone(),
                    EndpointTypeMetadata::Endpoint,
                    endpoint_metadata.allow_multiple_var_args,
                );
                Some(quote! {
                    #endpoint_def
                    contract_abi.endpoints.push(endpoint_abi);
                })
            }
            PublicRole::CallbackPromise(callback_metadata) => {
                let endpoint_def = generate_endpoint_snippet(
                    m,
                    &callback_metadata.callback_name.to_string(),
                    false,
                    false,
                    EndpointMutabilityMetadata::Mutable,
                    EndpointTypeMetadata::PromisesCallback,
                    m.is_allow_multiple_var_args(),
                );
                Some(quote! {
                    #endpoint_def
                    contract_abi.promise_callbacks.push(endpoint_abi);
                })
            }
            _ => None,
        })
        .collect()
}

fn generate_event_snippet(m: &Method, event_name: &str) -> proc_macro2::TokenStream {
    let event_docs = &m.docs;
    let input_snippets: Vec<proc_macro2::TokenStream> = m
        .method_args
        .iter()
        .map(|arg| {
            let mut arg_type = arg.ty.clone();
            let indexed = arg.metadata.event_topic;
            clear_all_type_lifetimes(&mut arg_type);
            let arg_name = &arg.pat;
            let arg_name_str = quote! { #arg_name }.to_string();
            quote! {
                event_abi.add_input::<#arg_type>(#arg_name_str, #indexed);
                contract_abi.add_type_descriptions::<#arg_type>();
            }
        })
        .collect();

    quote! {
        let mut event_abi = multiversx_sc::abi::EventAbi::new(
            &[ #(#event_docs),* ],
            #event_name,
        );
        #(#input_snippets)*
    }
}

fn generate_event_snippets(contract: &ContractTrait) -> Vec<proc_macro2::TokenStream> {
    contract
        .methods
        .iter()
        .filter_map(|m| {
            if let MethodImpl::Generated(AutoImpl::Event { identifier }) = &m.implementation {
                let event_def = generate_event_snippet(m, identifier);
                Some(quote! {
                    #event_def
                    contract_abi.events.push(event_abi);
                })
            } else {
                None
            }
        })
        .collect()
}

fn has_callback(contract: &ContractTrait) -> bool {
    contract.methods.iter().any(|m| {
        matches!(
            m.public_role,
            PublicRole::Callback(_) | PublicRole::CallbackRaw
        )
    })
}

fn generate_supertrait_snippets(contract: &ContractTrait) -> Vec<proc_macro2::TokenStream> {
    contract
            .supertraits
            .iter()
            .map(|supertrait| {
                let module_path = &supertrait.module_path;
                quote! {
                    contract_abi.coalesce(<#module_path AbiProvider as multiversx_sc::contract_base::ContractAbiProvider>::abi());
                }
            })
            .collect()
}

fn generate_esdt_attribute_snippets(contract: &ContractTrait) -> Vec<proc_macro2::TokenStream> {
    contract
        .trait_attributes
        .esdt_attribute
        .iter()
        .map(|esdt_attr| {
            let ticker = &esdt_attr.ticker;
            let ty = &esdt_attr.ty;
            quote! {
                contract_abi.esdt_attributes.push(multiversx_sc::abi::EsdtAttributeAbi::new::<#ty>(#ticker));
                contract_abi.add_type_descriptions::<#ty>();
            }
        })
        .collect()
}

fn generate_abi_method_body(
    contract: &ContractTrait,
    is_contract_main: bool,
) -> proc_macro2::TokenStream {
    let contract_docs = &contract.docs;
    let contract_name = &contract.trait_name.to_string();
    let endpoint_snippets = generate_endpoint_snippets(contract);
    let event_snippets = generate_event_snippets(contract);
    let has_callbacks = has_callback(contract);
    let supertrait_snippets: Vec<proc_macro2::TokenStream> = if is_contract_main {
        generate_supertrait_snippets(contract)
    } else {
        Vec::new()
    };
    let esdt_attributes = if !&contract.trait_attributes.esdt_attribute.is_empty() {
        generate_esdt_attribute_snippets(contract)
    } else {
        Vec::new()
    };

    quote! {
        let mut contract_abi = multiversx_sc::abi::ContractAbi::new(
            multiversx_sc::abi::BuildInfoAbi {
                rustc: None,
                contract_crate: multiversx_sc::abi::ContractCrateBuildAbi::new(
                    env!("CARGO_PKG_NAME"),
                    env!("CARGO_PKG_VERSION"),
                ),
                framework: multiversx_sc::abi::FrameworkBuildAbi::create(),
            },
            &[ #(#contract_docs),* ],
            #contract_name,
            #has_callbacks,
        );
        #(#endpoint_snippets)*
        #(#event_snippets)*
        #(#supertrait_snippets)*
        #(#esdt_attributes)*
        contract_abi
    }
}

pub fn generate_abi_provider(
    contract: &ContractTrait,
    is_contract_main: bool,
) -> proc_macro2::TokenStream {
    let abi_body = generate_abi_method_body(contract, is_contract_main);
    quote! {
        pub struct AbiProvider {}

        impl multiversx_sc::contract_base::ContractAbiProvider for AbiProvider {
            type Api = multiversx_sc::api::uncallable::UncallableApi;

            fn abi() -> multiversx_sc::abi::ContractAbi {
                #abi_body
            }
        }
    }
}
