//! OsisObject derive macro for OSIS database objects.
//!
//! Generates the `OsisObject` trait implementation for structs.

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Meta, Token, parse_macro_input, punctuated::Punctuated};

/// Derive macro for implementing `OsisObject` trait.
///
/// The macro assumes the struct has a field named `sid` of type `SmartId`.
/// The `type_name()` defaults to the struct name in snake_case, but can be
/// overridden with `#[osis(type_name = "custom_name")]`.
///
/// # Indexing Support
///
/// Specify indexed fields at the struct level with `#[osis(index = "field1, field2")]`:
///
/// ```ignore
/// use herolib_derive::OsisObject;
/// use herolib_osis::sid::SmartId;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Default, Serialize, Deserialize, OsisObject)]
/// #[osis(index = "title, content")]
/// struct Article {
///     sid: SmartId,
///     title: String,
///     content: String,
///     author: String,  // Not indexed
/// }
///
/// // Can combine with type_name
/// #[derive(Default, Serialize, Deserialize, OsisObject)]
/// #[osis(type_name = "articles", index = "title, content")]
/// struct Article2 {
///     sid: SmartId,
///     title: String,
///     content: String,
/// }
/// ```
///
/// # Example
///
/// ```ignore
/// use herolib_derive::OsisObject;
/// use herolib_osis::sid::SmartId;
/// use serde::{Serialize, Deserialize};
///
/// // Auto-generated type_name: "user"
/// #[derive(Default, Serialize, Deserialize, OsisObject)]
/// struct User {
///     sid: SmartId,
///     name: String,
/// }
///
/// // Auto-generated type_name: "user_profile"
/// #[derive(Default, Serialize, Deserialize, OsisObject)]
/// struct UserProfile {
///     sid: SmartId,
///     email: String,
/// }
///
/// // Custom type_name: "members"
/// #[derive(Default, Serialize, Deserialize, OsisObject)]
/// #[osis(type_name = "members")]
/// struct Member {
///     sid: SmartId,
///     name: String,
/// }
/// ```
pub fn impl_osis_object(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    // Check that it's a struct with named fields
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => {
                return syn::Error::new_spanned(
                    &input,
                    "OsisObject can only be derived for structs with named fields",
                )
                .to_compile_error()
                .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(&input, "OsisObject can only be derived for structs")
                .to_compile_error()
                .into();
        }
    };

    // Check that the struct has a `sid` field
    let has_sid = fields
        .iter()
        .any(|f| f.ident.as_ref().is_some_and(|ident| ident == "sid"));

    if !has_sid {
        return syn::Error::new_spanned(
            &input,
            "OsisObject requires a field named `sid` of type `SmartId`",
        )
        .to_compile_error()
        .into();
    }

    // Parse osis attributes
    let osis_attrs = parse_osis_attrs(&input);

    // Get type_name from attribute or derive from struct name
    let type_name = osis_attrs
        .type_name
        .unwrap_or_else(|| to_snake_case(&name.to_string()));

    // Get indexed field names from attribute
    let indexed_field_names: Vec<String> = osis_attrs.index_fields;

    // Validate that indexed fields exist in the struct
    let field_names: Vec<String> = fields
        .iter()
        .filter_map(|f| f.ident.as_ref().map(|i| i.to_string()))
        .collect();

    for idx_field in &indexed_field_names {
        if !field_names.contains(idx_field) {
            return syn::Error::new_spanned(
                &input,
                format!(
                    "indexed field '{}' does not exist in struct '{}'",
                    idx_field, name
                ),
            )
            .to_compile_error()
            .into();
        }
    }

    // Find the indexed fields with their type info
    let indexed_fields_info: Vec<(&syn::Ident, bool)> = fields
        .iter()
        .filter_map(|f| {
            let ident = f.ident.as_ref()?;
            if indexed_field_names.contains(&ident.to_string()) {
                // Check if the field type is Option<T>
                let is_option = is_option_type(&f.ty);
                Some((ident, is_option))
            } else {
                None
            }
        })
        .collect();

    // Generate indexed_fields() implementation - returns Vec<(field_name, value)>
    // For Option<T> fields, we use unwrap_or_default() to get an empty string if None
    let indexed_field_exprs: Vec<TokenStream2> = indexed_fields_info
        .iter()
        .map(|(ident, is_option)| {
            let name = ident.to_string();
            if *is_option {
                quote! {
                    (#name.to_string(), self.#ident.as_ref().map(|v| v.to_string()).unwrap_or_default())
                }
            } else {
                quote! {
                    (#name.to_string(), self.#ident.to_string())
                }
            }
        })
        .collect();

    let indexed_field_name_strs: Vec<&str> =
        indexed_field_names.iter().map(|s| s.as_str()).collect();

    let indexed_fields_impl = if indexed_field_names.is_empty() {
        quote! {
            fn indexed_fields(&self) -> Vec<(String, String)> {
                Vec::new()
            }

            fn indexed_field_names() -> &'static [&'static str] {
                &[]
            }
        }
    } else {
        quote! {
            fn indexed_fields(&self) -> Vec<(String, String)> {
                vec![#(#indexed_field_exprs),*]
            }

            fn indexed_field_names() -> &'static [&'static str] {
                &[#(#indexed_field_name_strs),*]
            }
        }
    };

    let expanded = quote! {
        impl herolib_osis::db::OsisObject for #name {
            fn sid(&self) -> &herolib_osis::sid::SmartId {
                &self.sid
            }

            fn set_sid(&mut self, sid: herolib_osis::sid::SmartId) {
                self.sid = sid;
            }

            fn type_name() -> &'static str {
                #type_name
            }

            #indexed_fields_impl
        }
    };

    TokenStream::from(expanded)
}

