//! Procedural macros for the `mire` event-sourcing library.
//!
//! The headline export is `#[derive(EventData)]` on an enum, which
//! emits `impl mire::EventData` by reading a required enum-level
//! `#[mire(entity = "X")]` and converting each variant's CamelCase
//! name to kebab-case, joined with a dot. See `mire::EventData` for
//! the runtime trait and `mire`'s README for the design rationale.

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{Data, DeriveInput, Fields, LitStr, parse_macro_input, spanned::Spanned};

#[proc_macro_derive(EventData, attributes(mire))]
pub fn derive_event_data(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match &input.data {
        Data::Enum(_) => derive_for_enum(&input),
        Data::Struct(_) => derive_for_struct(&input),
        Data::Union(_) => syn::Error::new(
            input.span(),
            "#[derive(EventData)] is not supported on unions",
        )
        .to_compile_error()
        .into(),
    }
}

/// `#[derive(IntoResponse)]` on an enum whose every variant is a single
/// unnamed field wrapping an event type. Generates a delegating
/// `IntoResponse` impl that calls `inner.into_response()` per variant.
///
/// ```rust,ignore
/// #[derive(IntoResponse)]
/// enum FlightOutcome {
///     Held(FlightHeld),
///     Backordered(FlightBackordered),
/// }
/// ```
///
/// Each variant must be a one-field tuple variant whose field's type
/// already implements `IntoResponse` (which the `#[derive(EventData)]`
/// macro provides for struct events). For variants needing custom
/// `is_error` / `error_reason`, write the impl by hand.
#[proc_macro_derive(IntoResponse)]
pub fn derive_into_response(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let enum_data = match &input.data {
        Data::Enum(e) => e,
        Data::Struct(_) | Data::Union(_) => {
            return syn::Error::new(
                input.span(),
                "#[derive(IntoResponse)] is only supported on enums — \
                 events should derive EventData (which provides IntoResponse) instead",
            )
            .to_compile_error()
            .into();
        }
    };

    let mut arms = Vec::with_capacity(enum_data.variants.len());
    for variant in &enum_data.variants {
        let variant_ident = &variant.ident;
        match &variant.fields {
            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
                arms.push(quote! {
                    Self::#variant_ident(inner) => ::mire::IntoResponse::into_response(inner),
                });
            }
            _ => {
                return syn::Error::new(
                    variant.span(),
                    "#[derive(IntoResponse)] requires each variant to be a single \
                     unnamed field, e.g. `Held(FlightHeld)`. For richer cases \
                     (struct/named-field variants, custom is_error, custom reason) \
                     write the impl by hand.",
                )
                .to_compile_error()
                .into();
            }
        }
    }

    let ty = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let expanded = quote! {
        #[automatically_derived]
        impl #impl_generics ::mire::IntoResponse for #ty #ty_generics #where_clause {
            fn into_response(self) -> ::mire::ResponseValue {
                match self {
                    #(#arms)*
                }
            }
        }
    };

    expanded.into()
}

