//! Proc-macro derives for the maat SDK.
//!
//! ## `#[derive(EntityType)]`
//!
//! (Previously `#[derive(Entity)]`; renamed in ER1 step 2 to avoid
//! colliding with the runtime `maat_sdk::entity_state::Entity`
//! struct.)
//!
//! Generates an `Entity` impl that produces an `EntityBlueprint` from a struct's
//! fields. Fields are mapped to traits by *type inference*:
//!
//! | Field type        | Trait applied                         |
//! |-------------------|---------------------------------------|
//! | `Position`        | `.tangible(TangibleData::at(field))`  |
//! | `SeatId`          | `.ownable(OwnableData::owned_by(field))` |
//! | `Owner`           | `.ownable(OwnableData { owner: field })` |
//! | `TangibleData`    | `.tangible(field)`                    |
//! | `OwnableData`     | `.ownable(field)`                     |
//! | `NamedData`       | `.named(field)`                       |
//! | `SustainedData`   | `.sustained(field)`                   |
//! | `TemporalData`    | `.temporal(field)`                    |
//! | `RangedData`      | `.ranged(field)`                      |
//! | `ArealData`       | `.areal(field)`                       |
//! | `FeaturedData`    | `.featured(field)`                    |
//! | `QuantifiedData`  | `.quantified(field)`                  |
//! | `MovableData`     | `.movable(field)`                     |
//! | `ContainerData`   | `.container(field)`                   |
//! | `ConnectorData`   | `.connector(field)`                   |
//! | `AttachedData`    | `.attached(field)`                    |
//! | `MorphableData`   | `.morphable(field)`                   |
//! | `ObscurableData`  | `.obscurable(field)`                  |
//! | `ActivatableData` | `.activatable(field)`                 |
//!
//! Fields starting with `_` are ignored.
//!
//! Struct-level attributes:
//!
//! ```ignore
//! #[derive(EntityType)]
//! #[entity(type_id = 42, display = "Pawn")]
//! pub struct Pawn {
//!     pub owner: SeatId,
//!     pub position: Position,
//! }
//! ```
//!
//! Generates:
//! - `const TYPE_ID: EntityTypeId = EntityTypeId::from_raw(42);`
//! - `fn blueprint(self) -> EntityBlueprint` — `display = "..."` produces `NamedData`
//!   (display data). To make the entity a top-level sidebar entry, additionally
//!   include a `pub featured: FeaturedData` field on the struct.
//! - `impl From<Pawn> for EntityBlueprint`

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

mod artifact;
mod gate;
mod wire_frame_hash;

/// Parsed `#[entity(type_id = ..., display = "...")]` attribute.
///
/// `type_id` accepts either an integer literal (the macro wraps it in
/// `EntityTypeId::from_raw(N)`) OR a full const expression that
/// evaluates to `EntityTypeId` — the canonical
/// `entity_type_id_for(canonical_id, "Name")` pattern is the intended
/// use of the expression form. The macro emits the expression verbatim;
/// any const expr that doesn't produce `EntityTypeId` fails at the call
/// site with a normal type error.
struct EntityAttrs {
    type_id: Option<TypeIdSource>,
    display: Option<String>,
    /// S27-followup — opt out of the default `.present().observable()`
    /// (and `.located()` when Tangible) marker bits added to the
    /// generated `.blueprint()` chain. Set via `#[entity(staged)]`.
    ///
    /// The combat plugin's `Encounter` / `Monster` use this: members
    /// are spawned hidden during the staging phase, then a phase-
    /// transition handler emits `TraitAdded { PRESENT }` to reveal
    /// them. Without `staged`, the derive's default markers would
    /// reveal them prematurely.
    staged: bool,
}

enum TypeIdSource {
    /// `#[entity(type_id = 42)]` — wrap in `EntityTypeId::from_raw(42)`.
    Literal(u64),
    /// `#[entity(type_id = entity_type_id_for(...))]` — emit verbatim.
    Expr(syn::Expr),
}

