//! Proc-macro internals for `rok-validate`.
//!
//! Do not use this crate directly — import `rok_validate` instead.

use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, LitInt, LitStr, Type};

// ── field type classification ─────────────────────────────────────────────────

#[derive(Debug, Clone, Copy)]
enum FieldKind {
    Str,
    OptStr,
    Num,
    OptNum,
    Other,
}

fn classify(ty: &Type) -> FieldKind {
    let Type::Path(tp) = ty else {
        return FieldKind::Other;
    };
    let segs = &tp.path.segments;
    if segs.is_empty() {
        return FieldKind::Other;
    }
    let name = segs.last().unwrap().ident.to_string();
    match name.as_str() {
        "String" => FieldKind::Str,
        "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "u128" | "f32"
        | "f64" | "isize" | "usize" => FieldKind::Num,
        "Option" => {
            if let syn::PathArguments::AngleBracketed(ab) = &segs.last().unwrap().arguments {
                if let Some(syn::GenericArgument::Type(inner)) = ab.args.first() {
                    return match classify(inner) {
                        FieldKind::Str => FieldKind::OptStr,
                        FieldKind::Num => FieldKind::OptNum,
                        _ => FieldKind::Other,
                    };
                }
            }
            FieldKind::Other
        }
        _ => FieldKind::Other,
    }
}

fn is_vec_type(ty: &Type) -> bool {
    let Type::Path(tp) = ty else { return false };
    let segs = &tp.path.segments;
    segs.last().map(|s| s.ident == "Vec").unwrap_or(false)
}

// ── parsed rule set for one field ─────────────────────────────────────────────

#[derive(Default)]
struct FieldRules {
    required: bool,
    optional: bool,
    email: bool,
    url: bool,
    uuid: bool,
    ip: bool,
    ipv4: bool,
    ipv6: bool,
    slug: bool,
    alpha: bool,
    alphanumeric: bool,
    ascii: bool,
    lowercase: bool,
    uppercase: bool,
    hex_color: bool,
    numeric: bool,
    confirmed: bool,
    min: Option<i64>,
    max: Option<i64>,
    same: Option<String>,
    pattern: Option<String>,
    starts_with: Option<String>,
    ends_with: Option<String>,
    contains: Option<String>,
    r#in: Vec<String>,
    not_in: Vec<String>,
    nested: bool,
    min_items: Option<usize>,
    max_items: Option<usize>,
}

// ── #[derive(Validate)] ───────────────────────────────────────────────────────

/// Derive `rok_validate::Validate` for a struct, generating `validate()` from
/// `#[validate(...)]` field attributes.
///
/// # Rules
///
/// | Category | Rule | Description |
/// |---|---|---|
/// | Presence | `required` | Value must be present and non-empty |
/// | | `optional` | Skip remaining rules if absent |
/// | Format | `email` | Valid email address |
/// | | `url` | Valid URL (http/https) |
/// | | `uuid` | Valid UUID v4 |
/// | | `ip` | Valid IP address (v4 or v6) |
/// | | `ipv4` | Valid IPv4 address |
/// | | `ipv6` | Valid IPv6 address |
/// | | `slug` | URL-friendly slug |
/// | | `hex_color` | Hex color (#RGB, #RRGGBB) |
/// | Content | `alpha` | Only alphabetic characters |
/// | | `alphanumeric` | Only letters and digits |
/// | | `ascii` | Only ASCII characters |
/// | | `lowercase` | All lowercase |
/// | | `uppercase` | All uppercase |
/// | | `numeric` | String parses as a number |
/// | Length | `min = N` | String: ≥ N chars; Number: ≥ N |
/// | | `max = N` | String: ≤ N chars; Number: ≤ N |
/// | Pattern | `pattern = "..."` | Match regex pattern |
/// | | `regex = "..."` | Alias for pattern |
/// | Substring | `starts_with = "..."` | String starts with prefix |
/// | | `ends_with = "..."` | String ends with suffix |
/// | | `contains = "..."` | String contains substring |
/// | Cross-field | `same = "field"` | Must equal another field |
/// | | `confirmed` | Shorthand: must equal `{field}_confirmation` |
/// | Membership | `in = ["a", "b"]` | Must be one of the values |
/// | | `not_in = ["a", "b"]` | Must not be any of the values |
/// | Collections | `min_items = N` | Vec/Slice minimum length |
/// | | `max_items = N` | Vec/Slice maximum length |
/// | Nested | `nested` | Validate nested struct or Vec<T: Validate> |
#[proc_macro_derive(Validate, attributes(validate))]
pub fn derive_validate(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand_validate(input).unwrap_or_else(|e| e.to_compile_error().into())
}

