//! Derive macros for Robomotion SDK
//!
//! This crate provides procedural macros for defining Robomotion nodes.

use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, Attribute, Data, DeriveInput, Field, Fields, Meta, Type};

/// Derive macro for Robomotion nodes.
///
/// # Example
///
/// ```ignore
/// #[derive(Node)]
/// #[node(id = "MyPackage.MyNode", name = "My Node", icon = "mdiIcon", color = "#3498db")]
/// struct MyNode {
///     #[input(title = "Input Data", var_type = "string", scope = "Message", name = "data")]
///     in_data: InVariable<String>,
///
///     #[output(title = "Result", var_type = "string", scope = "Message", name = "result")]
///     out_result: OutVariable<String>,
///
///     #[option(title = "My Option", value = "default")]
///     my_option: String,
/// }
/// ```
#[proc_macro_derive(Node, attributes(node, input, output, option, tool))]
pub fn derive_node(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let expanded = impl_node_derive(&input);
    TokenStream::from(expanded)
}

fn impl_node_derive(input: &DeriveInput) -> TokenStream2 {
    let name = &input.ident;

    // Parse node attributes
    let node_attrs = parse_node_attributes(&input.attrs);

    let node_id = node_attrs
        .get("id")
        .cloned()
        .unwrap_or_else(|| name.to_string());
    let node_name = node_attrs
        .get("name")
        .cloned()
        .unwrap_or_else(|| name.to_string());
    let node_icon = node_attrs.get("icon").cloned().unwrap_or_default();
    let node_color = node_attrs.get("color").cloned().unwrap_or_default();
    let node_inputs = node_attrs
        .get("inputs")
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(1);
    let node_outputs = node_attrs
        .get("outputs")
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(1);

    // Parse tool attributes if present
    let tool_attrs = parse_tool_attributes(&input.attrs);
    let has_tool = !tool_attrs.is_empty();
    let tool_name = tool_attrs.get("name").cloned().unwrap_or_default();
    let tool_description = tool_attrs.get("description").cloned().unwrap_or_default();

    // Parse field specs
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Node derive only supports named fields"),
        },
        _ => panic!("Node derive only supports structs"),
    };

    let field_specs = fields.iter().map(|f| generate_field_spec(f)).collect::<Vec<_>>();

    let tool_spec = if has_tool {
        quote! {
            Some(robomotion::runtime::ToolSpec {
                name: #tool_name.to_string(),
                description: #tool_description.to_string(),
            })
        }
    } else {
        quote! { None }
    };

    quote! {
        impl robomotion::runtime::NodeSpec for #name {
            fn node_id() -> &'static str {
                #node_id
            }

            fn node_name() -> &'static str {
                #node_name
            }

            fn node_icon() -> &'static str {
                #node_icon
            }

            fn node_color() -> &'static str {
                #node_color
            }

            fn inputs() -> usize {
                #node_inputs
            }

            fn outputs() -> usize {
                #node_outputs
            }

            fn tool_spec() -> Option<robomotion::runtime::ToolSpec> {
                #tool_spec
            }

            fn properties() -> Vec<robomotion::runtime::PropertySpec> {
                vec![
                    #(#field_specs),*
                ]
            }
        }

        impl robomotion::runtime::HasNodeBase for #name {
            fn node_base(&self) -> &robomotion::runtime::NodeBase {
                &self.node
            }

            fn node_base_mut(&mut self) -> &mut robomotion::runtime::NodeBase {
                &mut self.node
            }
        }
    }
}

fn parse_node_attributes(attrs: &[Attribute]) -> std::collections::HashMap<String, String> {
    let mut map = std::collections::HashMap::new();

    for attr in attrs {
        if attr.path().is_ident("node") {
            if let Meta::List(list) = &attr.meta {
                let tokens = list.tokens.to_string();
                for pair in tokens.split(',') {
                    let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
                    if parts.len() == 2 {
                        let key = parts[0].trim().to_string();
                        let value = parts[1].trim().trim_matches('"').to_string();
                        map.insert(key, value);
                    }
                }
            }
        }
    }

    map
}

fn parse_tool_attributes(attrs: &[Attribute]) -> std::collections::HashMap<String, String> {
    let mut map = std::collections::HashMap::new();

    for attr in attrs {
        if attr.path().is_ident("tool") {
            if let Meta::List(list) = &attr.meta {
                let tokens = list.tokens.to_string();
                for pair in tokens.split(',') {
                    let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
                    if parts.len() == 2 {
                        let key = parts[0].trim().to_string();
                        let value = parts[1].trim().trim_matches('"').to_string();
                        map.insert(key, value);
                    }
                }
            }
        }
    }

    map
}

fn parse_field_attributes(
    attrs: &[Attribute],
) -> (Option<String>, std::collections::HashMap<String, String>) {
    let mut attr_type = None;
    let mut map = std::collections::HashMap::new();

    for attr in attrs {
        let attr_name = if attr.path().is_ident("input") {
            Some("input")
        } else if attr.path().is_ident("output") {
            Some("output")
        } else if attr.path().is_ident("option") {
            Some("option")
        } else {
            None
        };

        if let Some(name) = attr_name {
            attr_type = Some(name.to_string());
            if let Meta::List(list) = &attr.meta {
                let tokens = list.tokens.to_string();
                for pair in tokens.split(',') {
                    let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
                    if parts.len() == 2 {
                        let key = parts[0].trim().to_string();
                        let value = parts[1].trim().trim_matches('"').to_string();
                        map.insert(key, value);
                    }
                }
            }
        }
    }

    (attr_type, map)
}

fn generate_field_spec(field: &Field) -> TokenStream2 {
    let field_name = field.ident.as_ref().unwrap();
    let field_name_str = field_name.to_string();
    let lower_field_name = field_name_str.to_case(Case::Camel);

    let (attr_type, attrs) = parse_field_attributes(&field.attrs);

    let title = attrs
        .get("title")
        .cloned()
        .unwrap_or_else(|| field_name_str.clone());
    let var_type = attrs
        .get("var_type")
        .cloned()
        .unwrap_or_else(|| "string".to_string());
    let scope = attrs
        .get("scope")
        .cloned()
        .unwrap_or_else(|| "Custom".to_string());
    let name = attrs
        .get("name")
        .cloned()
        .unwrap_or_else(|| lower_field_name.clone());
    let value = attrs.get("value").cloned().unwrap_or_default();
    let description = attrs.get("description");

    let property_type = match attr_type.as_deref() {
        Some("input") => quote! { robomotion::runtime::PropertyType::Input },
        Some("output") => quote! { robomotion::runtime::PropertyType::Output },
        Some("option") => quote! { robomotion::runtime::PropertyType::Option },
        _ => return quote! {},
    };

    let description_opt = match description {
        Some(d) => quote! { Some(#d.to_string()) },
        None => quote! { None },
    };

    // Check if this is a variable type
    let is_variable = is_variable_type(&field.ty);

    quote! {
        robomotion::runtime::PropertySpec {
            field_name: #lower_field_name.to_string(),
            title: #title.to_string(),
            var_type: #var_type.to_string(),
            scope: #scope.to_string(),
            name: #name.to_string(),
            default_value: #value.to_string(),
            property_type: #property_type,
            is_variable: #is_variable,
            description: #description_opt,
        }
    }
}

fn is_variable_type(ty: &Type) -> bool {
    if let Type::Path(type_path) = ty {
        if let Some(segment) = type_path.path.segments.last() {
            let name = segment.ident.to_string();
            return name == "InVariable" || name == "OutVariable" || name == "OptVariable";
        }
    }
    false
}
