//! `extrolib-macros` — procedural macros for Extro.
//!
//! ## `#[service]` / `#[actor]`
//!
//! Attribute macros applied to a trait. `#[service]` emits the trait
//! unchanged, plus an `{Trait}Op` enum, a dispatch-table function, and a
//! `{SCREAMING_TRAIT}_MANIFEST_FRAGMENT: ServiceManifest<'static>` const.
//! `#[actor]` wraps the same machinery and additionally emits a message enum,
//! actor struct, handle struct, a run-loop, and an
//! `{SCREAMING_TRAIT}_ACTOR_MANIFEST: ActorManifest<'static>` const.
//!
//! ## `#[derive(Frame)]`
//!
//! Implements `::extrolib::core::FrameCode` for a `Copy` enum whose variants
//! carry `#[frame(0xNN)]` attributes.  Emits
//! `{ENUM}_FRAMES_MANIFEST: FramesManifest<'static>`.
//!
//! ## `#[derive(StatusMap)]`
//!
//! Implements `::extrolib::core::StatusMap` for an error enum whose variants
//! carry `#[status(NNN)]` attributes.  Emits
//! `{ENUM}_STATUS_MANIFEST: StatusManifest<'static>`.
//!
//! ## `#[deny_floats]`
//!
//! Attribute macro that rejects `f32` / `f64` fields on structs and enum
//! variants.  Apply to every `#[derive(Archive, ...)]` type (Open Invariant #47).

#![forbid(unsafe_code)]

use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Ident, ItemTrait, LitInt, LitStr, TraitItem};

// ─── #[service] ─────────────────────────────────────────────────────────────

#[proc_macro_attribute]
pub fn service(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let trait_def = parse_macro_input!(item as ItemTrait);
    service_inner(trait_def).into()
}

fn service_inner(trait_def: ItemTrait) -> proc_macro2::TokenStream {
    let trait_name = &trait_def.ident;

    let mut method_names: Vec<String> = Vec::new();
    let mut variant_idents: Vec<Ident> = Vec::new();
    for item in &trait_def.items {
        if let TraitItem::Fn(func) = item {
            let name = func.sig.ident.to_string();
            variant_idents.push(Ident::new(&snake_to_pascal(&name), Span::call_site()));
            method_names.push(name);
        }
    }

    let op_enum_name = Ident::new(&format!("{trait_name}Op"), Span::call_site());
    let dispatch_fn_name = Ident::new(
        &format!(
            "{}_dispatch_table",
            pascal_to_snake(&trait_name.to_string())
        ),
        Span::call_site(),
    );
    let manifest_const_name = Ident::new(
        &format!(
            "{}_MANIFEST_FRAGMENT",
            pascal_to_screaming(&trait_name.to_string())
        ),
        Span::call_site(),
    );

    let trait_name_str = trait_name.to_string();

    quote! {
        #trait_def

        #[doc = "Operations exposed by `"]
        #[doc = #trait_name_str]
        #[doc = "`. Generated by `#[service]`."]
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        #[allow(non_camel_case_types)]
        pub enum #op_enum_name {
            #( #variant_idents, )*
        }

        #[doc = "Method names of `"]
        #[doc = #trait_name_str]
        #[doc = "`, in declaration order. Generated by `#[service]`."]
        pub fn #dispatch_fn_name() -> &'static [&'static str] {
            &[ #( #method_names ),* ]
        }

        #[doc = "Typed manifest fragment for `"]
        #[doc = #trait_name_str]
        #[doc = "`. Generated by `#[service]`."]
        pub const #manifest_const_name: ::extrolib::core::manifest::ServiceManifest<'static> =
            ::extrolib::core::manifest::ServiceManifest {
                trait_name: #trait_name_str,
                ops: &[ #( #method_names ),* ],
            };
    }
}

// ─── #[actor] ───────────────────────────────────────────────────────────────