fn expand_validate(input: DeriveInput) -> syn::Result<TokenStream> {
    let name = &input.ident;

    let Data::Struct(data) = &input.data else {
        return Err(syn::Error::new(
            Span::call_site(),
            "#[derive(Validate)] only supports structs",
        ));
    };
    let Fields::Named(fields) = &data.fields else {
        return Err(syn::Error::new(
            Span::call_site(),
            "#[derive(Validate)] only supports structs with named fields",
        ));
    };

    let mut field_checks: Vec<TokenStream2> = Vec::new();

    for field in &fields.named {
        let field_ident = field.ident.as_ref().unwrap();
        let field_name = field_ident.to_string();
        let kind = classify(&field.ty);

        let mut rules = FieldRules::default();

        for attr in &field.attrs {
            if !attr.path().is_ident("validate") {
                continue;
            }
            attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("required") {
                    rules.required = true;
                    Ok(())
                } else if meta.path.is_ident("optional") {
                    rules.optional = true;
                    Ok(())
                } else if meta.path.is_ident("email") {
                    rules.email = true;
                    Ok(())
                } else if meta.path.is_ident("url") {
                    rules.url = true;
                    Ok(())
                } else if meta.path.is_ident("uuid") {
                    rules.uuid = true;
                    Ok(())
                } else if meta.path.is_ident("ip") {
                    rules.ip = true;
                    Ok(())
                } else if meta.path.is_ident("ipv4") {
                    rules.ipv4 = true;
                    Ok(())
                } else if meta.path.is_ident("ipv6") {
                    rules.ipv6 = true;
                    Ok(())
                } else if meta.path.is_ident("slug") {
                    rules.slug = true;
                    Ok(())
                } else if meta.path.is_ident("alpha") {
                    rules.alpha = true;
                    Ok(())
                } else if meta.path.is_ident("alphanumeric") {
                    rules.alphanumeric = true;
                    Ok(())
                } else if meta.path.is_ident("ascii") {
                    rules.ascii = true;
                    Ok(())
                } else if meta.path.is_ident("lowercase") {
                    rules.lowercase = true;
                    Ok(())
                } else if meta.path.is_ident("uppercase") {
                    rules.uppercase = true;
                    Ok(())
                } else if meta.path.is_ident("hex_color") {
                    rules.hex_color = true;
                    Ok(())
                } else if meta.path.is_ident("numeric") {
                    rules.numeric = true;
                    Ok(())
                } else if meta.path.is_ident("confirmed") {
                    rules.confirmed = true;
                    Ok(())
                } else if meta.path.is_ident("min") {
                    let v = meta.value()?;
                    let n: LitInt = v.parse()?;
                    rules.min = Some(n.base10_parse()?);
                    Ok(())
                } else if meta.path.is_ident("max") {
                    let v = meta.value()?;
                    let n: LitInt = v.parse()?;
                    rules.max = Some(n.base10_parse()?);
                    Ok(())
                } else if meta.path.is_ident("same") {
                    let v = meta.value()?;
                    let s: LitStr = v.parse()?;
                    rules.same = Some(s.value());
                    Ok(())
                } else if meta.path.is_ident("pattern") || meta.path.is_ident("regex") {
                    let v = meta.value()?;
                    let s: LitStr = v.parse()?;
                    rules.pattern = Some(s.value());
                    Ok(())
                } else if meta.path.is_ident("starts_with") {
                    let v = meta.value()?;
                    let s: LitStr = v.parse()?;
                    rules.starts_with = Some(s.value());
                    Ok(())
                } else if meta.path.is_ident("ends_with") {
                    let v = meta.value()?;
                    let s: LitStr = v.parse()?;
                    rules.ends_with = Some(s.value());
                    Ok(())
                } else if meta.path.is_ident("contains") {
                    let v = meta.value()?;
                    let s: LitStr = v.parse()?;
                    rules.contains = Some(s.value());
                    Ok(())
                } else if meta.path.is_ident("in") {
                    let v = meta.value()?;
                    let content;
                    syn::bracketed!(content in v);
                    let vals: syn::punctuated::Punctuated<LitStr, syn::Token![,]> =
                        content.parse_terminated(|i| i.parse(), syn::Token![,])?;
                    for val in vals {
                        rules.r#in.push(val.value());
                    }
                    Ok(())
                } else if meta.path.is_ident("not_in") {
                    let v = meta.value()?;
                    let content;
                    syn::bracketed!(content in v);
                    let vals: syn::punctuated::Punctuated<LitStr, syn::Token![,]> =
                        content.parse_terminated(|i| i.parse(), syn::Token![,])?;
                    for val in vals {
                        rules.not_in.push(val.value());
                    }
                    Ok(())
                } else if meta.path.is_ident("nested") {
                    rules.nested = true;
                    Ok(())
                } else if meta.path.is_ident("min_items") {
                    let v = meta.value()?;
                    let n: LitInt = v.parse()?;
                    rules.min_items = Some(n.base10_parse::<usize>()?);
                    Ok(())
                } else if meta.path.is_ident("max_items") {
                    let v = meta.value()?;
                    let n: LitInt = v.parse()?;
                    rules.max_items = Some(n.base10_parse::<usize>()?);
                    Ok(())
                } else {
                    let name = meta
                        .path
                        .get_ident()
                        .map(|i| i.to_string())
                        .unwrap_or_default();
                    Err(meta.error(format!("unknown validate rule: `{name}`")))
                }
            })?;
        }

        let checks = build_checks(field_ident, &field_name, kind, &rules, &field.ty)?;
        field_checks.push(checks);
    }

    let expanded = quote! {
        impl ::rok_validate::Validate for #name {
            fn validate(&self) -> ::std::result::Result<(), ::rok_validate::ValidationErrors> {
                let mut __errors = ::rok_validate::ValidationErrors::new();
                #(#field_checks)*
                if __errors.is_empty() {
                    ::std::result::Result::Ok(())
                } else {
                    ::std::result::Result::Err(__errors)
                }
            }
        }
    };

    Ok(expanded.into())
}

