//! Code generation for `from_slice_with_peer_ctx` overrides (`#[rapira(peer)]`).
//!
//! Consumption contract: unlike the default trait method (which validates `shape_compat`
//! up front and never consumes input on a schema error), these generated overrides read
//! peer fields in order. If a later field hits `ShapeRel::Mismatch`, earlier fields have
//! already advanced the input slice — i.e. on `Err(SchemaMismatch)` the cursor position is
//! unspecified. Callers must not reuse the slice after an error; rewind from a saved copy.

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Fields, Ident, Type};

fn gen_shape_compat_read(ty: &Type, schema_path: &TokenStream) -> TokenStream {
    quote! {
        match ::rapira::schema::shape_compat(&<#ty as #schema_path::GetType>::TYPE, __pty) {
            ::rapira::schema::ShapeRel::Exact => {
                <#ty as ::rapira::Rapira>::from_slice_ctx(__s, __f)?
            }
            ::rapira::schema::ShapeRel::Compatible => {
                <#ty as ::rapira::Rapira>::from_slice_with_peer_ctx(__s, __pty, __f)?
            }
            ::rapira::schema::ShapeRel::Mismatch => {
                return Err(::rapira::RapiraError::SchemaMismatch);
            }
        }
    }
}

pub struct NamedWireField<'a> {
    pub ident: &'a Ident,
    pub ty: &'a Type,
    pub name: String,
}

pub struct NamedInitField<'a> {
    pub ident: &'a Ident,
    pub skipped: bool,
}

pub fn gen_named_peer(
    name: &Ident,
    string_name: &str,
    schema_path: &TokenStream,
    wire_fields: &[NamedWireField<'_>],
    init_fields: &[NamedInitField<'_>],
) -> TokenStream {
    let mut accumulators = Vec::new();
    let mut match_arms = Vec::new();
    let mut struct_fields = Vec::new();

    for wf in wire_fields {
        let acc = format_ident!("__acc_{}", wf.ident);
        let ty = wf.ty;
        let field_name = &wf.name;
        let read = gen_shape_compat_read(ty, schema_path);
        accumulators.push(quote! {
            let mut #acc: Option<#ty> = None;
        });
        match_arms.push(quote! {
            #field_name => {
                #acc = Some(#read);
            }
        });
    }

    for f in init_fields {
        let ident = f.ident;
        if f.skipped {
            struct_fields.push(quote! {
                #ident: Default::default(),
            });
        } else {
            let acc = format_ident!("__acc_{}", ident);
            struct_fields.push(quote! {
                #ident: #acc.unwrap_or_default(),
            });
        }
    }

    quote! {
        fn from_slice_with_peer_ctx(
            __s: &mut &[u8],
            __peer: &#schema_path::SchemaTyp,
            __f: ::rapira::RapiraFlags,
        ) -> ::rapira::Result<Self>
        where
            Self: Sized + #schema_path::GetType,
        {
            use #schema_path::{SchemaFields, SchemaTyp};
            let SchemaTyp::Struct { name: __pn, fields: SchemaFields::Named(__pf) } = __peer else {
                return Err(::rapira::RapiraError::SchemaMismatch);
            };
            if __pn != #string_name {
                return Err(::rapira::RapiraError::SchemaMismatch);
            }

            #(#accumulators)*

            for (__pname, __pty) in __pf.iter() {
                match __pname.as_str() {
                    #(#match_arms)*
                    _ => ::rapira::schema::skip_value(__s, __pty)?,
                }
            }

            Ok(#name {
                #(#struct_fields)*
            })
        }
    }
}

pub struct UnnamedField<'a> {
    pub ty: &'a Type,
}

pub fn gen_unnamed_peer(
    name: &Ident,
    string_name: &str,
    schema_path: &TokenStream,
    fields: &[UnnamedField<'_>],
) -> TokenStream {
    let local_len = fields.len();
    let mut accumulators = Vec::new();
    let mut position_arms = Vec::new();
    let mut tuple_fields = Vec::new();

    for (i, f) in fields.iter().enumerate() {
        let acc = format_ident!("__acc_{}", i);
        let ty = f.ty;
        let read = gen_shape_compat_read(ty, schema_path);
        accumulators.push(quote! {
            let mut #acc: Option<#ty> = None;
        });
        position_arms.push(quote! {
            #i => {
                #acc = Some(#read);
            }
        });
        tuple_fields.push(quote! {
            #acc.unwrap_or_default()
        });
    }

    quote! {
        fn from_slice_with_peer_ctx(
            __s: &mut &[u8],
            __peer: &#schema_path::SchemaTyp,
            __f: ::rapira::RapiraFlags,
        ) -> ::rapira::Result<Self>
        where
            Self: Sized + #schema_path::GetType,
        {
            use #schema_path::{SchemaFields, SchemaTyp};
            let SchemaTyp::Struct { name: __pn, fields: SchemaFields::Unnamed(__pf) } = __peer else {
                return Err(::rapira::RapiraError::SchemaMismatch);
            };
            if __pn != #string_name {
                return Err(::rapira::RapiraError::SchemaMismatch);
            }

            #(#accumulators)*

            let __n = usize::min(#local_len, __pf.len());
            for __pi in 0..__n {
                let __pty = &__pf[__pi];
                match __pi {
                    #(#position_arms)*
                    _ => return Err(::rapira::RapiraError::SchemaMismatch),
                }
            }
            for __pty in &__pf[__n..] {
                ::rapira::schema::skip_value(__s, __pty)?;
            }

            Ok(#name(
                #(#tuple_fields),*
            ))
        }
    }
}