fn derive_for_enum(input: &DeriveInput) -> TokenStream {
    let Data::Enum(enum_data) = &input.data else {
        unreachable!()
    };

    let entity = match read_entity(&input.attrs) {
        Ok(e) => e,
        Err(err) => return err.to_compile_error().into(),
    };

    let mut arms = Vec::with_capacity(enum_data.variants.len());
    for variant in &enum_data.variants {
        let variant_ident = &variant.ident;
        let suffix = match read_rename(&variant.attrs) {
            Ok(Some(s)) => s,
            Ok(None) => to_kebab(&variant_ident.to_string()),
            Err(err) => return err.to_compile_error().into(),
        };
        let event_type = format!("{entity}.{suffix}");

        let pattern = match &variant.fields {
            Fields::Named(_) => quote! { Self::#variant_ident { .. } },
            Fields::Unnamed(_) => quote! { Self::#variant_ident(..) },
            Fields::Unit => quote! { Self::#variant_ident },
        };

        arms.push(quote! { #pattern => #event_type, });
    }

    let ty = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let expanded = quote! {
        #[automatically_derived]
        impl #impl_generics ::mire::EventData for #ty #ty_generics #where_clause {
            fn event_type(&self) -> &'static str {
                match self {
                    #(#arms)*
                }
            }
        }
    };

    expanded.into()
}

/// `#[derive(EventData)]` on a struct takes a `#[mire(event_type = "...")]`
/// attribute and emits THREE impls from one declaration:
///   - `EventData::event_type(&self)`
///   - `EventTypeStatic::EVENT_TYPE`
///   - `mire_sagas::IntoResponse::into_response()` so the struct can be
///     returned directly from a request handler.
fn derive_for_struct(input: &DeriveInput) -> TokenStream {
    let event_type = match read_event_type(&input.attrs) {
        Ok(e) => e,
        Err(err) => return err.to_compile_error().into(),
    };

    let ty = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let expanded = quote! {
        #[automatically_derived]
        impl #impl_generics ::mire::EventData for #ty #ty_generics #where_clause {
            fn event_type(&self) -> &'static str {
                #event_type
            }
        }

        #[automatically_derived]
        impl #impl_generics ::mire::EventTypeStatic for #ty #ty_generics #where_clause {
            const EVENT_TYPE: &'static str = #event_type;
        }

        #[automatically_derived]
        impl #impl_generics ::mire::IntoResponse for #ty #ty_generics #where_clause {
            fn into_response(self) -> ::mire::ResponseValue {
                // A non-serialisable response (e.g. a map with non-string
                // keys) becomes an error *response* rather than a panic —
                // the saga routes it through the step-failure/compensation
                // path instead of crashing the host (review Phase 5,
                // fallible IntoResponse).
                match ::serde_json::to_value(&self) {
                    ::core::result::Result::Ok(payload) => ::mire::ResponseValue {
                        event_type: <Self as ::mire::EventTypeStatic>::EVENT_TYPE,
                        payload,
                        is_error: false,
                        error_reason: ::core::option::Option::None,
                    },
                    ::core::result::Result::Err(e) => ::mire::ResponseValue {
                        event_type: <Self as ::mire::EventTypeStatic>::EVENT_TYPE,
                        payload: ::serde_json::Value::Null,
                        is_error: true,
                        error_reason: ::core::option::Option::Some(::std::format!(
                            "response of type `{}` failed to serialise: {e}",
                            ::core::any::type_name::<Self>(),
                        )),
                    },
                }
            }
        }
    };

    expanded.into()
}

/// Read `#[mire(entity = "name")]` from the enum's attributes. Required.
fn read_entity(attrs: &[syn::Attribute]) -> syn::Result<String> {
    let mut entity: Option<String> = None;
    let mut span_for_error = Span::call_site();

    for attr in attrs {
        if !attr.path().is_ident("mire") {
            continue;
        }
        span_for_error = attr.span();
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("entity") {
                let lit: LitStr = meta.value()?.parse()?;
                let value = lit.value();
                if value.is_empty() {
                    return Err(meta.error("#[mire(entity = ...)] must be a non-empty string"));
                }
                if value.contains('.') {
                    return Err(meta.error(
                        "entity must not contain '.' — the dot is the entity/variant separator",
                    ));
                }
                entity = Some(value);
                Ok(())
            } else if meta.path.is_ident("rename") {
                Err(meta.error("#[mire(rename = ...)] is only valid on variants, not on the enum"))
            } else {
                Err(meta.error("unknown #[mire(...)] argument; expected `entity = \"...\"`"))
            }
        })?;
    }

    entity.ok_or_else(|| {
        syn::Error::new(
            span_for_error,
            "missing required attribute: add `#[mire(entity = \"<name>\")]` above the enum. \
             The entity name becomes the persisted event_type prefix (e.g. `account.opened`).",
        )
    })
}