#[proc_macro_attribute]
pub fn actor(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let trait_def = parse_macro_input!(item as ItemTrait);
    let trait_name = &trait_def.ident;

    // Collect methods: name + (input arg idents + types) + return type.
    let mut method_names: Vec<String> = Vec::new();
    let mut variant_idents: Vec<Ident> = Vec::new();
    let mut variant_arg_names: Vec<Vec<Ident>> = Vec::new();
    let mut variant_arg_types: Vec<Vec<syn::Type>> = Vec::new();
    let mut return_types: Vec<syn::Type> = Vec::new();

    for item in &trait_def.items {
        if let TraitItem::Fn(func) = item {
            let name = func.sig.ident.to_string();
            variant_idents.push(Ident::new(&snake_to_pascal(&name), Span::call_site()));
            method_names.push(name);

            let mut arg_names: Vec<Ident> = Vec::new();
            let mut arg_types: Vec<syn::Type> = Vec::new();
            for input in &func.sig.inputs {
                if let syn::FnArg::Typed(pat_type) = input {
                    if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
                        arg_names.push(pat_ident.ident.clone());
                        arg_types.push(*pat_type.ty.clone());
                    }
                }
            }
            variant_arg_names.push(arg_names);
            variant_arg_types.push(arg_types);

            let ret = match &func.sig.output {
                syn::ReturnType::Default => {
                    syn::parse_quote!(())
                }
                syn::ReturnType::Type(_, ty) => {
                    if func.sig.asyncness.is_some() {
                        // async fn: return type is the awaited result directly.
                        *ty.clone()
                    } else {
                        // sync fn: extract T from `impl Future<Output = T> + Send`
                        // (RPITIT — Rust 1.75+). Falls back to the raw type if the
                        // bound isn't found, so plain `-> ()` methods still compile
                        // (the generated `.await` will fail at the call site if the
                        // type is not actually a future).
                        extract_future_output(ty).unwrap_or_else(|| *ty.clone())
                    }
                }
            };
            return_types.push(ret);
        }
    }

    let msg_enum_name = Ident::new(&format!("{trait_name}Message"), Span::call_site());
    let actor_name = Ident::new(&format!("{trait_name}Actor"), Span::call_site());
    let handle_name = Ident::new(&format!("{trait_name}Handle"), Span::call_site());
    let trait_name_str = trait_name.to_string();
    let actor_manifest_const = Ident::new(
        &format!("{}_ACTOR_MANIFEST", pascal_to_screaming(&trait_name_str)),
        Span::call_site(),
    );

    // Message enum variants: one per method, carrying args + oneshot sender.
    let msg_variants = variant_idents
        .iter()
        .zip(variant_arg_names.iter())
        .zip(variant_arg_types.iter())
        .zip(return_types.iter())
        .map(|(((variant, arg_names), arg_types), ret)| {
            let respond_field = if arg_names.is_empty() {
                quote! {
                    respond: ::tokio::sync::oneshot::Sender<#ret>,
                }
            } else {
                quote! {
                    #( #arg_names: #arg_types, )*
                    respond: ::tokio::sync::oneshot::Sender<#ret>,
                }
            };
            quote! { #variant { #respond_field } }
        });

    // Handle methods: one per trait method, does the oneshot dance.
    let handle_methods = variant_idents
        .iter()
        .zip(method_names.iter())
        .zip(variant_arg_names.iter())
        .zip(variant_arg_types.iter())
        .zip(return_types.iter())
        .map(|((((variant, name), arg_names), arg_types), ret)| {
            let method_ident = Ident::new(name, Span::call_site());
            let send_args = if arg_names.is_empty() {
                quote! { respond: tx }
            } else {
                quote! { #( #arg_names, )* respond: tx }
            };
            quote! {
                pub async fn #method_ident(&self, #( #arg_names: #arg_types ),*) -> #ret {
                    let (tx, rx) = ::tokio::sync::oneshot::channel();
                    let _ = self.0.send(#msg_enum_name::#variant { #send_args }).await;
                    rx.await.expect("actor dropped before responding")
                }
            }
        });

    // Actor run loop: match dispatch.
    let dispatch_arms = variant_idents
        .iter()
        .zip(method_names.iter())
        .zip(variant_arg_names.iter())
        .map(|((variant, name), arg_names)| {
            let method_ident = Ident::new(name, Span::call_site());
            let call_args = if arg_names.is_empty() {
                quote! {}
            } else {
                quote! { #( #arg_names ),* }
            };
            let pat = if arg_names.is_empty() {
                quote! { #msg_enum_name::#variant { respond } }
            } else {
                quote! { #msg_enum_name::#variant { #( #arg_names, )* respond } }
            };
            quote! {
                #pat => { let _ = respond.send(self.handler.#method_ident(#call_args).await); }
            }
        });

    // Service machinery (op enum, dispatch table, manifest fragment) via service_inner.
    let service_tokens = service_inner(trait_def.clone());

    quote! {
        #service_tokens

        #[allow(missing_docs)]
        pub enum #msg_enum_name {
            #( #msg_variants, )*
        }

        pub struct #actor_name<H> {
            pub handler: H,
            pub inbox: ::tokio::sync::mpsc::Receiver<#msg_enum_name>,
        }

        impl<H: #trait_name> #actor_name<H> {
            pub async fn run(mut self) {
                while let Some(msg) = self.inbox.recv().await {
                    match msg {
                        #( #dispatch_arms )*
                    }
                }
            }
        }

        #[derive(Clone)]
        pub struct #handle_name(pub ::std::sync::Arc<::tokio::sync::mpsc::Sender<#msg_enum_name>>);

        impl #handle_name {
            pub fn new(tx: ::tokio::sync::mpsc::Sender<#msg_enum_name>) -> Self {
                Self(::std::sync::Arc::new(tx))
            }

            #( #handle_methods )*
        }

        #[doc = "Typed actor manifest for `"]
        #[doc = #trait_name_str]
        #[doc = "`. Generated by `#[actor]`."]
        pub const #actor_manifest_const: ::extrolib::core::manifest::ActorManifest<'static> =
            ::extrolib::core::manifest::ActorManifest {
                trait_name: #trait_name_str,
                ops: &[
                    #( ::extrolib::core::manifest::OpDef { name: #method_names } ),*
                ],
            };
    }
    .into()
}