pub struct EnumVariantPeer<'a> {
    pub ident: &'a Ident,
    pub name: String,
    pub fields: &'a Fields,
}

pub fn gen_enum_peer(
    name: &Ident,
    string_name: &str,
    schema_path: &TokenStream,
    variants: &[EnumVariantPeer<'_>],
) -> TokenStream {
    let mut variant_arms = Vec::new();

    for v in variants {
        let vname = &v.name;
        let payload = gen_enum_variant_payload(name, v.ident, v.fields, schema_path);
        variant_arms.push(quote! {
            #vname => {
                return #payload;
            }
        });
    }

    quote! {
        fn from_slice_with_peer_ctx(
            __s: &mut &[u8],
            __peer: &#schema_path::SchemaTyp,
            __f: ::rapira::RapiraFlags,
        ) -> ::rapira::Result<Self>
        where
            Self: Sized + #schema_path::GetType,
        {
            use #schema_path::SchemaTyp;
            let SchemaTyp::Enum { name: __pn, variants: __pv } = __peer else {
                return Err(::rapira::RapiraError::SchemaMismatch);
            };
            if __pn != #string_name {
                return Err(::rapira::RapiraError::SchemaMismatch);
            }
            let __tag = ::rapira::byte_rapira::from_slice(__s)?;
            let Some((_, __vname, __pty)) = __pv.iter().find(|(t, _, _)| *t == __tag) else {
                return Err(::rapira::RapiraError::UnknownVariant);
            };
            match __vname.as_str() {
                #(#variant_arms)*
                _ => Err(::rapira::RapiraError::SchemaMismatch),
            }
        }
    }
}

fn gen_enum_variant_payload(
    enum_name: &Ident,
    variant_ident: &Ident,
    fields: &Fields,
    schema_path: &TokenStream,
) -> TokenStream {
    match fields {
        Fields::Unit => quote! {
            {
                match ::rapira::schema::shape_compat(
                    &#schema_path::Typ::Scalar(#schema_path::ScalarTyp::Void),
                    __pty,
                ) {
                    ::rapira::schema::ShapeRel::Exact => Ok(#enum_name::#variant_ident),
                    ::rapira::schema::ShapeRel::Compatible => {
                        ::rapira::schema::skip_value(__s, __pty)?;
                        Ok(#enum_name::#variant_ident)
                    }
                    ::rapira::schema::ShapeRel::Mismatch => {
                        Err(::rapira::RapiraError::SchemaMismatch)
                    }
                }
            }
        },
        Fields::Unnamed(f) if f.unnamed.len() == 1 => {
            let ty = &f.unnamed[0].ty;
            let read = gen_shape_compat_read(ty, schema_path);
            quote! {
                Ok(#enum_name::#variant_ident(#read))
            }
        }
        Fields::Unnamed(f) => {
            let local_len = f.unnamed.len();
            let mut accumulators = Vec::new();
            let mut position_arms = Vec::new();
            let mut args = Vec::new();
            for (i, field) in f.unnamed.iter().enumerate() {
                let acc = format_ident!("__varg_{}", i);
                let ty = &field.ty;
                let read = gen_shape_compat_read(ty, schema_path);
                accumulators.push(quote! {
                    let mut #acc: Option<#ty> = None;
                });
                position_arms.push(quote! {
                    #i => {
                        #acc = Some(#read);
                    }
                });
                args.push(quote! { #acc.unwrap_or_default() });
            }
            quote! {
                {
                    use #schema_path::{SchemaFields, SchemaTyp};
                    let SchemaTyp::Struct { fields: SchemaFields::Unnamed(__vf), .. } = __pty else {
                        return Err(::rapira::RapiraError::SchemaMismatch);
                    };
                    #(#accumulators)*
                    let __n = usize::min(#local_len, __vf.len());
                    for __pi in 0..__n {
                        let __pty = &__vf[__pi];
                        match __pi {
                            #(#position_arms)*
                            _ => return Err(::rapira::RapiraError::SchemaMismatch),
                        }
                    }
                    for __pty in &__vf[__n..] {
                        ::rapira::schema::skip_value(__s, __pty)?;
                    }
                    Ok(#enum_name::#variant_ident(#(#args),*))
                }
            }
        }
        Fields::Named(f) => {
            let mut accumulators = Vec::new();
            let mut match_arms = Vec::new();
            let mut inits = Vec::new();
            for field in &f.named {
                let fname = field.ident.as_ref().unwrap();
                let fname_str = fname.to_string();
                let acc = format_ident!("__vacc_{}", fname);
                let ty = &field.ty;
                let read = gen_shape_compat_read(ty, schema_path);
                accumulators.push(quote! {
                    let mut #acc: Option<#ty> = None;
                });
                match_arms.push(quote! {
                    #fname_str => {
                        #acc = Some(#read);
                    }
                });
                inits.push(quote! {
                    #fname: #acc.unwrap_or_default(),
                });
            }
            quote! {
                {
                    use #schema_path::{SchemaFields, SchemaTyp};
                    let SchemaTyp::Struct { fields: SchemaFields::Named(__vf), .. } = __pty else {
                        return Err(::rapira::RapiraError::SchemaMismatch);
                    };
                    #(#accumulators)*
                    for (__pname, __pty) in __vf.iter() {
                        match __pname.as_str() {
                            #(#match_arms)*
                            _ => ::rapira::schema::skip_value(__s, __pty)?,
                        }
                    }
                    Ok(#enum_name::#variant_ident {
                        #(#inits)*
                    })
                }
            }
        }
    }
}