/// Read `#[mire(event_type = "...")]` from a struct's attributes. Required
/// for struct-shaped events. The string becomes the durable event_type both
/// at runtime (`EventData::event_type`) and at compile time
/// (`EventTypeStatic::EVENT_TYPE`).
fn read_event_type(attrs: &[syn::Attribute]) -> syn::Result<String> {
    let mut event_type: Option<String> = None;
    let mut span_for_error = Span::call_site();

    for attr in attrs {
        if !attr.path().is_ident("mire") {
            continue;
        }
        span_for_error = attr.span();
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("event_type") {
                let lit: LitStr = meta.value()?.parse()?;
                let value = lit.value();
                if value.is_empty() {
                    return Err(meta.error("#[mire(event_type = ...)] must be a non-empty string"));
                }
                event_type = Some(value);
                Ok(())
            } else if meta.path.is_ident("entity") {
                Err(meta.error(
                    "#[mire(entity = ...)] is only valid on enums; on structs use `event_type = \"...\"`",
                ))
            } else if meta.path.is_ident("rename") {
                Err(meta.error(
                    "#[mire(rename = ...)] is only valid on variants",
                ))
            } else {
                Err(meta.error(
                    "unknown #[mire(...)] argument on struct; expected `event_type = \"...\"`",
                ))
            }
        })?;
    }

    event_type.ok_or_else(|| {
        syn::Error::new(
            span_for_error,
            "missing required attribute: add `#[mire(event_type = \"...\")]` above the struct. \
             The string becomes the persisted event_type and the EventTypeStatic const.",
        )
    })
}

/// Read `#[mire(rename = "kebab-name")]` from a variant's attributes. Optional.
fn read_rename(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
    let mut rename: Option<String> = None;

    for attr in attrs {
        if !attr.path().is_ident("mire") {
            continue;
        }
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename") {
                let lit: LitStr = meta.value()?.parse()?;
                let value = lit.value();
                if value.is_empty() {
                    return Err(meta.error("#[mire(rename = ...)] must be a non-empty string"));
                }
                if value.contains('.') {
                    return Err(meta.error(
                        "variant rename must not contain '.' — the entity prefix is locked at the enum level",
                    ));
                }
                rename = Some(value);
                Ok(())
            } else if meta.path.is_ident("entity") {
                Err(meta.error(
                    "#[mire(entity = ...)] belongs on the enum, not on a variant",
                ))
            } else {
                Err(meta.error("unknown #[mire(...)] argument; expected `rename = \"...\"`"))
            }
        })?;
    }

    Ok(rename)
}

/// Convert a CamelCase identifier to kebab-case:
/// - `Opened` → `opened`
/// - `MoneyDeposited` → `money-deposited`
/// - `HTTPError` → `http-error` (consecutive uppercase letters stay grouped)
/// - `OrderV2` → `order-v2` (split on lowercase→uppercase boundary)
fn to_kebab(name: &str) -> String {
    let chars: Vec<char> = name.chars().collect();
    let mut out = String::with_capacity(name.len() + 4);

    for (i, &ch) in chars.iter().enumerate() {
        if i == 0 {
            out.push(ch.to_ascii_lowercase());
            continue;
        }
        let prev = chars[i - 1];
        let next = chars.get(i + 1).copied();
        // Split before an uppercase letter that starts a new word:
        //   - lowercase → upper: `aB` → `a-b`
        //   - upper-upper-lower: `ABc` → `A-Bc` (so `HTTPError` → `http-error`)
        let split_lower_to_upper = ch.is_ascii_uppercase() && prev.is_ascii_lowercase();
        let split_acronym_end = ch.is_ascii_uppercase()
            && prev.is_ascii_uppercase()
            && matches!(next, Some(n) if n.is_ascii_lowercase());
        if split_lower_to_upper || split_acronym_end {
            out.push('-');
        }
        out.push(ch.to_ascii_lowercase());
    }
    out
}

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

    #[test]
    fn kebab_basics() {
        assert_eq!(to_kebab("Opened"), "opened");
        assert_eq!(to_kebab("MoneyDeposited"), "money-deposited");
        assert_eq!(to_kebab("Closed"), "closed");
        assert_eq!(to_kebab("ABC"), "abc");
        assert_eq!(to_kebab("HTTPError"), "http-error");
        assert_eq!(to_kebab("OrderV2"), "order-v2");
        assert_eq!(
            to_kebab("OrderShippedToWarehouse"),
            "order-shipped-to-warehouse"
        );
    }
}
