//! UC-16e — `#[derive(Artifact)]` proc-macro.
//!
//! Walks the struct's fields in declared order, builds a canonical
//! string representation, hashes it with Blake3 at proc-macro
//! expansion time, and emits an `impl Artifact for T { const
//! SCHEMA_HASH: &'static str = "<hex>"; }`.
//!
//! Canonical string format:
//!   `<StructName>{<field0_name>:<field0_type>,<field1_name>:<field1_type>,...}`
//!
//! For tuple structs, the "name" is the positional index (`0`, `1`, ...).
//! For unit structs, the body is empty.
//!
//! Field types are rendered via `ToTokens::to_token_stream().to_string()`,
//! which is stable for a given source text. Two structs with the same
//! shape produce the same hash; renaming/retyping/reordering/adding
//! fields produces a different hash.
//!
//! Detection: structural (field-level). Misses semantic-only changes
//! (e.g. unit switch from ms to s while keeping `u64`). Authors must
//! still bump the schema on semantic changes; the derive removes only
//! the structural-forget-to-bump risk.
//!
//! See spec at
//! `docs/superpowers/specs/2026-05-14-uc-16e-artifact-derive-macro.md`.

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

pub fn derive_impl(input: DeriveInput) -> TokenStream2 {
    if !input.generics.params.is_empty() {
        return error(
            input.ident.span(),
            "#[derive(Artifact)] does not support generic or lifetime parameters; \
             stamped artifacts must be concrete types",
        );
    }
    let name = &input.ident;
    let name_str = name.to_string();

    let canonical = match &input.data {
        Data::Struct(s) => canonicalize_struct(&name_str, &s.fields),
        Data::Enum(_) => {
            return error(
                input.ident.span(),
                "#[derive(Artifact)] only supports structs (enums are rejected at v1)",
            );
        }
        Data::Union(_) => {
            return error(
                input.ident.span(),
                "#[derive(Artifact)] does not support unions",
            );
        }
    };

    // Compute Blake3 at proc-macro expansion time; emit the hex string
    // as a literal so consumers don't pull in a runtime blake3 dep.
    let hash_hex = blake3::hash(canonical.as_bytes()).to_hex().to_string();

    quote! {
        impl ::maat_sdk::artifact::Artifact for #name {
            const SCHEMA_HASH: &'static str = #hash_hex;
        }
    }
}

fn canonicalize_struct(name: &str, fields: &Fields) -> String {
    let body = match fields {
        Fields::Named(named) => named
            .named
            .iter()
            .map(|f| {
                let field_name = f
                    .ident
                    .as_ref()
                    .map(|i| i.to_string())
                    .unwrap_or_default();
                let ty = f.ty.to_token_stream().to_string();
                // Normalize whitespace in type tokens — `quote` may emit
                // `Vec < String >` with spaces; collapse them so visually-
                // identical types hash identically across rustc versions.
                let ty = ty.split_whitespace().collect::<String>();
                format!("{field_name}:{ty}")
            })
            .collect::<Vec<_>>()
            .join(","),
        Fields::Unnamed(unnamed) => unnamed
            .unnamed
            .iter()
            .enumerate()
            .map(|(i, f)| {
                let ty = f.ty.to_token_stream().to_string();
                let ty = ty.split_whitespace().collect::<String>();
                format!("{i}:{ty}")
            })
            .collect::<Vec<_>>()
            .join(","),
        Fields::Unit => String::new(),
    };
    format!("{name}{{{body}}}")
}

fn error(span: proc_macro2::Span, msg: &str) -> TokenStream2 {
    syn::Error::new(span, msg).to_compile_error()
}

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

    fn run(src: &str) -> String {
        let input: DeriveInput = syn::parse_str(src).expect("parse");
        derive_impl(input).to_string()
    }

    #[test]
    fn unit_struct_emits_impl() {
        let out = run("struct Foo;");
        assert!(out.contains("SCHEMA_HASH"));
        // Canonical "Foo{}" Blake3 (stable across runs).
        let expected = blake3::hash(b"Foo{}").to_hex().to_string();
        assert!(out.contains(&expected), "expected {expected:?} in {out}");
    }

    #[test]
    fn named_struct_emits_field_canonical() {
        let out = run("struct Foo { a: u64, b: String }");
        let expected = blake3::hash(b"Foo{a:u64,b:String}").to_hex().to_string();
        assert!(out.contains(&expected));
    }

    #[test]
    fn tuple_struct_emits_positional_canonical() {
        let out = run("struct Foo(u32, String);");
        let expected = blake3::hash(b"Foo{0:u32,1:String}").to_hex().to_string();
        assert!(out.contains(&expected));
    }

    #[test]
    fn renaming_field_changes_hash() {
        let a = run("struct Foo { a: u64 }");
        let b = run("struct Foo { b: u64 }");
        assert_ne!(a, b);
    }

    #[test]
    fn retyping_field_changes_hash() {
        let a = run("struct Foo { a: u32 }");
        let b = run("struct Foo { a: u64 }");
        assert_ne!(a, b);
    }

    #[test]
    fn adding_field_changes_hash() {
        let a = run("struct Foo { a: u64 }");
        let b = run("struct Foo { a: u64, b: u64 }");
        assert_ne!(a, b);
    }

    #[test]
    fn reordering_fields_changes_hash() {
        let a = run("struct Foo { a: u64, b: u64 }");
        let b = run("struct Foo { b: u64, a: u64 }");
        assert_ne!(a, b);
    }

    #[test]
    fn renaming_struct_changes_hash() {
        let a = run("struct Foo { x: u64 }");
        let b = run("struct Bar { x: u64 }");
        assert_ne!(a, b);
    }

    #[test]
    fn enum_input_emits_compile_error() {
        let out = run("enum Foo { A, B }");
        assert!(out.contains("compile_error"));
        assert!(out.contains("only supports structs"));
    }

    #[test]
    fn generic_input_emits_compile_error() {
        let out = run("struct Foo<T> { x: T }");
        assert!(out.contains("compile_error"));
        assert!(out.contains("generic"));
    }

    #[test]
    fn whitespace_in_type_normalized() {
        // Different whitespace in the source should produce the same hash
        // because quote may emit `Vec < T >` with spaces in some cases.
        let a = run("struct Foo { x: Vec<u32> }");
        let b = run("struct Foo { x: Vec < u32 > }");
        // Both canonicalize to "Foo{x:Vec<u32>}"
        let expected = blake3::hash(b"Foo{x:Vec<u32>}").to_hex().to_string();
        assert!(a.contains(&expected), "first form should match");
        assert!(b.contains(&expected), "spaced form should match");
    }
}
