use crate::reducer::{assert_only_lifetime_generics, extract_typed_args};
use crate::util::ident_to_litstr;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{ItemFn, ReturnType};

pub(crate) fn handler_impl(args: TokenStream, original_function: &ItemFn) -> syn::Result<TokenStream> {
    if !args.is_empty() {
        return Err(syn::Error::new_spanned(
            args,
            "The `handler` attribute does not accept arguments",
        ));
    }

    let func_name = &original_function.sig.ident;
    let vis = &original_function.vis;
    let handler_name = ident_to_litstr(func_name);

    assert_only_lifetime_generics(original_function, "http handlers")?;

    let typed_args = extract_typed_args(original_function)?;
    if typed_args.len() != 2 {
        return Err(syn::Error::new_spanned(
            original_function.sig.clone(),
            "HTTP handlers must take exactly two arguments",
        ));
    }

    let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();
    let first_arg_ty = &arg_tys[0];
    let second_arg_ty = &arg_tys[1];

    let ret_ty = match &original_function.sig.output {
        ReturnType::Type(_, t) => t.as_ref(),
        ReturnType::Default => {
            return Err(syn::Error::new_spanned(
                original_function.sig.clone(),
                "HTTP handlers must return `spacetimedb::http::Response`",
            ));
        }
    };

    let internal_ident = syn::Ident::new(&format!("__spacetimedb_http_handler_{func_name}"), func_name.span());
    let mut inner_fn = original_function.clone();
    inner_fn.sig.ident = internal_ident.clone();

    let register_describer_symbol = format!("__preinit__20_register_http_handler_{}", handler_name.value());

    let lifetime_params = &original_function.sig.generics;
    let lifetime_where_clause = &lifetime_params.where_clause;

    let generated_describe_function = quote! {
        #[unsafe(export_name = #register_describer_symbol)]
        pub extern "C" fn __register_describer() {
            spacetimedb::rt::register_http_handler(#handler_name, #internal_ident)
        }
    };

    Ok(quote! {
        #inner_fn

        #[allow(non_upper_case_globals)]
        #vis const #func_name: spacetimedb::http::Handler = spacetimedb::http::Handler::new(#handler_name);

        const _: () = {
            #generated_describe_function
        };

        const _: () = {
            fn _assert_args #lifetime_params () #lifetime_where_clause {
                let _ = <#first_arg_ty as spacetimedb::rt::HttpHandlerContextArg>::_ITEM;
                let _ = <#second_arg_ty as spacetimedb::rt::HttpHandlerRequestArg>::_ITEM;
                let _ = <#ret_ty as spacetimedb::rt::HttpHandlerReturn>::_ITEM;
            }
        };
    })
}

pub(crate) fn router_impl(args: TokenStream, original_function: &ItemFn) -> syn::Result<TokenStream> {
    if !args.is_empty() {
        return Err(syn::Error::new_spanned(
            args,
            "The `router` attribute does not accept arguments",
        ));
    }

    if !original_function.sig.inputs.is_empty() {
        return Err(syn::Error::new_spanned(
            original_function.sig.clone(),
            "HTTP router functions must take no arguments",
        ));
    }

    let func_name = &original_function.sig.ident;
    let register_symbol = "__preinit__30_register_http_router";

    Ok(quote! {
        #original_function

        const _: () = {
            fn _assert_router() {
                let _f: fn() -> spacetimedb::http::Router = #func_name;
            }
        };

        const _: () = {
            #[unsafe(export_name = #register_symbol)]
            pub extern "C" fn __register_router() {
                spacetimedb::rt::register_http_router(#func_name)
            }
        };
    })
}