/// Parsed osis attributes from the struct.
struct OsisAttrs {
    type_name: Option<String>,
    index_fields: Vec<String>,
}

/// Parse #[osis(...)] attributes from the struct.
/// Supports:
/// - `#[osis(type_name = "name")]`
/// - `#[osis(index = "field1, field2")]`
/// - `#[osis(type_name = "name", index = "field1, field2")]`
fn parse_osis_attrs(input: &DeriveInput) -> OsisAttrs {
    let mut attrs = OsisAttrs {
        type_name: None,
        index_fields: Vec::new(),
    };

    for attr in &input.attrs {
        if !attr.path().is_ident("osis") {
            continue;
        }

        if let Meta::List(meta_list) = &attr.meta {
            // Parse as comma-separated name-value pairs
            let parsed: Result<Punctuated<syn::MetaNameValue, Token![,]>, _> =
                meta_list.parse_args_with(Punctuated::parse_terminated);

            if let Ok(pairs) = parsed {
                for nv in pairs {
                    if nv.path.is_ident("type_name") {
                        if let syn::Expr::Lit(syn::ExprLit {
                            lit: syn::Lit::Str(lit_str),
                            ..
                        }) = &nv.value
                        {
                            attrs.type_name = Some(lit_str.value());
                        }
                    } else if nv.path.is_ident("index") {
                        if let syn::Expr::Lit(syn::ExprLit {
                            lit: syn::Lit::Str(lit_str),
                            ..
                        }) = &nv.value
                        {
                            // Parse comma-separated field names
                            attrs.index_fields = lit_str
                                .value()
                                .split(',')
                                .map(|s| s.trim().to_string())
                                .filter(|s| !s.is_empty())
                                .collect();
                        }
                    }
                }
            }
        }
    }

    attrs
}

/// Convert CamelCase to snake_case.
fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, ch) in s.chars().enumerate() {
        if ch.is_uppercase() {
            if i > 0 {
                result.push('_');
            }
            result.push(ch.to_ascii_lowercase());
        } else {
            result.push(ch);
        }
    }
    result
}

/// Check if a type is Option<T>.
fn is_option_type(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty {
        if let Some(segment) = type_path.path.segments.last() {
            return segment.ident == "Option";
        }
    }
    false
}

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

    #[test]
    fn test_to_snake_case() {
        assert_eq!(to_snake_case("User"), "user");
        assert_eq!(to_snake_case("UserProfile"), "user_profile");
        assert_eq!(to_snake_case("HTTPServer"), "h_t_t_p_server");
        assert_eq!(to_snake_case("MyAPIClient"), "my_a_p_i_client");
        assert_eq!(to_snake_case("Simple"), "simple");
    }
}
