//! `#[derive(Mizan)]` — emit `MizanType` impl + linkme registration.

use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    parse::Parser, punctuated::Punctuated, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit,
    Meta, Token,
};

use crate::shape::type_shape_expr;

/// Apply a `#[serde(rename_all = "...")]` casing transform to a Rust
/// variant identifier so the IR's enum variant matches what serde emits
/// on the wire. Supported casings mirror serde's set.
fn apply_rename_all(rule: &str, ident: &str) -> String {
    match rule {
        "lowercase" => ident.to_lowercase(),
        "UPPERCASE" => ident.to_uppercase(),
        "PascalCase" => ident.to_upper_camel_case(),
        "camelCase" => ident.to_lower_camel_case(),
        "snake_case" => ident.to_snake_case(),
        "SCREAMING_SNAKE_CASE" => ident.to_shouty_snake_case(),
        "kebab-case" => ident.to_kebab_case(),
        _ => ident.to_string(),
    }
}

/// Walk the enum's outer attributes for `#[serde(rename_all = "...")]`.
fn serde_rename_all(attrs: &[syn::Attribute]) -> Option<String> {
    for attr in attrs {
        if !attr.path().is_ident("serde") {
            continue;
        }
        let list = match &attr.meta {
            Meta::List(l) => l,
            _ => continue,
        };
        let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
        let metas = match parser.parse2(list.tokens.clone()) {
            Ok(m) => m,
            Err(_) => continue,
        };
        for meta in metas {
            if let Meta::NameValue(nv) = meta {
                if nv.path.is_ident("rename_all") {
                    if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
                        return Some(s.value());
                    }
                }
            }
        }
    }
    None
}

/// Walk a variant's attributes for an explicit `#[serde(rename = "...")]`
/// override. Variant-level rename overrides the enum-level rename_all.
fn serde_rename(attrs: &[syn::Attribute]) -> Option<String> {
    for attr in attrs {
        if !attr.path().is_ident("serde") {
            continue;
        }
        let list = match &attr.meta {
            Meta::List(l) => l,
            _ => continue,
        };
        let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
        let metas = match parser.parse2(list.tokens.clone()) {
            Ok(m) => m,
            Err(_) => continue,
        };
        for meta in metas {
            if let Meta::NameValue(nv) = meta {
                if nv.path.is_ident("rename") {
                    if let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = nv.value {
                        return Some(s.value());
                    }
                }
            }
        }
    }
    None
}

/// Expand `#[derive(Mizan)]`. Emits the `MizanType` impl AND a linkme
/// TypeEntry registration. Every Mizan-shaped type lands in the IR;
/// the emitter's inline-substitution pass collapses primitive-aliases
/// and enums at use sites so the IR stays tight.
pub fn expand(input: DeriveInput) -> TokenStream {
    let ident = input.ident.clone();
    let type_name = ident.to_string();

    let rename_all = serde_rename_all(&input.attrs);

    let named_type_body = match &input.data {
        Data::Struct(s) => emit_struct(s),
        Data::Enum(e) => emit_enum(e, rename_all.as_deref()),
        Data::Union(_) => {
            return syn::Error::new_spanned(
                &input,
                "#[derive(Mizan)] does not support `union` types — use a struct or enum.",
            )
            .to_compile_error();
        }
    };

    let register_static =
        quote::format_ident!("__MIZAN_TYPE_REGISTER_{}", ident.to_string().to_shouty_snake_case());

    quote! {
        impl ::mizan_core::MizanType for #ident {
            const TYPE_NAME: &'static str = #type_name;
            fn shape() -> ::mizan_core::NamedType { #named_type_body }
        }

        #[::mizan_core::__priv::linkme::distributed_slice(::mizan_core::TYPES)]
        #[linkme(crate = ::mizan_core::__priv::linkme)]
        #[allow(non_upper_case_globals)]
        static #register_static: ::mizan_core::TypeEntry = ::mizan_core::TypeEntry {
            name: #type_name,
            shape_fn: <#ident as ::mizan_core::MizanType>::shape,
        };
    }
}

fn emit_struct(s: &DataStruct) -> TokenStream {
    let fields = match &s.fields {
        Fields::Named(named) => &named.named,
        Fields::Unnamed(_) | Fields::Unit => {
            return syn::Error::new_spanned(
                &s.fields,
                "#[derive(Mizan)] requires named fields. Tuple structs and unit structs aren't part of the IR shape.",
            )
            .to_compile_error();
        }
    };

    let mut field_exprs: Vec<TokenStream> = Vec::new();
    for field in fields {
        let ident = field
            .ident
            .as_ref()
            .expect("named field always has an ident");
        // Field-level `#[serde(rename = "...")]` wins; otherwise strip
        // the raw-identifier prefix that Rust uses to escape keywords
        // (`r#type` → `type`). Serde itself strips the prefix when
        // computing the default field name; the IR has to match the
        // wire form, not the Rust source form.
        let raw_ident = ident.to_string();
        let stripped = raw_ident.strip_prefix("r#").unwrap_or(&raw_ident);
        let name = serde_rename(&field.attrs).unwrap_or_else(|| stripped.to_string());
        let shape = type_shape_expr(&field.ty);

        // A field is `required` iff its type is not `Option<...>`. Defaults
        // are not encodable from Rust syntax (no `= expr` on a struct field
        // declaration) — the macro emits `required: false, default: None`
        // for Option-wrapped fields, leaving defaults for a future
        // attribute-based extension.
        let is_optional = crate::shape::unwrap_option(&field.ty).is_some();
        let required = !is_optional;
        field_exprs.push(quote! {
            ::mizan_core::StructField {
                name: #name,
                required: #required,
                default: ::std::option::Option::None,
                shape: #shape,
            }
        });
    }

    quote! {
        ::mizan_core::NamedType::Struct(::std::vec![
            #(#field_exprs),*
        ])
    }
}

fn emit_enum(e: &DataEnum, rename_all: Option<&str>) -> TokenStream {
    let mut variants: Vec<TokenStream> = Vec::new();
    for variant in &e.variants {
        if !matches!(variant.fields, Fields::Unit) {
            return syn::Error::new_spanned(
                &variant.fields,
                "#[derive(Mizan)] only supports unit-variant enums (string-literal enums in the IR). Variants with payload aren't expressible in the current IR.",
            )
            .to_compile_error();
        }
        let raw = variant.ident.to_string();
        // Variant-level `#[serde(rename = "...")]` wins; otherwise apply
        // the enum-level `#[serde(rename_all = "...")]` rule.
        let name = if let Some(explicit) = serde_rename(&variant.attrs) {
            explicit
        } else if let Some(rule) = rename_all {
            apply_rename_all(rule, &raw)
        } else {
            raw
        };
        variants.push(quote! { #name });
    }
    quote! {
        ::mizan_core::NamedType::Enum(::std::vec![
            #(#variants),*
        ])
    }
}
