//! `#[derive(WireFrameHash)]` — compile-time stable schema hash.
//!
//! Walks the type's AST in declaration order and produces a `blake3` hash
//! truncated to `u64` from a canonical string representation. The hash is
//! computed in the proc-macro at compile time and emitted as a literal `u64`,
//! so consumers do not pull in a runtime `blake3` dependency.
//!
//! Canonical string format:
//! - Enums:   `enum: {variant1} | {variant2} | ...`
//!   where each `variant` is one of:
//!     - `name`                                — unit variant
//!     - `name(field0:Type0, field1:Type1)`    — tuple variant
//!     - `name{field0:Type0, field1:Type1}`    — struct variant
//! - Structs: `struct: {field0:Type0}, {field1:Type1}, ...`
//!   For tuple structs the "name" is the field index (`0`, `1`, ...).
//!   For unit structs the body is empty.
//!
//! Field types are rendered via `quote::ToTokens::to_token_stream().to_string()`,
//! which is stable for a given source text. The Rust type identifier itself
//! (the outer `Frame` ident), doc comments, derives, visibility modifiers, and
//! whitespace are NOT included in the hash input — only structural information
//! that affects the wire format. Two distinct Rust types with identical shapes
//! therefore hash identically (they encode/decode identically over postcard).
//!
//! Variant and field order ARE significant: postcard uses positional encoding,
//! so reordering changes the wire format and must change the hash.

use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{Data, DeriveInput, Fields};

/// Build the canonical string for a parsed `DeriveInput`.
fn canonical_string(input: &DeriveInput) -> String {
    match &input.data {
        Data::Enum(e) => {
            let mut out = String::from("enum: ");
            let mut first = true;
            for variant in &e.variants {
                if !first {
                    out.push_str(" | ");
                }
                first = false;
                out.push_str(&variant.ident.to_string());
                match &variant.fields {
                    Fields::Unit => {}
                    Fields::Named(named) => {
                        out.push('{');
                        write_named_fields(&mut out, named.named.iter());
                        out.push('}');
                    }
                    Fields::Unnamed(unnamed) => {
                        out.push('(');
                        write_unnamed_fields(&mut out, unnamed.unnamed.iter());
                        out.push(')');
                    }
                }
            }
            out
        }
        Data::Struct(s) => {
            let mut out = String::from("struct: ");
            match &s.fields {
                Fields::Unit => {}
                Fields::Named(named) => {
                    write_named_fields(&mut out, named.named.iter());
                }
                Fields::Unnamed(unnamed) => {
                    write_unnamed_fields(&mut out, unnamed.unnamed.iter());
                }
            }
            out
        }
        Data::Union(_) => {
            // Unions aren't serde-friendly; return a sentinel so the resulting
            // hash is at least deterministic if someone tries.
            String::from("union")
        }
    }
}

fn write_named_fields<'a, I: Iterator<Item = &'a syn::Field>>(out: &mut String, fields: I) {
    let mut first = true;
    for field in fields {
        if !first {
            out.push_str(", ");
        }
        first = false;
        let name = field
            .ident
            .as_ref()
            .map(|i| i.to_string())
            .unwrap_or_default();
        let ty = field.ty.to_token_stream().to_string();
        out.push_str(&name);
        out.push(':');
        out.push_str(&ty);
    }
}

fn write_unnamed_fields<'a, I: Iterator<Item = &'a syn::Field>>(out: &mut String, fields: I) {
    let mut first = true;
    for (idx, field) in fields.enumerate() {
        if !first {
            out.push_str(", ");
        }
        first = false;
        let ty = field.ty.to_token_stream().to_string();
        out.push_str(&idx.to_string());
        out.push(':');
        out.push_str(&ty);
    }
}

/// Compute the truncated u64 schema hash from a canonical string.
fn schema_hash_u64(canonical: &str) -> u64 {
    let digest = blake3::hash(canonical.as_bytes());
    let bytes = digest.as_bytes();
    // Take the first 8 bytes, little-endian.
    let mut head = [0u8; 8];
    head.copy_from_slice(&bytes[..8]);
    u64::from_le_bytes(head)
}

/// Entry point for the proc-macro derive. The `lib.rs` `#[proc_macro_derive]`
/// shim parses the `DeriveInput` and calls this.
pub(crate) fn derive_impl(input: DeriveInput) -> TokenStream2 {
    let canonical = canonical_string(&input);
    let hash = schema_hash_u64(&canonical);
    let ident = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    // Producer-side registration into the linkme distributed slice
    // `REGISTERED_WIRE_FRAMES`. The walker filters by crate name
    // only; `plugin_struct` was dropped from `WireFrameRegistration`
    // (P5, 2026-05-21). The 8-byte u64 hash is left-padded into the
    // 32-byte `Blake3` signature slot.
    let frame_name_str = ident.to_string();
    let reg_ident = quote::format_ident!(
        "__MAAT_WIRE_FRAME_REG_{}",
        ident.to_string().to_uppercase()
    );
    let hash_bytes = hash.to_le_bytes();
    let sig_lits = (0..32usize).map(|i| {
        let b = if i < 8 { hash_bytes[i] } else { 0u8 };
        quote!(#b)
    });

    // linkme can't link on wasm32 (no supported section attribute);
    // gate the registration so the WASM client still builds. Native
    // targets pick it up; the registry has the entry there.
    let registration = quote! {
        #[cfg(not(target_arch = "wasm32"))]
        const _: () = {
            #[::maat::__sdk::linkme::distributed_slice(::maat::__sdk::REGISTERED_WIRE_FRAMES)]
            #[linkme(crate = ::maat::__sdk::linkme)]
            #[allow(non_upper_case_globals)]
            static #reg_ident: ::maat::__sdk::WireFrameRegistration =
                ::maat::__sdk::WireFrameRegistration {
                    plugin_crate_name: env!("CARGO_PKG_NAME"),
                    frame_name:        #frame_name_str,
                    signature:         [#(#sig_lits),*],
                };
        };
    };

    quote! {
        #[automatically_derived]
        impl #impl_generics ::maat_sdk::WireFrameHash for #ident #ty_generics #where_clause {
            const SCHEMA_HASH: u64 = #hash;
        }
        #registration
    }
}

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

    #[test]
    fn enum_canonical_includes_variant_and_field_names() {
        let input: DeriveInput = parse_quote! {
            enum Frame {
                A,
                B { x: u32 },
            }
        };
        let s = canonical_string(&input);
        assert!(s.contains("enum"));
        assert!(s.contains("A"));
        assert!(s.contains("B"));
        assert!(s.contains("x"));
        assert!(s.contains("u32"));
    }

    #[test]
    fn variant_order_changes_canonical() {
        let a: DeriveInput = parse_quote! {
            enum Frame {
                A,
                B { x: u32 },
            }
        };
        let b: DeriveInput = parse_quote! {
            enum Frame {
                B { x: u32 },
                A,
            }
        };
        assert_ne!(canonical_string(&a), canonical_string(&b));
    }

    #[test]
    fn field_type_change_changes_canonical() {
        let a: DeriveInput = parse_quote! {
            struct S { x: u32 }
        };
        let b: DeriveInput = parse_quote! {
            struct S { x: u64 }
        };
        assert_ne!(canonical_string(&a), canonical_string(&b));
    }
}
