//! FormatEnum derive macro — unified string mapping for serde + sqlx.
//! (Backward-compatible alias: PersistEnum)

use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Ident, Variant};

/// Rename strategy for enum variants.
enum RenameAll {
    SnakeCase,
    Lowercase,
}

impl RenameAll {
    fn parse(s: &str) -> Option<Self> {
        match s {
            "snake_case" => Some(RenameAll::SnakeCase),
            "lowercase" => Some(RenameAll::Lowercase),
            _ => None,
        }
    }

    fn apply(&self, ident: &str) -> String {
        match self {
            RenameAll::SnakeCase => to_snake_case(ident),
            RenameAll::Lowercase => ident.to_lowercase(),
        }
    }
}

/// Convert CamelCase to snake_case.
/// ReadMsg -> read_msg, Idle -> idle, SendMsg -> send_msg
fn to_snake_case(s: &str) -> String {
    let mut result = String::with_capacity(s.len() + 4);
    for (i, ch) in s.chars().enumerate() {
        if ch.is_uppercase() {
            if i > 0 {
                result.push('_');
            }
            result.push(ch.to_lowercase().next().unwrap());
        } else {
            result.push(ch);
        }
    }
    result
}

/// Parsed info about each variant.
struct VariantInfo {
    ident: Ident,
    /// None for unit variants, Some for the `other` variant.
    is_other: bool,
    /// The string representation (e.g. "read_msg").
    string_value: String,
}

/// Check if an attribute is `format_enum` or `persist_enum` (backward compat).
fn is_format_enum_attr(attr: &syn::Attribute) -> bool {
    attr.path().is_ident("format_enum") || attr.path().is_ident("persist_enum")
}

/// Extract `#[format_enum(rename_all = "...")]` from enum-level attributes.
fn extract_rename_all(input: &DeriveInput) -> Result<RenameAll, syn::Error> {
    for attr in &input.attrs {
        if !is_format_enum_attr(attr) {
            continue;
        }
        let mut rename = None;
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename_all") {
                let value = meta.value()?;
                let lit: syn::LitStr = value.parse()?;
                rename = Some(lit);
            }
            Ok(())
        })?;
        if let Some(lit) = rename {
            return RenameAll::parse(&lit.value()).ok_or_else(|| {
                syn::Error::new_spanned(
                    &lit,
                    "unsupported rename_all value; expected \"snake_case\" or \"lowercase\"",
                )
            });
        }
    }
    Err(syn::Error::new_spanned(
        &input.ident,
        "FormatEnum requires #[format_enum(rename_all = \"...\")]",
    ))
}

/// Check if a variant has `#[format_enum(other)]` or `#[persist_enum(other)]`.
fn is_other_variant(v: &Variant) -> bool {
    v.attrs.iter().any(|attr| {
        if !is_format_enum_attr(attr) {
            return false;
        }
        let mut found = false;
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("other") {
                found = true;
            }
            Ok(())
        });
        found
    })
}

/// Validate and collect variant info.
fn collect_variants(
    variants: &syn::punctuated::Punctuated<Variant, syn::Token![,]>,
    rename: &RenameAll,
) -> Result<Vec<VariantInfo>, syn::Error> {
    let mut infos = Vec::new();
    let mut other_count = 0;

    for v in variants {
        let is_other = is_other_variant(v);

        if is_other {
            other_count += 1;
            if other_count > 1 {
                return Err(syn::Error::new_spanned(
                    v,
                    "at most one #[format_enum(other)] variant allowed",
                ));
            }
            // Must be tuple variant with exactly one String field
            match &v.fields {
                Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {}
                _ => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "#[format_enum(other)] variant must be a tuple variant with one String field",
                    ));
                }
            }
            infos.push(VariantInfo {
                ident: v.ident.clone(),
                is_other: true,
                string_value: String::new(), // not used for other
            });
        } else {
            // Must be unit variant
            match &v.fields {
                Fields::Unit => {}
                _ => {
                    return Err(syn::Error::new_spanned(
                        v,
                        "FormatEnum variants must be unit variants (or use #[format_enum(other)] for a String fallback)",
                    ));
                }
            }
            let string_value = rename.apply(&v.ident.to_string());
            infos.push(VariantInfo {
                ident: v.ident.clone(),
                is_other: false,
                string_value,
            });
        }
    }

    Ok(infos)
}

pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = syn::parse_macro_input!(input as DeriveInput);
    match derive_inner(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn derive_inner(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
    let enum_name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let variants = match &input.data {
        Data::Enum(data) => &data.variants,
        _ => {
            return Err(syn::Error::new_spanned(
                enum_name,
                "FormatEnum can only be derived for enums",
            ));
        }
    };

    let rename = extract_rename_all(input)?;
    let infos = collect_variants(variants, &rename)?;

    // ── as_str() ──
    let as_str_arms: Vec<_> = infos
        .iter()
        .map(|v| {
            let ident = &v.ident;
            if v.is_other {
                quote! { #enum_name::#ident(ref s) => s.as_str() }
            } else {
                let val = &v.string_value;
                quote! { #enum_name::#ident => #val }
            }
        })
        .collect();

    // ── from_str match arms (with Ok wrapper for Deserialize/FromSql) ──
    let from_str_arms: Vec<_> = infos
        .iter()
        .filter(|v| !v.is_other)
        .map(|v| {
            let ident = &v.ident;
            let val = &v.string_value;
            quote! { #val => Ok(#enum_name::#ident) }
        })
        .collect();

    let other_ident = infos.iter().find(|v| v.is_other).map(|v| &v.ident);

    // Build the known variant strings for error messages
    let known_strings: Vec<_> = infos
        .iter()
        .filter(|v| !v.is_other)
        .map(|v| &v.string_value)
        .collect();

    let deserialize_fallback = if let Some(other_id) = other_ident {
        quote! { other => Ok(#enum_name::#other_id(other.to_owned())) }
    } else {
        quote! {
            other => Err(serde::de::Error::unknown_variant(other, &[#(#known_strings),*]))
        }
    };

    let sqlx_decode_fallback = if let Some(other_id) = other_ident {
        quote! { other => Ok(#enum_name::#other_id(other.to_owned())) }
    } else {
        quote! {
            other => Err(format!("unknown variant for {}: {}", stringify!(#enum_name), other).into())
        }
    };

    let output = quote! {
        // ── as_str() + Display ──
        impl #impl_generics #enum_name #ty_generics #where_clause {
            pub fn as_str(&self) -> &str {
                match self {
                    #(#as_str_arms,)*
                }
            }
        }

        impl #impl_generics ::std::fmt::Display for #enum_name #ty_generics #where_clause {
            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
                f.write_str(self.as_str())
            }
        }

        // ── Serde Serialize ──
        impl #impl_generics ::serde::Serialize for #enum_name #ty_generics #where_clause {
            fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
                serializer.serialize_str(self.as_str())
            }
        }

        // ── Serde Deserialize ──
        impl<'de> ::serde::Deserialize<'de> for #enum_name #ty_generics #where_clause {
            fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
                let s = <String as ::serde::Deserialize>::deserialize(deserializer)?;
                match s.as_str() {
                    #(#from_str_arms,)*
                    #deserialize_fallback
                }
            }
        }

        // ── sqlx Type ──
        impl #impl_generics ::mad_hatter::__sqlx::Type<::mad_hatter::__sqlx::sqlite::Sqlite>
            for #enum_name #ty_generics #where_clause
        {
            fn type_info() -> ::mad_hatter::__sqlx::sqlite::SqliteTypeInfo {
                <String as ::mad_hatter::__sqlx::Type<::mad_hatter::__sqlx::sqlite::Sqlite>>::type_info()
            }
        }

        // ── sqlx Encode ──
        impl<'__q> ::mad_hatter::__sqlx::Encode<'__q, ::mad_hatter::__sqlx::sqlite::Sqlite>
            for #enum_name #ty_generics #where_clause
        {
            fn encode_by_ref(
                &self,
                args: &mut Vec<::mad_hatter::__sqlx::sqlite::SqliteArgumentValue<'__q>>,
            ) -> Result<::mad_hatter::__sqlx::encode::IsNull, ::mad_hatter::__sqlx::error::BoxDynError> {
                <String as ::mad_hatter::__sqlx::Encode<'__q, ::mad_hatter::__sqlx::sqlite::Sqlite>>::encode(
                    self.as_str().to_owned(), args
                )
            }
        }

        // ── sqlx Decode ──
        impl<'__r> ::mad_hatter::__sqlx::Decode<'__r, ::mad_hatter::__sqlx::sqlite::Sqlite>
            for #enum_name #ty_generics #where_clause
        {
            fn decode(
                value: ::mad_hatter::__sqlx::sqlite::SqliteValueRef<'__r>,
            ) -> Result<Self, ::mad_hatter::__sqlx::error::BoxDynError> {
                let s = <String as ::mad_hatter::__sqlx::Decode<'__r, ::mad_hatter::__sqlx::sqlite::Sqlite>>::decode(value)?;
                match s.as_str() {
                    #(#from_str_arms,)*
                    #sqlx_decode_fallback
                }
            }
        }
    };

    Ok(output)
}