fn parse_entity_attrs(attrs: &[syn::Attribute]) -> EntityAttrs {
    let mut out = EntityAttrs { type_id: None, display: None, staged: false };
    for attr in attrs {
        if !attr.path().is_ident("entity") {
            continue;
        }
        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("type_id") {
                // Parse any const expression. If it's a single integer
                // literal, materialise to the `Literal` arm so the macro
                // can wrap it in `EntityTypeId::from_raw(...)`. Anything
                // else is preserved as `Expr` and emitted verbatim — the
                // intended use is `entity_type_id_for(canonical_id, "Name")`.
                let expr: syn::Expr = meta.value()?.parse()?;
                if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(lit_int), .. }) = &expr {
                    let v = lit_int.base10_parse::<u64>()?;
                    out.type_id = Some(TypeIdSource::Literal(v));
                } else {
                    out.type_id = Some(TypeIdSource::Expr(expr));
                }
            } else if meta.path.is_ident("display") {
                let v: syn::LitStr = meta.value()?.parse()?;
                out.display = Some(v.value());
            } else if meta.path.is_ident("staged") {
                out.staged = true;
            }
            Ok(())
        });
    }
    out
}

/// Return a short type name (last segment, no generics) for simple matching.
fn type_ident_name(ty: &Type) -> Option<String> {
    if let Type::Path(p) = ty {
        p.path.segments.last().map(|s| s.ident.to_string())
    } else {
        None
    }
}

/// Bit numbers must mirror `maat_sdk::traits::TraitSet`. Used by the
/// derive macro to compute a `trait_set_bits: u32` from field types so
/// the linkme registration can carry the trait-set without running plugin
/// code.
fn trait_bit_for_field_type(name: &str) -> Option<u64> {
    Some(match name {
        "FeaturedData" => 1u64 << 0,
        "ObscurableData" => 1u64 << 2,
        "OwnableData" | "Owner" | "SeatId" => 1u64 << 3,
        "MovableData" => 1u64 << 5,
        "ContainerData" => 1u64 << 6,
        "QuantifiedData" => 1u64 << 9,
        "ActivatableData" => 1u64 << 10,
        "MorphableData" => 1u64 << 11,
        "TangibleData" | "Position" => 1u64 << 12,
        "AttachedData" => 1u64 << 13,
        "ConnectorData" => 1u64 << 14,
        "NamedData" => 1u64 << 15,
        "RangedData" => 1u64 << 16,
        "ArealData" => 1u64 << 17,
        "TemporalData" => 1u64 << 18,
        "SustainedData" => 1u64 << 19,
        "GatedData" => 1u64 << 20,
        "SenseData" => 1u64 << 23,
        "NarratedData" => 1u64 << 31,
        _ => return None,
    })
}

/// `#[derive(EntityType)]` — the entity-type derive (ER1, 2026-05-09).
///
/// The previous name `#[derive(Entity)]` was retired in ER1 step 2 because
/// it collided with the runtime `maat_sdk::entity_state::Entity`
/// struct.
#[proc_macro_derive(EntityType, attributes(entity))]
pub fn derive_entity_type(input: TokenStream) -> TokenStream {
    derive_entity_impl(input)
}