// ── code generation per field ─────────────────────────────────────────────────

fn build_checks(
    ident: &Ident,
    name: &str,
    kind: FieldKind,
    rules: &FieldRules,
    ty: &Type,
) -> syn::Result<TokenStream2> {
    let mut stmts: Vec<TokenStream2> = Vec::new();

    // ── nested struct or Vec<T: Validate> ─────────────────────────────────────
    if rules.nested {
        if is_vec_type(ty) {
            stmts.push(quote! {
                for (__i, __item) in self.#ident.iter().enumerate() {
                    if let Err(__e) = ::rok_validate::Validate::validate(__item) {
                        __errors.merge(__e.prefix(&format!("{}[{}]", #name, __i)));
                    }
                }
            });
        } else {
            stmts.push(quote! {
                if let Err(__e) = ::rok_validate::Validate::validate(&self.#ident) {
                    __errors.merge(__e.prefix(#name));
                }
            });
        }
        if is_vec_type(ty) {
            if let Some(min) = rules.min_items {
                stmts.push(quote! {
                    if self.#ident.len() < #min {
                        __errors.add(#name, &format!("The {} field must contain at least {} item(s).", #name, #min));
                    }
                });
            }
            if let Some(max) = rules.max_items {
                stmts.push(quote! {
                    if self.#ident.len() > #max {
                        __errors.add(#name, &format!("The {} field may not contain more than {} item(s).", #name, #max));
                    }
                });
            }
        }
        return Ok(quote! { #(#stmts)* });
    }

    // ── Vec length checks ───────────────────────────────────────────────────
    if rules.min_items.is_some() || rules.max_items.is_some() || (rules.required && is_vec_type(ty))
    {
        if rules.required {
            stmts.push(quote! {
                if self.#ident.is_empty() {
                    __errors.add(#name, &format!("The {} field is required.", #name));
                }
            });
        }
        if let Some(min) = rules.min_items {
            stmts.push(quote! {
                if self.#ident.len() < #min {
                    __errors.add(#name, &format!("The {} field must contain at least {} item(s).", #name, #min));
                }
            });
        }
        if let Some(max) = rules.max_items {
            stmts.push(quote! {
                if self.#ident.len() > #max {
                    __errors.add(#name, &format!("The {} field may not contain more than {} item(s).", #name, #max));
                }
            });
        }
        return Ok(quote! { #(#stmts)* });
    }

    // ── shared string rules ─────────────────────────────────────────────────
    let str_rules = build_str_rules(ident, name, rules);
    let has_str_rules = !str_rules.is_empty();

    // ── per-kind checks ─────────────────────────────────────────────────────
    match kind {
        FieldKind::Str => {
            if rules.required {
                stmts.push(quote! {
                    ::rok_validate::rules::check_required_str(self.#ident.as_str(), #name, &mut __errors);
                });
            }
            if has_str_rules {
                stmts.extend(str_rules);
            }
            if let Some(min) = rules.min {
                let min = min as usize;
                stmts.push(quote! {
                    ::rok_validate::rules::check_min_len(self.#ident.as_str(), #min, #name, &mut __errors);
                });
            }
            if let Some(max) = rules.max {
                let max = max as usize;
                stmts.push(quote! {
                    ::rok_validate::rules::check_max_len(self.#ident.as_str(), #max, #name, &mut __errors);
                });
            }
            if let Some(ref other) = rules.same {
                let other_ident = Ident::new(other, Span::call_site());
                stmts.push(quote! {
                    ::rok_validate::rules::check_same(self.#ident.as_str(), self.#other_ident.as_str(), #name, &mut __errors);
                });
            }
            handle_confirmed(ident, name, rules, &mut stmts);
            Ok(quote! { #(#stmts)* })
        }

        FieldKind::OptStr => {
            if rules.required {
                stmts.push(quote! {
                    ::rok_validate::rules::check_required_opt(self.#ident.as_deref(), #name, &mut __errors);
                });
            }
            let mut inner: Vec<TokenStream2> = Vec::new();
            if rules.email {
                inner.push(
                    quote! { ::rok_validate::rules::check_email(__val, #name, &mut __errors); },
                );
            }
            if rules.url {
                inner.push(
                    quote! { ::rok_validate::rules::check_url(__val, #name, &mut __errors); },
                );
            }
            if rules.uuid {
                inner.push(
                    quote! { ::rok_validate::rules::check_uuid(__val, #name, &mut __errors); },
                );
            }
            if rules.ip {
                inner
                    .push(quote! { ::rok_validate::rules::check_ip(__val, #name, &mut __errors); });
            }
            if rules.ipv4 {
                inner.push(
                    quote! { ::rok_validate::rules::check_ipv4(__val, #name, &mut __errors); },
                );
            }
            if rules.ipv6 {
                inner.push(
                    quote! { ::rok_validate::rules::check_ipv6(__val, #name, &mut __errors); },
                );
            }
            if rules.slug {
                inner.push(
                    quote! { ::rok_validate::rules::check_slug(__val, #name, &mut __errors); },
                );
            }
            if rules.alpha {
                inner.push(
                    quote! { ::rok_validate::rules::check_alpha(__val, #name, &mut __errors); },
                );
            }
            if rules.alphanumeric {
                inner.push(quote! { ::rok_validate::rules::check_alphanumeric(__val, #name, &mut __errors); });
            }
            if rules.ascii {
                inner.push(
                    quote! { ::rok_validate::rules::check_ascii(__val, #name, &mut __errors); },
                );
            }
            if rules.lowercase {
                inner.push(
                    quote! { ::rok_validate::rules::check_lowercase(__val, #name, &mut __errors); },
                );
            }
            if rules.uppercase {
                inner.push(
                    quote! { ::rok_validate::rules::check_uppercase(__val, #name, &mut __errors); },
                );
            }
            if rules.hex_color {
                inner.push(
                    quote! { ::rok_validate::rules::check_hex_color(__val, #name, &mut __errors); },
                );
            }
            if rules.numeric {
                inner.push(quote! { ::rok_validate::rules::check_numeric_str(__val, #name, &mut __errors); });
            }
            if let Some(ref pat) = rules.pattern {
                let pl = proc_macro2::Literal::string(pat);
                inner.push(quote! { ::rok_validate::rules::check_pattern(__val, #pl, #name, &mut __errors); });
            }
            if let Some(ref pre) = rules.starts_with {
                let pl = proc_macro2::Literal::string(pre);
                inner.push(quote! { ::rok_validate::rules::check_starts_with(__val, #pl, #name, &mut __errors); });
            }
            if let Some(ref suf) = rules.ends_with {
                let sl = proc_macro2::Literal::string(suf);
                inner.push(quote! { ::rok_validate::rules::check_ends_with(__val, #sl, #name, &mut __errors); });
            }
            if let Some(ref sub) = rules.contains {
                let sl = proc_macro2::Literal::string(sub);
                inner.push(quote! { ::rok_validate::rules::check_contains(__val, #sl, #name, &mut __errors); });
            }
            if !rules.r#in.is_empty() {
                let list: Vec<_> = rules
                    .r#in
                    .iter()
                    .map(|s| proc_macro2::Literal::string(s))
                    .collect();
                inner.push(quote! {
                    ::rok_validate::rules::check_in_list(__val, &[#(#list),*], #name, &mut __errors);
                });
            }
            if !rules.not_in.is_empty() {
                let list: Vec<_> = rules
                    .not_in
                    .iter()
                    .map(|s| proc_macro2::Literal::string(s))
                    .collect();
                inner.push(quote! {
                    ::rok_validate::rules::check_not_in_list(__val, &[#(#list),*], #name, &mut __errors);
                });
            }
            if let Some(min) = rules.min {
                let min = min as usize;
                inner.push(quote! { ::rok_validate::rules::check_min_len(__val, #min, #name, &mut __errors); });
            }
            if let Some(max) = rules.max {
                let max = max as usize;
                inner.push(quote! { ::rok_validate::rules::check_max_len(__val, #max, #name, &mut __errors); });
            }
            if let Some(ref other) = rules.same {
                let other_ident = Ident::new(other, Span::call_site());
                inner.push(quote! {
                    ::rok_validate::rules::check_same(__val, self.#other_ident.as_str(), #name, &mut __errors);
                });
            }
            handle_confirmed_opt(ident, name, rules, &mut inner);
            if !inner.is_empty() {
                stmts.push(quote! {
                    if let Some(ref __val) = self.#ident {
                        let __val: &str = __val.as_str();
                        #(#inner)*
                    }
                });
            }
            Ok(quote! { #(#stmts)* })
        }

        FieldKind::Num => {
            if let Some(min) = rules.min {
                let min = min as f64;
                stmts.push(quote! {
                    ::rok_validate::rules::check_min_num(self.#ident as f64, #min, #name, &mut __errors);
                });
            }
            if let Some(max) = rules.max {
                let max = max as f64;
                stmts.push(quote! {
                    ::rok_validate::rules::check_max_num(self.#ident as f64, #max, #name, &mut __errors);
                });
            }
            Ok(quote! { #(#stmts)* })
        }

        FieldKind::OptNum => {
            let mut inner: Vec<TokenStream2> = Vec::new();
            if rules.required {
                stmts.push(quote! {
                    if self.#ident.is_none() {
                        __errors.add(#name, "This field is required.");
                    }
                });
            }
            if let Some(min) = rules.min {
                let min = min as f64;
                inner.push(quote! { ::rok_validate::rules::check_min_num(__v as f64, #min, #name, &mut __errors); });
            }
            if let Some(max) = rules.max {
                let max = max as f64;
                inner.push(quote! { ::rok_validate::rules::check_max_num(__v as f64, #max, #name, &mut __errors); });
            }
            if !inner.is_empty() {
                stmts.push(quote! {
                    if let Some(__v) = self.#ident { #(#inner)* }
                });
            }
            Ok(quote! { #(#stmts)* })
        }

        FieldKind::Other => Ok(quote! {}),
    }
}

fn build_str_rules(ident: &Ident, name: &str, rules: &FieldRules) -> Vec<TokenStream2> {
    let mut stmts: Vec<TokenStream2> = Vec::new();

    if rules.email {
        stmts.push(quote! {
            ::rok_validate::rules::check_email(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.url {
        stmts.push(quote! {
            ::rok_validate::rules::check_url(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.uuid {
        stmts.push(quote! {
            ::rok_validate::rules::check_uuid(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.ip {
        stmts.push(quote! {
            ::rok_validate::rules::check_ip(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.ipv4 {
        stmts.push(quote! {
            ::rok_validate::rules::check_ipv4(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.ipv6 {
        stmts.push(quote! {
            ::rok_validate::rules::check_ipv6(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.slug {
        stmts.push(quote! {
            ::rok_validate::rules::check_slug(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.alpha {
        stmts.push(quote! {
            ::rok_validate::rules::check_alpha(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.alphanumeric {
        stmts.push(quote! {
            ::rok_validate::rules::check_alphanumeric(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.ascii {
        stmts.push(quote! {
            ::rok_validate::rules::check_ascii(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.lowercase {
        stmts.push(quote! {
            ::rok_validate::rules::check_lowercase(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.uppercase {
        stmts.push(quote! {
            ::rok_validate::rules::check_uppercase(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.hex_color {
        stmts.push(quote! {
            ::rok_validate::rules::check_hex_color(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if rules.numeric {
        stmts.push(quote! {
            ::rok_validate::rules::check_numeric_str(self.#ident.as_str(), #name, &mut __errors);
        });
    }
    if let Some(ref pat) = rules.pattern {
        let pl = proc_macro2::Literal::string(pat);
        stmts.push(quote! {
            ::rok_validate::rules::check_pattern(self.#ident.as_str(), #pl, #name, &mut __errors);
        });
    }
    if let Some(ref pre) = rules.starts_with {
        let pl = proc_macro2::Literal::string(pre);
        stmts.push(quote! {
            ::rok_validate::rules::check_starts_with(self.#ident.as_str(), #pl, #name, &mut __errors);
        });
    }
    if let Some(ref suf) = rules.ends_with {
        let sl = proc_macro2::Literal::string(suf);
        stmts.push(quote! {
            ::rok_validate::rules::check_ends_with(self.#ident.as_str(), #sl, #name, &mut __errors);
        });
    }
    if let Some(ref sub) = rules.contains {
        let sl = proc_macro2::Literal::string(sub);
        stmts.push(quote! {
            ::rok_validate::rules::check_contains(self.#ident.as_str(), #sl, #name, &mut __errors);
        });
    }
    if !rules.r#in.is_empty() {
        let list: Vec<_> = rules
            .r#in
            .iter()
            .map(|s| proc_macro2::Literal::string(s))
            .collect();
        stmts.push(quote! {
            ::rok_validate::rules::check_in_list(self.#ident.as_str(), &[#(#list),*], #name, &mut __errors);
        });
    }
    if !rules.not_in.is_empty() {
        let list: Vec<_> = rules
            .not_in
            .iter()
            .map(|s| proc_macro2::Literal::string(s))
            .collect();
        stmts.push(quote! {
            ::rok_validate::rules::check_not_in_list(self.#ident.as_str(), &[#(#list),*], #name, &mut __errors);
        });
    }

    stmts
}

fn handle_confirmed(ident: &Ident, name: &str, rules: &FieldRules, stmts: &mut Vec<TokenStream2>) {
    if rules.confirmed {
        let confirm_ident = Ident::new(&format!("{name}_confirmation"), Span::call_site());
        stmts.push(quote! {
            ::rok_validate::rules::check_confirmed(
                self.#ident.as_str(),
                self.#confirm_ident.as_str(),
                #name, &mut __errors,
            );
        });
    }
}

fn handle_confirmed_opt(
    _ident: &Ident,
    name: &str,
    rules: &FieldRules,
    inner: &mut Vec<TokenStream2>,
) {
    if rules.confirmed {
        let confirm_ident = Ident::new(&format!("{name}_confirmation"), Span::call_site());
        inner.push(quote! {
            if let Some(ref __confirm) = self.#confirm_ident {
                ::rok_validate::rules::check_confirmed(
                    __val,
                    __confirm.as_str(),
                    #name, &mut __errors,
                );
            }
        });
    }
}

// ── Filterable ────────────────────────────────────────────────────────────────

struct FilterField {
    skip: bool,
    like: bool,
    gte: bool,
    lte: bool,
    column: Option<String>,
}

fn parse_filter_attr(attrs: &[syn::Attribute]) -> syn::Result<FilterField> {
    let mut f = FilterField {
        skip: false,
        like: false,
        gte: false,
        lte: false,
        column: None,
    };
    for attr in attrs {
        if !attr.path().is_ident("filter") {
            continue;
        }
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("skip") {
                f.skip = true;
                Ok(())
            } else if meta.path.is_ident("like") {
                f.like = true;
                Ok(())
            } else if meta.path.is_ident("gte") {
                f.gte = true;
                Ok(())
            } else if meta.path.is_ident("lte") {
                f.lte = true;
                Ok(())
            } else if meta.path.is_ident("column") {
                let v = meta.value()?;
                let s: LitStr = v.parse()?;
                f.column = Some(s.value());
                Ok(())
            } else {
                Err(meta.error("unknown filter option"))
            }
        })?;
    }
    Ok(f)
}

/// Derive `Filterable` for a struct, generating `apply_filters()` that
/// adds WHERE clauses to a `QueryBuilder` from `Option` fields.
///
/// # Attributes
///
/// | Attribute | Behaviour |
/// |---|---|
/// | `#[filter(skip)]` | Field is ignored |
/// | `#[filter(like)]` | Uses `LIKE` instead of equality |
/// | `#[filter(gte, column = "c")]` | Uses `>=` (optionally on a custom column) |
/// | `#[filter(lte, column = "c")]` | Uses `<=` (optionally on a custom column) |
#[proc_macro_derive(Filterable, attributes(filter))]
pub fn derive_filterable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand_filterable(input).unwrap_or_else(|e| e.to_compile_error().into())
}

fn expand_filterable(input: DeriveInput) -> syn::Result<TokenStream> {
    let name = &input.ident;

    let Data::Struct(data) = &input.data else {
        return Err(syn::Error::new(
            Span::call_site(),
            "#[derive(Filterable)] only supports structs",
        ));
    };
    let Fields::Named(fields) = &data.fields else {
        return Err(syn::Error::new(
            Span::call_site(),
            "#[derive(Filterable)] only supports structs with named fields",
        ));
    };

    let mut filter_stmts: Vec<TokenStream2> = Vec::new();

    for field in &fields.named {
        let field_ident = field.ident.as_ref().unwrap();
        let field_name = field_ident.to_string();
        let fa = parse_filter_attr(&field.attrs)?;

        if fa.skip {
            continue;
        }

        let col = fa.column.as_deref().unwrap_or(&field_name);

        if fa.like {
            filter_stmts.push(quote! {
                if let Some(ref __v) = self.#field_ident {
                    q = q.where_like(#col, &::std::format!("%{}%", __v));
                }
            });
        } else if fa.gte {
            filter_stmts.push(quote! {
                if let Some(__v) = self.#field_ident {
                    q = q.where_gte(#col, __v);
                }
            });
        } else if fa.lte {
            filter_stmts.push(quote! {
                if let Some(__v) = self.#field_ident {
                    q = q.where_lte(#col, __v);
                }
            });
        } else {
            filter_stmts.push(quote! {
                if let Some(__v) = self.#field_ident {
                    q = q.where_eq(#col, __v);
                }
            });
        }
    }

    let expanded = quote! {
        impl #name {
            pub fn apply_filters<T>(self, q: ::rok_orm_core::QueryBuilder<T>) -> ::rok_orm_core::QueryBuilder<T> {
                let mut q = q;
                #(#filter_stmts)*
                q
            }
        }
    };

    Ok(expanded.into())
}

// ── Sortable ──────────────────────────────────────────────────────────────────

struct SortableAttr {
    allow: Vec<String>,
}

fn parse_sortable_attr(attrs: &[syn::Attribute]) -> syn::Result<SortableAttr> {
    let mut s = SortableAttr { allow: Vec::new() };
    for attr in attrs {
        if !attr.path().is_ident("sortable") {
            continue;
        }
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("allow") {
                let v = meta.value()?;
                let content;
                syn::bracketed!(content in v);
                let vals: syn::punctuated::Punctuated<LitStr, syn::Token![,]> =
                    content.parse_terminated(|i| i.parse(), syn::Token![,])?;
                for val in vals {
                    s.allow.push(val.value());
                }
                Ok(())
            } else {
                Err(meta.error("unknown sortable option"))
            }
        })?;
    }
    Ok(s)
}

/// Derive `Sortable` for a struct, generating `apply_sort()` that adds
/// `ORDER BY` clauses to a `QueryBuilder` from a `sort: Option<String>` field.
///
/// # Attributes
///
/// **Struct-level:**
/// ```rust,ignore
/// #[sortable(allow = ["col_a", "col_b"])]
/// ```
#[proc_macro_derive(Sortable, attributes(sortable))]
pub fn derive_sortable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand_sortable(input).unwrap_or_else(|e| e.to_compile_error().into())
}

fn expand_sortable(input: DeriveInput) -> syn::Result<TokenStream> {
    let name = &input.ident;
    let sa = parse_sortable_attr(&input.attrs)?;

    let allow_lits: Vec<_> = sa
        .allow
        .iter()
        .map(|s| proc_macro2::Literal::string(s))
        .collect();

    let expanded = quote! {
        impl #name {
            pub fn apply_sort<T>(self, q: ::rok_orm_core::QueryBuilder<T>) -> ::rok_orm_core::QueryBuilder<T> {
                if let Some(s) = self.sort {
                    let sp = ::rok_validate::SortParam::parse(&s, &[#(#allow_lits),*]);
                    let mut q = q;
                    for (col, asc) in sp.0 {
                        q = if asc { q.order_by(&col) } else { q.order_by_desc(&col) };
                    }
                    q
                } else {
                    q
                }
            }
        }
    };

    Ok(expanded.into())
}

// ── FormRequest ───────────────────────────────────────────────────────────────

/// Derive [`rok_validate::FormRequest`] for a struct.
///
/// Generates a blank `impl FormRequest for T {}` using the default `authorize()`
/// (always `true`).  Override `authorize()` manually when authorization logic is needed.
///
/// # Example
///
/// ```rust,ignore
/// #[derive(Deserialize, Validate, FormRequest)]
/// pub struct CreatePostRequest {
///     #[validate(required, min = 3, max = 255)]
///     pub title: String,
/// }
///
/// // In handler:
/// pub async fn store(ValidatedForm(body): ValidatedForm<CreatePostRequest>) -> impl IntoResponse {
///     // body is validated and authorized
/// }
/// ```
#[proc_macro_derive(FormRequest)]
pub fn derive_form_request(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    quote! {
        impl #impl_generics ::rok_validate::FormRequest for #name #ty_generics #where_clause {}
    }
    .into()
}
