use std::ops::Not;

use proc_macro2::TokenStream;
use quote::quote;
use syn::{Attribute, Ident, ItemFn, Pat, Token, parse_quote, punctuated::Punctuated};


pub(super) fn cli_macro_implementation(function: ItemFn) -> syn::Result<TokenStream> {
    let mut expanded = template_function(function.clone())?;

    expanded.extend(
        template_struct(function)?
    );

    Ok(expanded)
}


fn template_function(mut function: ItemFn) -> syn::Result<TokenStream> {
    strip_illegal_function_attributes(&mut function)?;

    let expanded = quote! {
        #[allow(unused)] //users will generally use the struct, but can optionally call the function directly
        #function
    };
    Ok(expanded)
}

fn template_struct(mut function: ItemFn) -> syn::Result<TokenStream> {

    let ItemFn {
        attrs,
        vis,
        sig: syn::Signature { ident, inputs, .. },
        block,
    } = function.clone();


    let parameter_names = inputs.clone().into_iter()
        .map(|input| match input {
            syn::FnArg::Typed(input) => {
                Ok(input.pat)
            }
            syn::FnArg::Receiver(input) => {
                Err(syn::Error::new_spanned(input, "function may not have a `self` parameter"))
            }
        })
        .collect::<syn::Result<Punctuated<Box<Pat>, Token![,]>>>()?;


    strip_illegal_function_attributes(&mut function)?;

    {
        strip_attributes(&mut function.attrs, &["doc"]); //Strip documentation, so we can replace it with something more general, since the struct itself already has the documentation.

        let original_function_name = function.sig.ident.to_string();
        let doc_comment = format!("Run the function associated with this task, which is declared here: [{original_function_name}]({original_function_name}())");
        function.attrs.push(parse_quote! {
            #[doc = #doc_comment]
        });
    }

    function.sig.ident = Ident::new("run", function.sig.ident.span());
    function.sig.inputs = parse_quote!(self);

    function.block = parse_quote!({
        let Self { #parameter_names } = self; //expose fields directly (without needing `self.` for access)

        #block
    });


    let mut struct_declaration: syn::ItemStruct = parse_quote! {

        #[derive(::clap::Args)]
        #[allow(non_camel_case_types)]
        #vis struct #ident {
            #inputs
        }
    };

    struct_declaration.attrs.extend(attrs);

    let expanded = quote! {
        #struct_declaration

        impl #ident {
            #function
        }
    };

    Ok(expanded)
}


/// Remove attributes from function item or parameters, because
/// users will put them there to have them placed onto the generated struct type,
/// which is passed to `clap`.
fn strip_illegal_function_attributes(function: &mut ItemFn) -> syn::Result<()> {

    for input in function.sig.inputs.iter_mut() {
        match input {
            syn::FnArg::Typed(input) => {
                strip_attributes(
                    &mut input.attrs,
                    &[
                        CLAP_ATTRIBUTES.as_slice(),
                        BUILTIN_FUNCTION_PARAMETER_ATTRIBUTES_TO_STRIP.as_slice()
                    ].concat()
                );
            }
            syn::FnArg::Receiver(input) => {
                return Err(syn::Error::new_spanned(input, "function may not have a `self` parameter"));
            }
        }
    }

    strip_attributes(&mut function.attrs, &CLAP_ATTRIBUTES);
    Ok(())
}


fn strip_attributes(attributes: &mut Vec<Attribute>, denylist: &[&'static str]) {
    attributes.retain(|attribute|
        attribute.path().segments.last()
            .map(|attribute|
                denylist
                    .contains(&attribute.ident.to_string().as_str())
                    .not()
            )
            .unwrap_or_default()
    )
}

const CLAP_ATTRIBUTES: [&str; 4] = ["clap", "command", "group", "arg"];
const BUILTIN_FUNCTION_PARAMETER_ATTRIBUTES_TO_STRIP: [&str; 1] = ["doc"]; //See for list of allow attributes: https://doc.rust-lang.org/reference/items/functions.html#attributes-on-function-parameters


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn should_strip_clap_args_attributes_from_a_function() { //Clap Args attributes: https://docs.rs/clap_derive/latest/clap_derive/derive.Args.html

        let mut testee: ItemFn = parse_quote! {
            /// #[doc] attribute
            #[allow(unused)]
            #[command(alias="to-be-stripped")]
            #[group(required = false)] //to be stripped
            fn test(
                /// #[doc] attribute
                #[warn(unused)]
                #[arg(long="to-be-stripped")]
                #[clap(value="to-be-stripped")]
                _: String,
            ) {}
        };

        strip_illegal_function_attributes(&mut testee).unwrap();

        let expected: ItemFn = parse_quote! {
            /// #[doc] attribute
            #[allow(unused)]
            fn test(
                #[warn(unused)]
                _: String,
            ) {}
        };

        assert_eq!(testee, expected);
    }
}