fn derive_entity_impl(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let attrs = parse_entity_attrs(&input.attrs);

    // Build the token stream that produces the entity's `EntityTypeId`.
    // For literal form `#[entity(type_id = 42)]`, wrap in
    // `EntityTypeId::from_raw(42)`. For expression form
    // `#[entity(type_id = entity_type_id_for(...))]`, emit the expression
    // verbatim — it must evaluate to `EntityTypeId`.
    let type_id_expr: proc_macro2::TokenStream = match attrs.type_id {
        Some(TypeIdSource::Literal(n)) => {
            let n_lit = proc_macro2::Literal::u64_unsuffixed(n);
            quote! { ::maat::__sdk::EntityTypeId::from_raw(#n_lit) }
        }
        Some(TypeIdSource::Expr(expr)) => quote! { #expr },
        None => {
            return syn::Error::new_spanned(
                &input.ident,
                "#[derive(EntityType)] requires #[entity(type_id = N)] (literal) or #[entity(type_id = entity_type_id_for(...))] (const expression)",
            )
            .to_compile_error()
            .into();
        }
    };

    let fields = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(named) => &named.named,
            _ => {
                return syn::Error::new_spanned(
                    &input.ident,
                    "#[derive(EntityType)] only supports structs with named fields",
                )
                .to_compile_error()
                .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(
                &input.ident,
                "#[derive(EntityType)] only supports structs",
            )
            .to_compile_error()
            .into();
        }
    };

    // Walk the fields; emit a trait call per field based on the field's type name.
    let mut chain = TokenStream2::new();
    // TS1: widened to u64 (was u32) — matches TraitSet's backing type.
    let mut trait_set_bits: u64 = 0;

    // If #[entity(display = "...")] is set, start with NamedData. Featured
    // (sidebar visibility) is added explicitly via a `FeaturedData` field on
    // the struct — display data and sidebar status are independent.
    if let Some(disp) = &attrs.display {
        chain.extend(quote! {
            .named(::maat::__sdk::NamedData::new(#disp))
        });
        trait_set_bits |= 1u64 << 15; // NAMED
    }

    // S27-followup: track whether the derived entity has a positional
    // component so we can default LOCATED only when meaningful.
    let mut has_tangible = false;

    for field in fields {
        let Some(fname) = field.ident.as_ref() else { continue };
        if fname.to_string().starts_with('_') {
            continue;
        }
        let Some(tname) = type_ident_name(&field.ty) else { continue };

        let call = match tname.as_str() {
            // Convenience: raw SeatId/Position become full data by default.
            "Position"        => { has_tangible = true; quote! { .tangible(::maat::__sdk::TangibleData::at(self.#fname)) } },
            "SeatId"          => quote! { .ownable(::maat::__sdk::OwnableData::owned_by(self.#fname)) },
            "Owner"           => quote! { .ownable(::maat::__sdk::OwnableData { owner: self.#fname }) },

            // Pre-built *Data values pass through directly.
            "TangibleData"    => { has_tangible = true; quote! { .tangible(self.#fname) } },
            "OwnableData"     => quote! { .ownable(self.#fname) },
            "NamedData"       => quote! { .named(self.#fname) },
            "SustainedData"   => quote! { .sustained(self.#fname) },
            "TemporalData"    => quote! { .temporal(self.#fname) },
            "RangedData"      => quote! { .ranged(self.#fname) },
            "ArealData"       => quote! { .areal(self.#fname) },
            "FeaturedData"    => quote! { .featured(self.#fname) },
            "QuantifiedData"  => quote! { .quantified(self.#fname) },
            "MovableData"     => quote! { .movable(self.#fname) },
            "ContainerData"   => quote! { .container(self.#fname) },
            "ConnectorData"   => quote! { .connector(self.#fname) },
            "AttachedData"    => quote! { .attached(self.#fname) },
            "MorphableData"   => quote! { .morphable(self.#fname) },
            // ObservableData removed (S27): use .present().located().observable() markers instead.
            // If a plugin still has an ObservableData field in a derive struct, skip it silently.
            "ObscurableData"  => quote! { .obscurable(self.#fname) },
            "ActivatableData" => quote! { .activatable(self.#fname) },
            "NarratedData"    => quote! { .narrated(self.#fname) },

            _ => continue, // unknown type — plugin-specific, ignored by this derive
        };
        chain.extend(call);
        if let Some(b) = trait_bit_for_field_type(&tname) {
            trait_set_bits |= b;
        }
    }

    // S27-followup (2026-05-30): the S27 visibility refactor removed
    // `ObservableData::public()` from plugin blueprints, but the
    // engine's perceivability filter (`session.entity_view_for`)
    // still requires PRESENT to surface an entity in client
    // snapshots. The migration left every derived entity invisible
    // unless the plugin author explicitly called `.present()` on the
    // blueprint — which most authors didn't (catan, chess, life,
    // dnd5e, stratego all regressed).
    //
    // Default to public perceivability for derived entities:
    // - PRESENT (entity exists in the snapshot)
    // - OBSERVABLE (full data is visible, not just a shell)
    // - LOCATED iff the entity has a positional component
    //
    // Plugins that want hidden entities should use per-seat
    // `ObservableData::perceivability_overrides` for the partial
    // case, or the `#[entity(staged)]` attribute to opt out of
    // the default markers entirely (combat's encounter staging
    // uses the latter).
    if !attrs.staged {
        chain.extend(quote! { .present().observable() });
        if has_tangible {
            chain.extend(quote! { .located() });
        }
    }

    // Linkme registration for the plugin registry (S25). One static
    // per `#[derive(EntityType)]` invocation, contributed to the global
    // `REGISTERED_ENTITIES` distributed slice. The signature byte is
    // currently zero-stubbed; downstream phases compute it from the
    // type's trait-data shape.
    let reg_ident = quote::format_ident!(
        "__METAGAMER_ENTITY_REG_{}",
        name.to_string().to_uppercase()
    );
    let type_name_str = name.to_string();
    let bits_lit = trait_set_bits;
    // 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_ENTITIES)]
            #[linkme(crate = ::maat::__sdk::linkme)]
            #[allow(non_upper_case_globals)]
            static #reg_ident: ::maat::__sdk::EntityRegistration =
                ::maat::__sdk::EntityRegistration {
                    plugin_crate_name:    env!("CARGO_PKG_NAME"),
                    type_name:            #type_name_str,
                    type_id:              #type_id_expr,
                    trait_set_bits:       #bits_lit,
                    trait_data_signature: [0u8; 32],
                };
        };
    };

    let expanded = quote! {
        impl #name {
            pub const TYPE_ID: ::maat::__sdk::EntityTypeId =
                #type_id_expr;

            /// Build an `EntityBlueprint` from this value.
            pub fn blueprint(self) -> ::maat::__sdk::EntityBlueprint {
                ::maat::__sdk::EntityBlueprint::new(Self::TYPE_ID)
                    #chain
            }
        }

        impl ::core::convert::From<#name> for ::maat::__sdk::EntityBlueprint {
            fn from(v: #name) -> Self {
                v.blueprint()
            }
        }

        #registration
    };

    expanded.into()
}

/// `#[derive(WireFrameHash)]` — emit a stable schema hash for a wire frame
/// type. See `crate::wire_frame_hash` for the canonical-string format and
/// hashing details. The hash is computed at proc-macro expansion time and
/// embedded as a literal `u64` const, so consumers do not pull in a runtime
/// `blake3` dependency.
#[proc_macro_derive(WireFrameHash)]
pub fn derive_wire_frame_hash(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    wire_frame_hash::derive_impl(input).into()
}

/// UC-16e — `#[derive(Artifact)]` generates an
/// `impl Artifact for T { const SCHEMA_HASH: &'static str = "..."; }`
/// where the hash is a Blake3 of the type's canonical structural
/// representation. Any structural change (field add/remove/rename/
/// retype/reorder) produces a different hash, so forgetting to bump
/// the schema after a destructive change is impossible.
///
/// Doesn't detect semantic-only changes (e.g. reinterpreting a `u64`
/// from milliseconds to seconds) — those still require a human-driven
/// version bump via a manual `impl Artifact for T`.
///
/// Restrictions at v1:
/// - structs only (enums/unions rejected)
/// - no generic or lifetime parameters
/// - nested artifact-shaped types should also derive `Artifact` if
///   changes to them should propagate (soft convention)
///
/// See spec at
/// `docs/superpowers/specs/2026-05-14-uc-16e-artifact-derive-macro.md`.
#[proc_macro_derive(Artifact)]
pub fn derive_artifact(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    artifact::derive_impl(input).into()
}

/// `gate!` — small DSL that lowers to the `Predicate` IR.
///
/// See `crate::gate` for the supported surface (boolean composition,
/// `pool!`/`pool_eff!`, `type_eq`, `exists`, `any_attached`/`all_attached`/
/// `any_contained`/`all_contained` ... `where { ... }`).
///
/// Expansion uses fully-qualified `::maat::__sdk::predicate::*` paths so
/// the macro works without any specific imports at the call site.
#[proc_macro]
pub fn gate(input: TokenStream) -> TokenStream {
    let parsed = parse_macro_input!(input as gate::GateExpr);
    gate::lower(&parsed).into()
}

mod plugin_attr;

/// `#[handler(event = ..., role = ..., priority = ..., name = "...")]`
/// — placed on a `&self` method *inside* an `impl PluginStruct {
/// ... }` block that itself carries `#[handlers]`. The outer
/// `#[handlers]` attribute walks each tagged method and emits the
/// thunk + linkme registration at module scope.
///
/// Phase 5a (S25): direct `#[handler]` on a free function is
/// rejected — handlers must be methods so they can read plugin
/// state through `&self`.
#[proc_macro_attribute]
pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::handler_impl(args.into(), input.into()).into()
}

/// `#[handlers]` — attribute macro on `impl PluginStruct { ... }`
/// blocks. Walks the inner methods looking for `#[handler(...)]` and
/// emits the registration boilerplate (thunk + linkme static) at
/// module scope, alongside the impl block.
#[proc_macro_attribute]
pub fn handlers(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::handlers_impl(args.into(), input.into()).into()
}

/// `#[plugin(canonical_id = "...", display_name = "...", authors = [...],
/// description = "...", license = "...", repository = "...")]` — annotate
/// a plugin struct.
///
/// Generates a `manifest()` method on the struct that filters the global
/// linkme slices by `CARGO_PKG_NAME && plugin_struct == "<name>"` and
/// assembles a `PluginManifest`. Cached in a `OnceLock`. Also exposes a
/// `__maat_dispatch_table()` helper used by `#[model_plugin]` to
/// route incoming `LocalHandlerId`s to the correct handler thunk.
#[proc_macro_attribute]
pub fn plugin(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::plugin_impl(args.into(), input.into()).into()
}

/// `#[model_plugin]` — attach to an `impl ModelPlugin for X { ... }`
/// block. If `dispatch_handler` is not author-supplied, the macro
/// fills it in using the `#[plugin]`-generated dispatch table.
#[proc_macro_attribute]
pub fn model_plugin(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::model_plugin_impl(args.into(), input.into()).into()
}

/// `#[capability_plugin(canonical_id = "...", display_name = "...",
/// authors = [...], description = "...", license = "...",
/// repository = "...", frame_variant = "annotations" | "audio" |
/// "journal" | "generic")]` — annotate a struct that already implements
/// `maat_sdk::Capability`. Generates an inherent `maat_manifest()`
/// method (returning `&'static PluginManifest` with `kind =
/// PluginKind::Capability` and content_hash from the build.rs-set
/// `METAGAMER_PLUGIN_CONTENT_HASH` env var) and an `impl
/// maat_engine::plugin::CapabilityPlugin for Self` block delegating
/// to `maat_sdk::Capability` for typed methods.
///
/// The `frame_variant` arg selects which `CapabilityFrame` envelope arm
/// carries this capability's payload. In-tree capabilities set this
/// explicitly; out-of-tree capabilities default to `"generic"` (uses the
/// `Generic { id, bytes }` escape variant).
#[proc_macro_attribute]
pub fn capability_plugin(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::capability_plugin_impl(args.into(), input.into()).into()
}

/// `#[view_plugin(canonical_id = "...", display_name = "...",
/// authors = [...], description = "...", license = "...",
/// repository = "...")]` — annotate the **renderer** struct (the one
/// that already impls `maat_view_sdk::ViewPlugin`).
///
/// Emits a sibling `<Renderer>Metadata` zero-sized struct with:
/// - An inherent `maat_manifest()` accessor returning a
///   `&'static PluginManifest` (cached in a `OnceLock`).
/// - An `impl maat_sdk::ViewPluginMetadata` block
///   delegating to the manifest accessor.
///
/// PG1 phase 2 MVP. Custom `applies_to_canonical_ids`,
/// `declared_entity_coverage`, `supported_canvas_modes` can be added
/// in a sibling `impl <Renderer>Metadata { ... }` block.
#[proc_macro_attribute]
pub fn view_plugin(args: TokenStream, input: TokenStream) -> TokenStream {
    plugin_attr::view_plugin_impl(args.into(), input.into()).into()
}