// ─── #[deny_floats] ─────────────────────────────────────────────────────────

/// Attribute macro: compile-error if any field of the annotated struct or
/// enum variant contains `f32` or `f64` (Open Invariant #47).
///
/// Apply to every type with `#[derive(Archive, ...)]`.
#[proc_macro_attribute]
pub fn deny_floats(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = item.clone();
    let ast = parse_macro_input!(input as DeriveInput);

    let errors = collect_float_fields(&ast);
    if errors.is_empty() {
        // Pass the item through unchanged.
        item
    } else {
        let msgs = errors.iter().map(|m| {
            let msg = m.as_str();
            quote! { ::core::compile_error!(#msg); }
        });
        let orig: TokenStream2 = item.into();
        quote! { #orig #( #msgs )* }.into()
    }
}

fn collect_float_fields(input: &DeriveInput) -> Vec<String> {
    let mut errors = Vec::new();
    match &input.data {
        Data::Struct(s) => check_fields(&s.fields, &input.ident.to_string(), &mut errors),
        Data::Enum(e) => {
            for variant in &e.variants {
                check_fields(
                    &variant.fields,
                    &format!("{}::{}", input.ident, variant.ident),
                    &mut errors,
                );
            }
        }
        Data::Union(_) => {}
    }
    errors
}

fn check_fields(fields: &syn::Fields, ctx: &str, errors: &mut Vec<String>) {
    for field in fields.iter() {
        if is_float_type(&field.ty) {
            let field_name = field
                .ident
                .as_ref()
                .map(|i| i.to_string())
                .unwrap_or_else(|| "_".to_string());
            errors.push(format!(
                "#[deny_floats]: field `{field_name}` in `{ctx}` has float type (f32/f64). \
                 Floats are forbidden in archived types (Invariant #47)."
            ));
        }
    }
}

fn is_float_type(ty: &syn::Type) -> bool {
    match ty {
        syn::Type::Path(tp) => {
            if let Some(seg) = tp.path.segments.last() {
                let name = seg.ident.to_string();
                matches!(name.as_str(), "f32" | "f64")
            } else {
                false
            }
        }
        _ => false,
    }
}

// ─── #[derive(Frame)] ───────────────────────────────────────────────────────

#[proc_macro_derive(Frame, attributes(frame_repr, frame, frame_crate))]
pub fn derive_frame(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let name_str = name.to_string();

    // Optional #[frame_crate("crate")] — overrides the default `::extrolib_core` path.
    // Used when deriving Frame from within extrolib-core itself.
    let core_path: TokenStream2 = {
        let mut path: Option<String> = None;
        for attr in &input.attrs {
            if attr.path().is_ident("frame_crate") {
                if let Ok(lit) = attr.parse_args::<LitStr>() {
                    path = Some(lit.value());
                } else if let syn::Meta::NameValue(ref nv) = attr.meta {
                    if let syn::Expr::Lit(ref el) = nv.value {
                        if let syn::Lit::Str(ref ls) = el.lit {
                            path = Some(ls.value());
                        }
                    }
                }
            }
        }
        match path.as_deref() {
            Some("crate") => quote! { crate },
            Some(p) => {
                let ts: TokenStream2 = p.parse().expect("frame_crate: invalid path");
                ts
            }
            None => quote! { ::extrolib::core },
        }
    };

    let variants = match &input.data {
        Data::Enum(data) => &data.variants,
        _ => panic!("#[derive(Frame)] is only valid on enums"),
    };

    let mut to_byte_arms = Vec::new();
    let mut from_byte_arms = Vec::new();
    let mut variant_name_strs: Vec<String> = Vec::new();
    let mut variant_byte_vals: Vec<u8> = Vec::new();

    for variant in variants {
        let var_name = &variant.ident;
        let var_str = var_name.to_string();

        // Look for #[frame(0xNN)]
        let mut frame_byte: Option<u8> = None;
        for attr in &variant.attrs {
            if attr.path().is_ident("frame") {
                let lit: LitInt = attr
                    .parse_args()
                    .expect("#[frame(0xNN)] expects an integer literal");
                let byte_val: u8 = lit
                    .base10_parse()
                    .expect("#[frame(0xNN)] value must fit in u8");
                frame_byte = Some(byte_val);
            }
        }

        if let Some(b) = frame_byte {
            to_byte_arms.push(quote! { Self::#var_name => #b, });
            from_byte_arms.push(quote! { #b => ::core::result::Result::Ok(Self::#var_name), });
            variant_name_strs.push(var_str);
            variant_byte_vals.push(b);
        }
    }

    let manifest_const = Ident::new(
        &format!("{}_FRAMES_MANIFEST", pascal_to_screaming(&name_str)),
        Span::call_site(),
    );

    quote! {
        impl #core_path::FrameCode for #name {
            fn to_byte(self) -> u8 {
                match self {
                    #( #to_byte_arms )*
                }
            }

            fn try_from_byte(b: u8) -> ::core::result::Result<Self, #core_path::FrameError> {
                match b {
                    #( #from_byte_arms )*
                    _ => ::core::result::Result::Err(#core_path::FrameError::Unknown(b)),
                }
            }
        }

        pub const #manifest_const: #core_path::manifest::FramesManifest<'static> =
            #core_path::manifest::FramesManifest {
                name: #name_str,
                variants: &[
                    #( #core_path::manifest::FrameVariant { name: #variant_name_strs, byte: #variant_byte_vals } ),*
                ],
            };
    }
    .into()
}

// ─── #[derive(StatusMap)] ───────────────────────────────────────────────────

#[proc_macro_derive(StatusMap, attributes(status))]
pub fn derive_status_map(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let name_str = name.to_string();

    let variants = match &input.data {
        Data::Enum(data) => &data.variants,
        _ => panic!("#[derive(StatusMap)] is only valid on enums"),
    };

    let mut arms = Vec::new();
    let mut variant_name_strs: Vec<String> = Vec::new();
    let mut variant_code_vals: Vec<u16> = Vec::new();

    for variant in variants {
        let var_name = &variant.ident;
        let var_str = var_name.to_string();

        // Look for #[status(NNN)]
        let mut status_code: Option<u16> = None;
        for attr in &variant.attrs {
            if attr.path().is_ident("status") {
                let lit: LitInt = attr
                    .parse_args()
                    .expect("#[status(NNN)] expects an integer literal");
                let code: u16 = lit
                    .base10_parse()
                    .expect("#[status(NNN)] value must fit in u16");
                status_code = Some(code);
            }
        }

        let code = status_code.unwrap_or(500);

        // Build the pattern — variants may have fields (named or unnamed).
        let pat = match &variant.fields {
            syn::Fields::Unit => quote! { Self::#var_name },
            syn::Fields::Named(_) => quote! { Self::#var_name { .. } },
            syn::Fields::Unnamed(_) => quote! { Self::#var_name(..) },
        };
        arms.push(quote! { #pat => #code, });

        variant_name_strs.push(var_str);
        variant_code_vals.push(code);
    }

    let manifest_const = Ident::new(
        &format!("{}_STATUS_MANIFEST", pascal_to_screaming(&name_str)),
        Span::call_site(),
    );

    quote! {
        impl ::extrolib::core::StatusMap for #name {
            fn http_status(&self) -> u16 {
                match self {
                    #( #arms )*
                }
            }
        }

        pub const #manifest_const: ::extrolib::core::manifest::StatusManifest<'static> =
            ::extrolib::core::manifest::StatusManifest {
                name: #name_str,
                variants: &[
                    #( ::extrolib::core::manifest::StatusVariant { name: #variant_name_strs, http: #variant_code_vals } ),*
                ],
            };
    }
    .into()
}

// ─── RPITIT helper ──────────────────────────────────────────────────────────

/// Extract `T` from `impl Future<Output = T> + ...`.
///
/// Walks the `ImplTrait` bounds, finds the `Future` trait bound, and returns
/// the type bound to its `Output` associated type.  Returns `None` if the type
/// is not a recognised `impl Future<Output = T>` shape.
fn extract_future_output(ty: &syn::Type) -> Option<syn::Type> {
    let syn::Type::ImplTrait(impl_trait) = ty else {
        return None;
    };
    for bound in &impl_trait.bounds {
        let syn::TypeParamBound::Trait(trait_bound) = bound else {
            continue;
        };
        let last_seg = trait_bound.path.segments.last()?;
        if last_seg.ident != "Future" {
            continue;
        }
        let syn::PathArguments::AngleBracketed(args) = &last_seg.arguments else {
            continue;
        };
        for arg in &args.args {
            let syn::GenericArgument::AssocType(assoc) = arg else {
                continue;
            };
            if assoc.ident == "Output" {
                return Some(assoc.ty.clone());
            }
        }
    }
    None
}

// ─── Helpers ────────────────────────────────────────────────────────────────

fn snake_to_pascal(s: &str) -> String {
    s.split('_')
        .map(|w| {
            let mut c = w.chars();
            c.next()
                .map(|f| f.to_uppercase().collect::<String>() + c.as_str())
                .unwrap_or_default()
        })
        .collect()
}

fn pascal_to_snake(s: &str) -> String {
    s.chars()
        .enumerate()
        .flat_map(|(i, c)| {
            if c.is_uppercase() && i != 0 {
                vec!['_', c.to_ascii_lowercase()]
            } else {
                vec![c.to_ascii_lowercase()]
            }
        })
        .collect()
}

fn pascal_to_screaming(s: &str) -> String {
    pascal_to_snake(s).to_uppercase()
}
