//! `#[node]` attribute macro implementation.
//!
//! Transforms an async function into a struct that implements `NodeFn<S>`.
//!
//! ## Generated code
//!
//! Given:
//! ```ignore
//! #[node]
//! async fn my_node(state: &MyState) -> NodeResult<MyStateUpdate> { ... }
//! ```
//!
//! Generates:
//! ```ignore
//! struct MyNodeNode;
//! impl NodeFn<MyState> for MyNodeNode {
//!     fn call(&self, state: &MyState, ctx: &NodeContext) -> NodeFuture<MyStateUpdate> {
//!         Box::pin(async move { /* original body */ })
//!     }
//!     fn name(&self) -> &str { "my_node" }
//! }
//! let my_node = MyNodeNode;
//! ```
//!
//! Supports two signatures:
//! - `(state: &S)` — context is ignored
//! - `(state: &S, ctx: &NodeContext)` — context is available

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Error, FnArg, ItemFn, PatType, Result};

use crate::node_return::{ReturnStyle, extract_return_update_type};
use crate::path_resolution::MacroPaths;

/// Parse and transform a `#[node]` annotated async function.
pub fn node_attr_impl(item: ItemFn) -> Result<TokenStream> {
    // Must be async
    if item.sig.asyncness.is_none() {
        return Err(Error::new_spanned(
            item.sig.fn_token,
            "#[node] functions must be async. Add `async` before `fn`.",
        ));
    }

    let fn_name = &item.sig.ident;
    let fn_name_str = fn_name.to_string();
    let body = &item.block;

    // Parse parameters
    let params: Vec<&PatType> = item
        .sig
        .inputs
        .iter()
        .filter_map(|arg| match arg {
            FnArg::Typed(pat) => Some(pat),
            FnArg::Receiver(_) => None,
        })
        .collect();

    if params.is_empty() || params.len() > 2 {
        return Err(Error::new_spanned(
            &item.sig.inputs,
            format!(
                "#[node] function `{}` has {} parameters, expected 1 or 2: \
                 (state: &S) or (state: &S, ctx: &NodeContext)",
                fn_name_str,
                params.len()
            ),
        ));
    }

    // Extract state parameter name and type
    let state_pat = &params[0].pat;
    let state_ty = extract_ref_type(&params[0].ty)?;

    // Check if ctx parameter is present
    let has_ctx = params.len() == 2;
    let ctx_pat = if has_ctx {
        let pat = &params[1].pat;
        quote! { #pat }
    } else {
        quote! { _ctx }
    };

    // Parse return type to extract the update type and style
    let (update_ty, return_style) = extract_return_update_type(&item.sig.output)?;

    // Generate struct name: snake_case -> PascalCase + "Node"
    let struct_name = format_ident!("{}Node", to_pascal_case(&fn_name_str));
    let paths = MacroPaths::new();
    let core = &paths.core;

    // The user writes: async fn my_node(state: &S, ctx: &NodeContext) -> NodeResult<U> { body }
    // But NodeFuture<U> is 'static, so we can't borrow state/ctx across await.
    // Solution: clone state and ctx into owned values, rebind the parameter names
    // so the user's body references the owned clones seamlessly.

    let ctx_clone = if has_ctx {
        quote! { let #ctx_pat = __pe_ctx.clone(); }
    } else {
        quote! {}
    };

    // For Result return style, wrap the body to convert Err into NodeResult::Error
    let body_expr = match return_style {
        ReturnStyle::Bare => quote! { async move #body },
        ReturnStyle::Fallible => quote! {
            async move {
                let __pe_result: ::core::result::Result<
                    #core::node::NodeResult<#update_ty>,
                    #core::PeError,
                > = async move #body .await;
                match __pe_result {
                    Ok(r) => r,
                    Err(e) => #core::node::NodeResult::Error(e),
                }
            }
        },
    };

    Ok(quote! {
        struct #struct_name;

        impl #core::node::NodeFn<#state_ty> for #struct_name {
            fn call(
                &self,
                __pe_state: &#state_ty,
                __pe_ctx: &#core::node::NodeContext,
            ) -> #core::node::NodeFuture<#update_ty> {
                let #state_pat = __pe_state.clone();
                #ctx_clone
                Box::pin(#body_expr)
            }

            fn name(&self) -> &str {
                #fn_name_str
            }
        }

        #[allow(non_upper_case_globals)]
        const #fn_name: #struct_name = #struct_name;
    })
}

/// Extract `T` from `&T` in a type position.
fn extract_ref_type(ty: &syn::Type) -> Result<&syn::Type> {
    match ty {
        syn::Type::Reference(r) => Ok(&*r.elem),
        _ => Err(Error::new_spanned(
            ty,
            "#[node] state parameter must be a reference (&State)",
        )),
    }
}

/// Convert `snake_case` to `PascalCase`.
///
/// Strips leading underscores so `_private_node` becomes `PrivateNode`,
/// not `PrivateNode` with an empty leading segment.
fn to_pascal_case(s: &str) -> String {
    let stripped = s.trim_start_matches('_');
    stripped
        .split('_')
        .filter(|w| !w.is_empty())
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                Some(c) => {
                    let upper: String = c.to_uppercase().collect();
                    upper + chars.as_str()
                }
                None => String::new(),
            }
        })
        .collect()
}

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

    #[test]
    fn test_to_pascal_case_single_word() {
        assert_eq!(to_pascal_case("prepare"), "Prepare");
    }

    #[test]
    fn test_to_pascal_case_multi_word() {
        assert_eq!(to_pascal_case("my_cool_node"), "MyCoolNode");
    }

    #[test]
    fn test_to_pascal_case_already_capitalized() {
        assert_eq!(to_pascal_case("Chat"), "Chat");
    }

    #[test]
    fn test_to_pascal_case_leading_underscore() {
        assert_eq!(to_pascal_case("_private"), "Private");
        assert_eq!(to_pascal_case("__double_under"), "DoubleUnder");
        assert_eq!(to_pascal_case("_my_cool_node"), "MyCoolNode");
    }
}
