use darling::ast::Data;
use darling::util::Ignored;
use darling::{FromDeriveInput, FromVariant};
use quote::quote;
use syn::{Attribute, DeriveInput, Expr, Field, Generics, Ident, LitStr, Result, Visibility};

use crate::snake_case;

#[derive(FromDeriveInput)]
#[darling(supports(enum_any), forward_attrs)]
struct StringEnumInput {
    ident: Ident,
    vis: Visibility,
    generics: Generics,
    attrs: Vec<Attribute>,
    data: Data<StringEnumVariant, Ignored>,
}

#[derive(FromVariant)]
#[darling(forward_attrs)]
struct StringEnumVariant {
    ident: Ident,
    fields: darling::ast::Fields<Field>,
    attrs: Vec<Attribute>,
    discriminant: Option<Expr>,
}

/// Attribute macro that wires up `Display` and `FromStr` for a string-backed
/// enum, optionally also injecting the matching `#[sea_orm(string_value)]` on
/// every variant so `DeriveActiveEnum` sees the correct DB strings without any
/// duplication.
///
/// Each variant's string form is taken from an optional `#[display("...")]`
/// attribute. When the attribute is absent the variant name is converted to
/// `snake_case` automatically.
///
/// When the enum itself carries a `#[sea_orm(...)]` attribute the macro
/// additionally emits `#[sea_orm(string_value = "...")]` on every variant.
/// Without it only the `parse_display` derives are injected, which is useful
/// for plain display/parse enums that are never stored in the database.
///
/// # Examples
///
/// ## SeaORM-backed enum
///
/// ```rust,ignore
/// #[string_enum]
/// #[derive(Clone, Debug, Default, DeriveActiveEnum, EnumIter)]
/// #[sea_orm(rs_type = "String", db_type = "Text")]
/// pub enum MediaType {
///     #[default]
///     Movie,
///     Series,
/// }
/// ```
///
/// ## Display/parse only (no database)
///
/// ```rust,ignore
/// #[string_enum]
/// #[derive(Clone, Debug, PartialEq)]
/// pub enum Lang {
///     #[display("zh")]
///     Chinese,
///     #[display("en")]
///     English,
/// }
/// ```
pub fn expand_string_enum(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
    let parsed = StringEnumInput::from_derive_input(input)?;

    let Data::Enum(parsed_variants) = parsed.data else {
        unreachable!("validated by darling")
    };

    let inject_sea_orm = parsed.attrs.iter().any(|a| a.path().is_ident("sea_orm"));
    let enum_attrs = parsed.attrs.iter().collect::<Vec<_>>();
    let ident = &parsed.ident;
    let vis = &parsed.vis;
    let generics = &parsed.generics;
    let mut variants = vec![];

    for parsed_variant in &parsed_variants {
        let display_str = extract_display_str(parsed_variant)?;
        let other_attrs = parsed_variant
            .attrs
            .iter()
            .filter(|a| !a.path().is_ident("display"))
            .collect::<Vec<_>>();
        let variant_ident = &parsed_variant.ident;
        let fields = &parsed_variant.fields;
        let discriminant = parsed_variant
            .discriminant
            .as_ref()
            .map(|expr| quote! { = #expr });
        let sea_orm_attr = inject_sea_orm.then(|| {
            quote! {
                #[sea_orm(string_value = #display_str)]
            }
        });

        variants.push(quote! {
            #( #other_attrs )*
            #[display(#display_str)]
            #sea_orm_attr
            #variant_ident #fields #discriminant
        });
    }

    Ok(quote! {
        #[derive(::parse_display::Display, ::parse_display::FromStr)]
        #( #enum_attrs )*
        #vis enum #ident #generics {
            #( #variants, )*
        }
    })
}

fn extract_display_str(variant: &StringEnumVariant) -> Result<String> {
    variant
        .attrs
        .iter()
        .find(|a| a.path().is_ident("display"))
        .map(|a| {
            a.parse_args::<LitStr>()
                .map(|lit| lit.value())
                .map_err(|e| {
                    syn::Error::new_spanned(a, format!("Failed to parse display string: {e}"))
                })
        })
        .unwrap_or_else(|| Ok(snake_case(&variant.ident.to_string())))
}
