use proc_macro::TokenStream;
use quote::quote;
use syn::{
    Data, DeriveInput, Error, Expr, ExprLit, Fields, GenericArgument, Lit, Meta, PathArguments,
    Type, parse_macro_input,
};

#[proc_macro_derive(Tool, attributes(tool, description))]
pub fn derive_tool(input: TokenStream) -> TokenStream {
    match derive_tool_impl(parse_macro_input!(input as DeriveInput)) {
        Ok(tokens) => tokens.into(),
        Err(error) => error.to_compile_error().into(),
    }
}

fn derive_tool_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
    let name = input.ident;
    let (tool_name, tool_description) = parse_tool_attributes(&input.attrs, &name.to_string())?;
    let rename_rule = parse_serde_rename_all(&input.attrs)?;

    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields.named,
            _ => {
                return Err(Error::new_spanned(
                    name,
                    "Tool derive only supports structs with named fields",
                ));
            }
        },
        _ => {
            return Err(Error::new_spanned(
                name,
                "Tool derive only supports structs",
            ));
        }
    };

    let field_schemas = fields
        .iter()
        .map(|field| {
            let description = parse_description_attribute(&field.attrs)?;
            let (schema_tokens, required) = schema_tokens(&field.ty)?;
            let description_tokens = option_string_tokens(description.as_deref());
            let field_name = serde_field_name(field, rename_rule)?;

            Ok(quote! {
                ::menta::ToolFieldSchema {
                    name: #field_name.to_string(),
                    description: #description_tokens,
                    schema: #schema_tokens,
                    required: #required,
                }
            })
        })
        .collect::<Result<Vec<_>, Error>>()?;

    let tool_name_literal = tool_name;
    let tool_description_literal = tool_description;
    let object_description = option_string_tokens(Some(&tool_description_literal));

    Ok(quote! {
        impl ::menta::ToolInput for #name {
            fn tool_name() -> &'static str {
                #tool_name_literal
            }

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

            fn input_schema() -> ::menta::ToolSchema {
                ::menta::ToolSchema::Object(::menta::ToolObjectSchema {
                    description: #object_description,
                    fields: vec![#(#field_schemas),*],
                })
            }
        }

        impl #name {
            pub fn definition() -> ::menta::ToolDefinition {
                <Self as ::menta::ToolInput>::definition()
            }

            pub fn tool() -> ::menta::Tool
            where
                Self: ::menta::ToolExecute,
            {
                <Self as ::menta::ToolExecute>::tool()
            }
        }
    })
}

#[derive(Clone, Copy)]
enum RenameRule {
    LowerCase,
    UpperCase,
    SnakeCase,
    ScreamingSnakeCase,
    KebabCase,
    ScreamingKebabCase,
    CamelCase,
    PascalCase,
}

fn parse_tool_attributes(
    attrs: &[syn::Attribute],
    type_name: &str,
) -> Result<(String, String), Error> {
    let mut name = default_tool_name(type_name);
    let mut description = String::new();

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

        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("name") {
                let value = meta.value()?;
                let literal: syn::LitStr = value.parse()?;
                name = literal.value();
                Ok(())
            } else if meta.path.is_ident("description") {
                let value = meta.value()?;
                let literal: syn::LitStr = value.parse()?;
                description = literal.value();
                Ok(())
            } else {
                Err(meta.error("unsupported tool attribute"))
            }
        })?;
    }

    Ok((name, description))
}

fn parse_description_attribute(attrs: &[syn::Attribute]) -> Result<Option<String>, Error> {
    for attr in attrs {
        if !attr.path().is_ident("description") {
            continue;
        }

        match &attr.meta {
            Meta::NameValue(name_value) => match &name_value.value {
                Expr::Lit(ExprLit {
                    lit: Lit::Str(value),
                    ..
                }) => return Ok(Some(value.value())),
                _ => {
                    return Err(Error::new_spanned(
                        &name_value.value,
                        "description must be a string literal",
                    ));
                }
            },
            _ => {
                return Err(Error::new_spanned(
                    attr,
                    "description must use #[description = \"...\"]",
                ));
            }
        }
    }

    Ok(None)
}

fn parse_serde_rename_all(attrs: &[syn::Attribute]) -> Result<Option<RenameRule>, Error> {
    let mut rename_rule = None;

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

        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename_all") {
                let value = meta.value()?;
                let literal: syn::LitStr = value.parse()?;
                rename_rule = Some(parse_rename_rule(&literal.value(), literal)?);
            }
            Ok(())
        })?;
    }

    Ok(rename_rule)
}

fn serde_field_name(field: &syn::Field, rename_rule: Option<RenameRule>) -> Result<String, Error> {
    for attr in &field.attrs {
        if !attr.path().is_ident("serde") {
            continue;
        }

        let mut renamed = None;
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("rename") {
                let value = meta.value()?;
                let literal: syn::LitStr = value.parse()?;
                renamed = Some(literal.value());
            }
            Ok(())
        })?;

        if let Some(renamed) = renamed {
            return Ok(renamed);
        }
    }

    let ident = field
        .ident
        .as_ref()
        .ok_or_else(|| Error::new_spanned(field, "missing field name"))?
        .to_string();
    Ok(apply_rename_rule(&ident, rename_rule))
}

