use core::iter::Iterator;
use std::collections::{HashMap, HashSet};

use cinderblock_extension_api::{
    Accept, ReadFilterValue, ResourceActionInputKind, ResourceAttributeInput, ResourceMacroInput,
    UpdateChange,
};
use syn::{spanned::Spanned, Ident, Type};

/// Checks whether a `syn::Type` is `Option<T>`.
///
/// We inspect the outermost path segment for the identifier `Option`. This
/// handles both `Option<T>` and `std::option::Option<T>` (by checking the
/// last segment).
fn is_option_type(ty: &Type) -> bool {
    if let Type::Path(type_path) = ty {
        type_path
            .path
            .segments
            .last()
            .is_some_and(|seg| seg.ident == "Option")
    } else {
        false
    }
}

#[proc_macro]
pub fn resource(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Capture the raw input tokens before parsing so we can forward them
    // verbatim to extension macros without a reconstruct roundtrip.
    let raw_tokens: proc_macro2::TokenStream = item.clone().into();

    let input = syn::parse_macro_input!(item as ResourceMacroInput);

    let ident = input.name.last().expect("Missing name segment");

    let fields = input
        .attributes
        .iter()
        .map(ResourceAttributeInput::to_field_definition);

    let primary_key_type = {
        let fields = input
            .attributes
            .iter()
            .filter(|attr| attr.primary_key.value())
            .collect::<Vec<_>>();

        match fields.len() {
            0 => todo!("Support no primary keys"),
            1 => {
                let ty = fields[0].ty.clone();
                quote::quote! { #ty }
            }
            _ => todo!("Support multiple primary keys"),
        }
    };

    let primary_key_generated = {
        let fields = input
            .attributes
            .iter()
            .filter(|attr| attr.primary_key.value())
            .collect::<Vec<_>>();

        match fields.len() {
            0 => todo!("Support no primary keys"),
            1 => fields[0].generated.value(),
            _ => todo!("Support multiple primary keys"),
        }
    };

    let primary_key_value = {
        let fields = input
            .attributes
            .iter()
            .filter(|attr| attr.primary_key.value())
            .collect::<Vec<_>>();

        match fields.len() {
            0 => todo!("Support no primary keys"),
            1 => {
                let ty = fields[0].name.clone();
                quote::quote! { &self.#ty }
            }
            _ => todo!("Support multiple primary keys"),
        }
    };

    let data_layer_specified = input.data_layer.is_some();

    let actions = input.actions.iter().map(|action| match &action.kind {
        ResourceActionInputKind::Read(read_action) => {
            let action_name = convert_case::ccase!(pascal, action.name.to_string());
            let action_name = Ident::new(&action_name, action.name.span());

            // # Arguments struct generation
            //
            // When the read action declares arguments, we generate a dedicated
            // `{ActionName}Arguments` struct with `Deserialize` so it can be
            // populated from query parameters. When no arguments are declared,
            // we use `()` as the arguments type.
            let has_arguments = !read_action.arguments.is_empty();

            let (arguments_type, arguments_struct) = if has_arguments {
                let args_name = Ident::new(&format!("{action_name}Arguments"), action.name.span());
                let arg_fields = read_action.arguments.iter().map(|arg| {
                    let name = &arg.name;
                    let ty = &arg.ty;
                    quote::quote! { pub #name: #ty }
                });

                (
                    quote::quote! { #args_name },
                    quote::quote! {
                        #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
                        struct #args_name {
                            #(#arg_fields),*
                        }
                    },
                )
            } else {
                (quote::quote! { () }, quote::quote! {})
            };

            // Setup InMemoryReadAction if no data layer is provided
            let data_layer_block = if data_layer_specified {
                quote::quote! { }
            } else {
                // # Filter codegen for InMemoryReadAction
                //
                // Each filter becomes a boolean clause AND'd together. Literal
                // values are emitted directly; `arg(name)` references access
                // the corresponding field on the args struct.
                //
                // For `Option<T>` argument types, the filter clause is
                // conditional: when `None`, the clause is skipped (evaluates
                // to `true`). This lets optional arguments act as "filter if
                // provided" semantics.
                let filters = read_action.filters.iter().map(|filter| {
                    let field = &filter.field;

                    let op = match filter.op {
                        cinderblock_extension_api::ReadFilterOperation::Eq => quote::quote! { == },
                    };

                    match &filter.value {
                        ReadFilterValue::Literal(expr) => {
                            quote::quote! {
                                row.#field #op #expr &&
                            }
                        }
                        ReadFilterValue::Arg(arg_name) => {
                            // Check if the argument type is Option<T> by
                            // looking it up in the declared arguments.
                            let arg_decl = read_action
                                .arguments
                                .iter()
                                .find(|a| a.name == *arg_name)
                                .expect("arg reference validated during parsing");

                            if is_option_type(&arg_decl.ty) {
                                // Optional arg: skip the filter when None
                                quote::quote! {
                                    args.#arg_name.as_ref().map_or(true, |v| row.#field #op *v) &&
                                }
                            } else {
                                quote::quote! {
                                    row.#field #op args.#arg_name &&
                                }
                            }
                        }
                    }
                });

                quote::quote! {
                    impl cinderblock_core::data_layer::in_memory::InMemoryReadAction for #action_name {
                        fn filter(row: &Self::Output, args: &Self::Arguments) -> bool {
                            #(#filters)* true
                        }
                    }
                }
            };

            quote::quote! {
                #arguments_struct

                struct #action_name;

                impl cinderblock_core::ReadAction for #action_name {
                    type Output = #ident;

                    type Arguments = #arguments_type;
                }

                #data_layer_block
            }
        }
        ResourceActionInputKind::Create { accept } => {
            let action_name = convert_case::ccase!(pascal, action.name.to_string());
            let action_name = Ident::new(&action_name, action.name.span());
            let input_name = Ident::new(&format!("{action_name}Input"), action.name.span());

            let attributes = input.attributes.iter().filter(|attr| attr.writable.value());

            let (present, mut missing_names) = match accept {
                Accept::Default => (
                    attributes
                        .map(|attr| (attr.name.to_string(), attr))
                        .collect::<HashMap<_, _>>(),
                    HashMap::new(),
                ),
                Accept::Only(idents) => {
                    let idents = idents
                        .iter()
                        .map(|ident| ident.to_string())
                        .collect::<HashSet<_>>();

                    attributes.fold(
                        (HashMap::new(), HashMap::new()),
                        |(mut present, mut missing), attr| {
                            if idents.contains(&attr.name.to_string()) {
                                present.insert(attr.name.to_string(), attr);
                            } else {
                                missing.insert(attr.name.to_string(), attr);
                            }
                            (present, missing)
                        },
                    )
                }
            };

            input
                .attributes
                .iter()
                .filter(|attr| {
                    !attr.writable.value() || !present.contains_key(&attr.name.to_string())
                })
                .for_each(|attr| {
                    missing_names.insert(attr.name.to_string(), attr);
                });

            let attributes = present.values().map(|attr| attr.to_field_definition());

            let missing_names = missing_names.values().map(|attr| attr.to_default());

            let present_names = present.values().map(|attr| attr.name.clone());

            quote::quote! {
                #[derive(::std::fmt::Debug)]
                struct #action_name;

                #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
                struct #input_name {
                    #(pub #attributes),*
                }

                impl cinderblock_core::Create<#action_name> for #ident {
                    type Input = #input_name;

                    fn from_create_input(input: Self::Input) -> Self {
                        #ident {
                            // Iterate over attributes in #action_name.
                            #(#present_names: input.#present_names,)*

                            // All types attributes not present in #action_name should use default
                            #(#missing_names),*
                        }
                    }
                }
            }
        }
        ResourceActionInputKind::Update(update) => {
            let action_name = convert_case::ccase!(pascal, action.name.to_string());
            let action_name = Ident::new(&action_name, action.name.span());
            let input_name = Ident::new(&format!("{action_name}Input"), action.name.span());

            let attributes = input.attributes.iter().filter(|attr| attr.writable.value());

            let present = match &update.accept {
                Accept::Default => attributes.collect::<Vec<_>>(),
                Accept::Only(idents) => {
                    let idents = idents
                        .iter()
                        .map(|ident| ident.to_string())
                        .collect::<HashSet<_>>();

                    attributes
                        .filter(|attr| idents.contains(&attr.name.to_string()))
                        .collect()
                }
            };

            let field_definitions = present.iter().map(|attr| attr.to_field_definition());

            // # Field assignment generation
            //
            // Each accepted field from the input struct gets assigned onto `self`
            // in the generated `apply_update_input` method.
            let field_assignments = present.iter().map(|attr| {
                let name = &attr.name;
                quote::quote! { self.#name = input.#name; }
            });

            // # Change closure generation
            //
            // Each `change_ref` closure is emitted as a typed closure bound to a
            // variable, then called with `self`. We inject `&mut Self` as the
            // parameter type so field access resolves without the user needing
            // to annotate the type in the DSL.
            let change_ref_calls =
                update
                    .changes
                    .iter()
                    .enumerate()
                    .filter_map(|(i, change)| match change {
                        UpdateChange::ChangeRef(closure) => {
                            let param = closure
                                .inputs
                                .first()
                                .expect("change_ref closure must have exactly one parameter");
                            let body = &closure.body;
                            let binding = Ident::new(&format!("change_ref_{i}"), param.span());
                            Some(quote::quote! {
                                let #binding = |#param: &mut Self| #body;
                                #binding(self);
                            })
                        }
                        // TODO: support `change` (by-value) variant
                        UpdateChange::Change(_) => None,
                    });

            quote::quote! {
                #[derive(::std::fmt::Debug)]
                struct #action_name;

                #[derive(::std::fmt::Debug, cinderblock_core::serde::Deserialize)]
                struct #input_name {
                    #(pub #field_definitions),*
                }

                impl cinderblock_core::Update<#action_name> for #ident {
                    type Input = #input_name;

                    fn apply_update_input(&mut self, input: Self::Input) {
                        #(#field_assignments)*
                        #(#change_ref_calls)*
                    }
                }
            }
        }
        ResourceActionInputKind::Destroy => {
            // # Destroy action codegen
            //
            // Destroy actions only need a marker struct and a `Destroy<A>` impl.
            // No input struct is generated — the primary key comes from the
            // URL path at the HTTP layer.
            let action_name = convert_case::ccase!(pascal, action.name.to_string());
            let action_name = Ident::new(&action_name, action.name.span());

            quote::quote! {
                #[derive(::std::fmt::Debug)]
                struct #action_name;

                impl cinderblock_core::Destroy<#action_name> for #ident {}
            }
        }
    });

    let name_segments = input.name.iter().map(|segment| segment.to_string());

    // # Data layer selection
    //
    // If the user specified `data_layer = some::Path;` in the DSL, use that
    // path. Otherwise default to the built-in in-memory data layer.
    let data_layer_path = input.data_layer.map_or_else(
        || quote::quote! { cinderblock_core::data_layer::in_memory::InMemoryDataLayer },
        |path| quote::quote! { #path },
    );

    // # Extension forwarding
    //
    // For each declared extension, we forward the raw DSL tokens (captured
    // before parsing) inside a braced group, followed by a `config = { ... }`
    // block containing the extension-specific configuration. This avoids a
    // parse-then-reconstruct roundtrip — the extension macro receives the
    // exact tokens the user wrote.
    let extension_calls = input.extensions.iter().map(|ext| {
        let path = &ext.path;
        let config_tokens = &ext.config_tokens;

        quote::quote! {
            #path::__resource_extension! {
                { #raw_tokens }

                config = {
                    #config_tokens
                }
            }
        }
    });

    quote::quote! {
        #[derive(::std::fmt::Debug, ::std::clone::Clone, cinderblock_core::serde::Serialize, cinderblock_core::serde::Deserialize)]
        struct #ident {
            #(#fields),*
        }

        impl cinderblock_core::Resource for #ident {
            type PrimaryKey = #primary_key_type;

            type DataLayer = #data_layer_path;

            const NAME: &'static [&'static str] = &[#(#name_segments),*];

            const PRIMARY_KEY_GENERATED: bool = #primary_key_generated;

            fn primary_key(&self) -> &Self::PrimaryKey {
                #primary_key_value
            }
        }

        #(#actions)*

        #(#extension_calls)*
    }
    .into()
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert2::{assert, check};
    use cinderblock_extension_api::ResourceActionInput;
    use quote::quote;

    fn parse_resource(tokens: proc_macro2::TokenStream) -> ResourceMacroInput {
        let result = syn::parse2::<ResourceMacroInput>(tokens);
        assert!(let Ok(input) = result);
        input
    }

    #[test]
    fn minimal_resource_with_one_simple_attribute() {
        let input = parse_resource(quote! {
            name = Foo;

            attributes {
                id String;
            }
        });

        check!(input.name.len() == 1);
        check!(input.name[0] == "Foo");

        check!(input.attributes.len() == 1);
        let attr = &input.attributes[0];
        check!(attr.name == "id");
        check!(!attr.primary_key.value());
        check!(!attr.generated.value());
        check!(attr.writable.value());
        check!(attr.default.is_none());

        check!(input.actions.is_empty());
    }

    #[test]
    fn dotted_name_parses_into_multiple_segments() {
        let input = parse_resource(quote! {
            name = Helpdesk.Support.Ticket;

            attributes {
                id String;
            }
        });

        check!(input.name.len() == 3);
        check!(input.name[0] == "Helpdesk");
        check!(input.name[1] == "Support");
        check!(input.name[2] == "Ticket");
    }

    #[test]
    fn attribute_with_options_block() {
        let input = parse_resource(quote! {
            name = Ticket;

            attributes {
                ticket_id Uuid {
                    primary_key true;
                    writable false;
                    default || uuid::Uuid::new_v4();
                }
            }
        });

        check!(input.attributes.len() == 1);
        let attr = &input.attributes[0];
        check!(attr.name == "ticket_id");
        check!(attr.primary_key.value());
        check!(!attr.writable.value());
        check!(attr.default.is_some());
    }

    #[test]
    fn attribute_with_generated_flag() {
        let input = parse_resource(quote! {
            name = Item;

            attributes {
                item_id Uuid {
                    primary_key true;
                    generated true;
                }
            }
        });

        let attr = &input.attributes[0];
        check!(attr.generated.value());
        check!(attr.primary_key.value());
        // writable should still be the default (true) since it wasn't set.
        check!(attr.writable.value());
    }

    #[test]
    fn multiple_attributes_mixed_simple_and_complex() {
        let input = parse_resource(quote! {
            name = Order;

            attributes {
                order_id Uuid {
                    primary_key true;
                    writable false;
                }
                item_name String;
                quantity u32;
            }
        });

        check!(input.attributes.len() == 3);

        check!(input.attributes[0].name == "order_id");
        check!(input.attributes[0].primary_key.value());
        check!(!input.attributes[0].writable.value());

        check!(input.attributes[1].name == "item_name");
        check!(!input.attributes[1].primary_key.value());
        check!(input.attributes[1].writable.value());

        check!(input.attributes[2].name == "quantity");
        check!(!input.attributes[2].primary_key.value());
        check!(input.attributes[2].writable.value());
    }

    #[test]
    fn actions_block_with_simple_create() {
        let input = parse_resource(quote! {
            name = Ticket;

            attributes {
                id String;
            }

            actions {
                create open;
            }
        });

        check!(input.actions.len() == 1);
        check!(input.actions[0].name == "open");
        check!(let ResourceActionInputKind::Create { accept: Accept::Default } = &input.actions[0].kind);
    }

    #[test]
    fn action_with_accept_list() {
        let input = parse_resource(quote! {
            name = Ticket;

            attributes {
                id String;
            }

            actions {
                create assign {
                    accept [subject];
                };
            }
        });

        check!(input.actions.len() == 1);
        check!(input.actions[0].name == "assign");
        assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = &input.actions[0].kind);
        check!(idents.len() == 1);
        check!(idents[0] == "subject");
    }

    #[test]
    fn no_actions_block_omitted() {
        let input = parse_resource(quote! {
            name = Simple;

            attributes {
                id u64;
            }
        });

        check!(input.actions.is_empty());
    }

    #[test]
    fn full_helpdesk_example() {
        let input = parse_resource(quote! {
            name = Helpdesk.Support.Ticket;

            attributes {
                ticket_id Uuid {
                    primary_key true;
                    writable false;
                    default || uuid::Uuid::new_v4();
                }

                subject String;

                status TicketStatus;
            }

            actions {
                create open;

                create assign {
                    accept [subject];
                };
            }
        });

        check!(input.name.len() == 3);
        check!(input.name[0] == "Helpdesk");
        check!(input.name[1] == "Support");
        check!(input.name[2] == "Ticket");

        check!(input.attributes.len() == 3);

        let ticket_id = &input.attributes[0];
        check!(ticket_id.name == "ticket_id");
        check!(ticket_id.primary_key.value());
        check!(!ticket_id.writable.value());
        check!(ticket_id.default.is_some());

        let subject = &input.attributes[1];
        check!(subject.name == "subject");
        check!(!subject.primary_key.value());
        check!(subject.writable.value());
        check!(subject.default.is_none());

        let status = &input.attributes[2];
        check!(status.name == "status");
        check!(!status.primary_key.value());
        check!(status.writable.value());

        check!(input.actions.len() == 2);

        check!(input.actions[0].name == "open");
        check!(let ResourceActionInputKind::Create { accept: Accept::Default } = &input.actions[0].kind);

        check!(input.actions[1].name == "assign");
        assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = &input.actions[1].kind);
        check!(idents.len() == 1);
        check!(idents[0] == "subject");
    }

    #[test]
    fn parse_simple_create_action() {
        assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
            create open;
        }));

        check!(action.name == "open");
        check!(let ResourceActionInputKind::Create { accept: Accept::Default } = action.kind);
    }

    #[test]
    fn parse_create_action_with_multiple_accept_idents() {
        assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
            create bulk_insert {
                accept [name, email, age];
            }
        }));

        check!(action.name == "bulk_insert");
        assert!(let ResourceActionInputKind::Create { accept: Accept::Only(idents) } = action.kind);
        let names: Vec<String> = idents.iter().map(|i| i.to_string()).collect();
        check!(names == vec!["name", "email", "age"]);
    }

    #[test]
    fn unknown_action_kind_produces_error() {
        let result = syn::parse2::<ResourceActionInput>(quote! {
            frobnicate foo;
        });

        assert!(let Err(err) = result);
        let msg = err.to_string();
        check!(msg.contains("Unexpected action kind"));
        check!(msg.contains("frobnicate"));
    }

    #[test]
    fn unknown_attribute_option_produces_error() {
        let result = syn::parse2::<ResourceMacroInput>(quote! {
            name = Thing;

            attributes {
                id String {
                    bogus true;
                }
            }
        });

        assert!(let Err(err) = result);
        let msg = err.to_string();
        check!(msg.contains("Unexpected attribute key"));
        check!(msg.contains("bogus"));
    }

    #[test]
    fn missing_semicolon_after_name_produces_error() {
        let result = syn::parse2::<ResourceMacroInput>(quote! {
            name = Foo

            attributes {
                id String;
            }
        });

        check!(let Err(_) = result);
    }

    #[test]
    fn parse_simple_update_action_with_default_accept() {
        assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
            update close;
        }));

        check!(action.name == "close");
        assert!(let ResourceActionInputKind::Update(update) = &action.kind);
        check!(let Accept::Default = update.accept);
        check!(update.changes.is_empty());
    }

    #[test]
    fn parse_update_action_with_accept_and_change_ref() {
        assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
            update close {
                accept [];
                change_ref |resource| {
                    resource.status = TicketStatus::Closed;
                };
            }
        }));

        check!(action.name == "close");
        assert!(let ResourceActionInputKind::Update(update) = &action.kind);
        assert!(let Accept::Only(idents) = &update.accept);
        check!(idents.is_empty());
        check!(update.changes.len() == 1);
        check!(let UpdateChange::ChangeRef(_) = &update.changes[0]);
    }

    #[test]
    fn parse_update_action_with_accept_fields() {
        assert!(let Ok(action) = syn::parse2::<ResourceActionInput>(quote! {
            update reassign {
                accept [subject, status];
            }
        }));

        check!(action.name == "reassign");
        assert!(let ResourceActionInputKind::Update(update) = &action.kind);
        assert!(let Accept::Only(idents) = &update.accept);
        let names: Vec<String> = idents.iter().map(|i| i.to_string()).collect();
        check!(names == vec!["subject", "status"]);
        check!(update.changes.is_empty());
    }
}