fn parse_rename_rule(value: &str, span: impl quote::ToTokens) -> Result<RenameRule, Error> {
    match value {
        "lowercase" => Ok(RenameRule::LowerCase),
        "UPPERCASE" => Ok(RenameRule::UpperCase),
        "snake_case" => Ok(RenameRule::SnakeCase),
        "SCREAMING_SNAKE_CASE" => Ok(RenameRule::ScreamingSnakeCase),
        "kebab-case" => Ok(RenameRule::KebabCase),
        "SCREAMING-KEBAB-CASE" => Ok(RenameRule::ScreamingKebabCase),
        "camelCase" => Ok(RenameRule::CamelCase),
        "PascalCase" => Ok(RenameRule::PascalCase),
        _ => Err(Error::new_spanned(
            span,
            format!("unsupported serde rename rule: {value}"),
        )),
    }
}

fn apply_rename_rule(value: &str, rename_rule: Option<RenameRule>) -> String {
    let Some(rename_rule) = rename_rule else {
        return value.to_string();
    };

    let words = split_words(value);
    match rename_rule {
        RenameRule::LowerCase => words.join(""),
        RenameRule::UpperCase => words.join("").to_ascii_uppercase(),
        RenameRule::SnakeCase => words.join("_"),
        RenameRule::ScreamingSnakeCase => words.join("_").to_ascii_uppercase(),
        RenameRule::KebabCase => words.join("-"),
        RenameRule::ScreamingKebabCase => words.join("-").to_ascii_uppercase(),
        RenameRule::CamelCase => {
            let mut result = String::new();
            for (index, word) in words.iter().enumerate() {
                if index == 0 {
                    result.push_str(word);
                } else {
                    result.push_str(&capitalize(word));
                }
            }
            result
        }
        RenameRule::PascalCase => words.iter().map(|word| capitalize(word)).collect(),
    }
}

fn split_words(value: &str) -> Vec<String> {
    value
        .split(['_', '-'])
        .filter(|part| !part.is_empty())
        .map(|part| part.to_ascii_lowercase())
        .collect()
}

fn capitalize(value: &str) -> String {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return String::new();
    };
    let mut result = first.to_ascii_uppercase().to_string();
    result.push_str(chars.as_str());
    result
}

fn schema_tokens(ty: &Type) -> Result<(proc_macro2::TokenStream, bool), Error> {
    if let Some(inner) = option_inner_type(ty) {
        let (schema, _) = schema_tokens(inner)?;
        return Ok((schema, false));
    }

    if let Some(inner) = vec_inner_type(ty) {
        let (item_schema, _) = schema_tokens(inner)?;
        return Ok((
            quote! {
                ::menta::ToolSchema::Array {
                    description: None,
                    items: Box::new(#item_schema),
                }
            },
            true,
        ));
    }

    if is_type(ty, "String") || is_type(ty, "str") {
        return Ok((
            quote! { ::menta::ToolSchema::String { description: None } },
            true,
        ));
    }

    if is_type(ty, "bool") {
        return Ok((
            quote! { ::menta::ToolSchema::Boolean { description: None } },
            true,
        ));
    }

    if is_integer(ty) {
        return Ok((
            quote! { ::menta::ToolSchema::Integer { description: None } },
            true,
        ));
    }

    if is_number(ty) {
        return Ok((
            quote! { ::menta::ToolSchema::Number { description: None } },
            true,
        ));
    }

    Err(Error::new_spanned(
        ty,
        "unsupported field type for Tool derive; supported: String, bool, integers, floats, Option<T>, Vec<T>",
    ))
}

fn option_inner_type(ty: &Type) -> Option<&Type> {
    generic_inner_type(ty, "Option")
}

fn vec_inner_type(ty: &Type) -> Option<&Type> {
    generic_inner_type(ty, "Vec")
}

fn generic_inner_type<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
    let Type::Path(type_path) = ty else {
        return None;
    };

    let segment = type_path.path.segments.last()?;
    if segment.ident != name {
        return None;
    }

    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
        return None;
    };

    let GenericArgument::Type(inner) = arguments.args.first()? else {
        return None;
    };

    Some(inner)
}

fn is_type(ty: &Type, name: &str) -> bool {
    let Type::Path(type_path) = ty else {
        return false;
    };

    type_path
        .path
        .segments
        .last()
        .map(|segment| segment.ident == name)
        .unwrap_or(false)
}

fn is_integer(ty: &Type) -> bool {
    [
        "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
    ]
    .iter()
    .any(|name| is_type(ty, name))
}

fn is_number(ty: &Type) -> bool {
    ["f32", "f64"].iter().any(|name| is_type(ty, name))
}

fn option_string_tokens(value: Option<&str>) -> proc_macro2::TokenStream {
    match value {
        Some(value) => quote! { Some(#value.to_string()) },
        None => quote! { None },
    }
}

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

    #[test]
    fn applies_camel_case_rename_rule() {
        assert_eq!(
            apply_rename_rule("file_path", Some(RenameRule::CamelCase)),
            "filePath"
        );
        assert_eq!(
            apply_rename_rule("task_id", Some(RenameRule::CamelCase)),
            "taskId"
        );
    }

    #[test]
    fn preserves_original_name_without_rule() {
        assert_eq!(apply_rename_rule("file_path", None), "file_path");
    }
}

fn default_tool_name(type_name: &str) -> String {
    let base = type_name.strip_suffix("Tool").unwrap_or(type_name);
    let mut output = String::new();

    for (index, ch) in base.chars().enumerate() {
        if ch.is_uppercase() {
            if index > 0 {
                output.push('_');
            }

            for lower in ch.to_lowercase() {
                output.push(lower);
            }
        } else {
            output.push(ch);
        }
    }

    output
}
