//! `PluginManifest` and supporting type/handler/etc. declarations.
//!
//! The manifest is the runtime view of a plugin's identity and
//! declared surface. Hashes are populated by the install layer (the
//! `interface_hash` is computed from the declarations + dep hashes;
//! the `content_hash` arrives via build.rs env-var). The structural
//! fields (`declared_*`) are populated from the linkme distributed
//! slices in `slices.rs`.

use std::collections::HashMap;

use crate::events::EventKind;
use crate::phases::PhaseDescriptor;
use crate::plugin_id::CanonicalId;
use crate::plugin_version::Blake3;
use crate::priority::Priority;
use crate::trait_data::{PipelineRole, TraitSet};
use crate::types::{EntityTypeId, PhaseId};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PluginKind {
    Model,
    View,
    Capability,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PluginBackend {
    Static,
    /// Reserved for future WASM-loaded plugins.
    Wasm,
}

/// SPDX license identifier (e.g. `"MIT"`, `"Apache-2.0"`,
/// `"MIT OR Apache-2.0"`). Validation is intentionally light — we
/// preserve the author's string verbatim.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SpdxLicenseId(pub String);

impl SpdxLicenseId {
    pub fn new(s: impl Into<String>) -> Self {
        Self(s.into())
    }
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// A type declared by `#[derive(EntityType)]`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TypeDeclaration {
    /// Local type name as written in the plugin (e.g. `"Fireball"`).
    pub type_name: String,
    /// Locally-assigned `EntityTypeId` (legacy compat: `#[entity(type_id = N)]`).
    pub type_id: EntityTypeId,
    /// Trait bits this type populates.
    pub trait_set: TraitSet,
    /// Stable per-trait-data signature hash for substitution checks.
    pub trait_data_signature: Blake3,
}

/// A handler declared by `#[handler(...)]`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HandlerDeclaration {
    /// Globally-namespaced handler name (e.g. `"dnd5e:opportunity_attack"`).
    pub name: String,
    pub event_kind: EventKind,
    pub role: PipelineRole,
    pub priority: Priority,
    /// Stable signature hash (event_kind || role || name).
    pub signature: Blake3,
}

/// A capability declared by a `CapabilityPlugin`. Future expansion;
/// today only carries `name` for surface-tracking.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CapabilityDeclaration {
    pub name: String,
    pub signature: Blake3,
}

/// A phase declared by a `ModelPlugin`. Mirrors `PhaseDescriptor` but
/// keeps just the identity bits relevant to compatibility.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PhaseDeclaration {
    pub phase_id: PhaseId,
    pub signature: Blake3,
}

/// A wire-frame schema declared by a CapabilityPlugin (or other
/// network-aware plugin). `signature` is the
/// `WireFrameHash`-style blake-derived `u64` reinterpreted into the
/// blake3 fingerprint slot for uniformity.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WireFrameDeclaration {
    pub frame_name: String,
    pub signature: Blake3,
}

/// C2-remainder: a scenario shipped by a plugin (initial-state preset,
/// e.g. "Standard 8×8 board", "Dragon's Keep — Session 1"). Surfaces
/// in the `GET /scenarios` listing. Phase 1 ships the *declaration*
/// only — no scenario *contents* live in the manifest yet; plugins
/// that author scenarios populate `declared_scenarios` with the
/// human-facing summary, and the actual entity placements travel
/// inside the plugin's own setup code (or a future scenario-payload
/// surface).
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScenarioDeclaration {
    pub name: String,
    pub description: String,
    pub entity_count: u32,
}

impl ScenarioDeclaration {
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        entity_count: u32,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            entity_count,
        }
    }
}

/// What a plugin declares about its dependencies.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PluginDependency {
    pub canonical_id: CanonicalId,
    pub required: bool,
    /// Compile-time fact: `(content_hash, interface_hash)` of the dep
    /// the dependent linked against.
    pub linked_against: (Blake3, Blake3),
    /// Per-item structural needs.
    pub needs: StructuralNeeds,
}

/// Per-item-name → signature_hash maps, for the structural-needs path
/// of dep resolution.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StructuralNeeds {
    pub types: HashMap<String, Blake3>,
    pub handlers: HashMap<String, Blake3>,
    pub capabilities: HashMap<String, Blake3>,
    pub phases: HashMap<String, Blake3>,
}

impl StructuralNeeds {
    pub fn is_empty(&self) -> bool {
        self.types.is_empty()
            && self.handlers.is_empty()
            && self.capabilities.is_empty()
            && self.phases.is_empty()
    }
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PluginManifest {
    // Identity (author-declared)
    pub canonical_id: CanonicalId,
    pub display_name: String,
    pub kind: PluginKind,
    pub authors: Vec<String>,
    pub description: String,
    pub license: SpdxLicenseId,
    pub repository: Option<String>,

    // Hashes
    pub content_hash: Blake3,
    pub interface_hash: Blake3,

    // Structural (linkme-derived)
    pub declared_types: Vec<TypeDeclaration>,
    pub declared_handlers: Vec<HandlerDeclaration>,
    pub declared_capabilities: Vec<CapabilityDeclaration>,
    pub declared_phases: Vec<PhaseDeclaration>,
    pub declared_wire_frames: Vec<WireFrameDeclaration>,

    // Constraints
    pub dependencies: Vec<PluginDependency>,

    /// Concrete phase descriptors used at session-create time to seed
    /// `PhaseStack`. Plugins that don't define phases at the manifest
    /// level leave this empty.
    ///
    /// Note: this is RUNTIME data (full descriptors with active-player
    /// rules, turn order, `PhaseNext`, etc.), distinct from the
    /// structural `declared_phases: Vec<PhaseDeclaration>` field above
    /// which is registration metadata for compatibility checks.
    ///
    /// WASM1-2 NOTE: skipped from serde because `PhaseDescriptor.name`
    /// is a `&'static str`, which can't be deserialized. WASM-backed
    /// plugins populate this field at install time from a parallel
    /// wire-safe representation (Phase 3+). Phase 2 only needs the
    /// identity / declaration surface, which round-trips cleanly.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub phase_descriptors: Vec<PhaseDescriptor>,

    // Forward-compat
    pub manifest_format_version: u32,

    /// WASM1 Phase 3c: the dispatch ABI version this plugin was
    /// compiled against. WASM-backed plugins set this via the
    /// `#[plugin]` macro's wasm32 codegen; native plugins set it
    /// via the macro's native branch. The host's
    /// `WasmPlugin::dispatch_handler` checks `== DISPATCH_ABI_V1`
    /// before invoking the plugin function and returns
    /// `Decision::Deny("unsupported dispatch ABI")` on mismatch —
    /// the plugin is not invoked.
    ///
    /// Bumped when the host↔plugin dispatch wire shape changes
    /// (handler entry signature, packed return layout, response
    /// encoding). Future versions can register migration shims
    /// keyed by the constant.
    #[cfg_attr(feature = "serde", serde(default = "default_dispatch_abi_version"))]
    pub dispatch_abi_version: u16,

    /// WASM1 Phase 4: Ed25519 public key of the signer who issued
    /// this manifest. All shipped marketplace plugins MUST have this
    /// populated. Hand-built fixtures (and pre-Phase-4 saved
    /// manifests after the serde default) leave it as `[0u8; 32]`,
    /// which `verify_maatpkg` rejects with `BadSigner` unless the
    /// zero key has been explicitly registered in the
    /// `LicenseRegistry` (dev-mode only).
    #[cfg_attr(feature = "serde", serde(default))]
    pub signer_key: [u8; 32],

    /// WASM1 Phase 5: per-plugin requested linear-memory ceiling in
    /// bytes. The host clamps this at `MAX_PLUGIN_MEMORY` (32 MiB)
    /// regardless of the request. `None` falls back to the
    /// host's `DEFAULT_PLUGIN_MEMORY` (16 MiB).
    #[cfg_attr(feature = "serde", serde(default))]
    pub requested_memory_bytes: Option<u64>,

    /// WASM1 Phase 5: per-plugin requested per-call instruction
    /// budget (wasmtime fuel units). The host clamps this at
    /// `MAX_FUEL_PER_CALL` regardless of the request. `None`
    /// falls back to the host's `DEFAULT_FUEL_PER_CALL`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub requested_fuel_per_call: Option<u64>,

    /// WASM1 Phase 5 sub-commit 3 (epoch) — per-plugin requested
    /// per-call wall-clock deadline expressed in wasmtime epoch
    /// ticks. With the host's `EPOCH_TICK_INTERVAL` of 50 ms, 10
    /// ticks ≈ 500 ms wall-clock. The host clamps this at
    /// `MAX_EPOCH_DEADLINE_TICKS` (100 ticks ≈ 5 s) regardless of
    /// the request. `None` falls back to the host's
    /// `DEFAULT_EPOCH_DEADLINE_TICKS`. Backward-compatible via
    /// `#[serde(default)]` — older manifests deserialize cleanly.
    #[cfg_attr(feature = "serde", serde(default))]
    pub requested_epoch_deadline_ticks: Option<u64>,

    /// WASM1 Phase 5: host-import allowlist. The runtime walks the
    /// module's `env`-namespaced imports and rejects with
    /// `MissingDeclaration` if any import name is not present in
    /// this list. Empty list (the serde default) means "allow all
    /// known host imports" — the v1 behaviour preserved for
    /// backwards compatibility while the macro-emitted allowlist
    /// stabilises. Macro-emitted plugins populate this from the
    /// `#[handler]` / `#[compose]` / register_* usage they actually
    /// reference.
    #[cfg_attr(feature = "serde", serde(default))]
    pub declared_host_imports: Vec<String>,

    /// F2-A: Semver of this plugin manifest. `(0, 1, 0)` by default.
    /// `cargo maat publish` sets this from the plugin's `Cargo.toml`
    /// `[package].version` field at publish time. Appended at the end
    /// of the struct (not slotted in next to the identity fields) so
    /// postcard's positional wire format keeps decoding pre-F2 blobs
    /// — the `#[serde(default)]` then fills `(0, 1, 0)`.
    #[cfg_attr(feature = "serde", serde(default = "default_version"))]
    pub version: (u32, u32, u32),

    /// F2-A / BL-021: Price in cents (USD). `None` = free;
    /// `Some(0)` = pay-what-you-want. The license registry is the
    /// authority on entitlement; this field tells the marketplace UI
    /// which packages are gated. Authors set this at publish time.
    /// Appended at the end of the struct alongside `version` so
    /// postcard wire compat with pre-F2 blobs holds.
    #[cfg_attr(feature = "serde", serde(default))]
    pub price_cents: Option<u32>,

    /// A3 — explicit plugin backend. Distinguishes statically-linked
    /// plugins (compiled into the server binary) from WASM-loaded
    /// plugins (signed `.maatpkg`). The host-import allowlist policy
    /// branches on this field: `Static` plugins skip the allowlist
    /// check (they share the server's address space and aren't
    /// sandboxed); `Wasm` plugins require a non-empty
    /// `declared_host_imports`. Appended at the end of the struct;
    /// `#[serde(default = "default_backend")]` provides `Static` as
    /// the default for JSON / pretty deserializers that honor the
    /// hint. (Note: postcard's positional wire format does NOT
    /// activate serde-default-on-EOF for missing trailing fields —
    /// pre-A3 wasm fixture blobs must be rebuilt to pick up the new
    /// field. Production plugins re-encode at compile time via the
    /// `#[plugin]` macro and pick up the field automatically.)
    #[cfg_attr(feature = "serde", serde(default = "default_backend"))]
    pub backend: PluginBackend,

    /// C2-remainder: plugin-declared scenarios. Surfaced by
    /// `GET /scenarios`. Empty by default. Authored via the
    /// `#[plugin]` macro's `scenarios = [...]` arg. Appended at the
    /// end of the struct (trailing field) and gated by
    /// `#[serde(default)]` so legacy postcard/JSON blobs continue to
    /// decode as `vec![]`.
    #[cfg_attr(feature = "serde", serde(default))]
    pub declared_scenarios: Vec<ScenarioDeclaration>,
}

pub const MANIFEST_FORMAT_VERSION: u32 = 1;

/// WASM1 Phase 3c: the current dispatch ABI version. WASM plugins
/// stamp this into their manifest at compile time via the
/// `#[plugin]` macro; the host gates `dispatch_handler` on a
/// match. Bumping this constant is a breaking change — every
/// shipped wasm plugin must be recompiled.
pub const DISPATCH_ABI_V1: u16 = 1;

/// Serde default for `dispatch_abi_version` so older serialized
/// manifests (pre-WASM1-3c) round-trip through deserialize as
/// `DISPATCH_ABI_V1`. New manifests always emit the field
/// explicitly via the `#[plugin]` macro.
#[cfg(feature = "serde")]
fn default_dispatch_abi_version() -> u16 {
    DISPATCH_ABI_V1
}

/// Serde default for `PluginManifest::version` so legacy postcard
/// blobs that pre-date F2 deserialize as `(0, 1, 0)`.
#[cfg(feature = "serde")]
fn default_version() -> (u32, u32, u32) {
    (0, 1, 0)
}

/// Serde default for `PluginManifest::backend` so pre-A3 postcard
/// blobs deserialize as `Static` (the legacy implicit default before
/// the field existed).
#[cfg(feature = "serde")]
fn default_backend() -> PluginBackend {
    PluginBackend::Static
}

/// Block-separator bytes for canonical interface-hash encoding.
/// Each declared-* surface is prefixed with a distinct byte so an
/// item migrating from one block to another (e.g. renamed from a
/// type into a capability) produces a different hash even if the
/// name/signature bytes match. Reserve `0x00` as a field separator
/// inside an entry's `name \0 signature` layout.
const HASH_TAG_TYPES: u8 = 0x01;
const HASH_TAG_HANDLERS: u8 = 0x02;
const HASH_TAG_CAPABILITIES: u8 = 0x03;
const HASH_TAG_PHASES: u8 = 0x04;
const HASH_TAG_WIRE_FRAMES: u8 = 0x05;
const HASH_TAG_DEPENDENCIES: u8 = 0x06;
const HASH_TAG_SCENARIOS: u8 = 0x07;

/// Compute the per-plugin `interface_hash` over the canonical form of
/// the plugin's declared surface plus its transitive dep
/// interface-hashes.
///
/// # Canonical form
///
/// Each block below is fed to a single Blake3 hasher in this order:
///
/// 1. `[HASH_TAG_TYPES]` then, for each entry of `declared_types`
///    sorted lexicographically by `type_name`:
///    `type_name.as_bytes() || 0x00 || trait_data_signature`.
/// 2. `[HASH_TAG_HANDLERS]` then, for each entry of `declared_handlers`
///    sorted lexicographically by `name`:
///    `name.as_bytes() || 0x00 || event_kind_debug.as_bytes()
///     || 0x00 || role_debug.as_bytes() || 0x00 || signature`.
/// 3. `[HASH_TAG_CAPABILITIES]` then, for each entry of
///    `declared_capabilities` sorted lexicographically by `name`:
///    `name.as_bytes() || 0x00 || signature`.
/// 4. `[HASH_TAG_PHASES]` then, for each entry of `declared_phases`
///    sorted lexicographically by `phase_id.as_str()`:
///    `phase_id.as_str().as_bytes() || 0x00 || signature`.
/// 5. `[HASH_TAG_WIRE_FRAMES]` then, for each entry of
///    `declared_wire_frames` sorted lexicographically by `frame_name`:
///    `frame_name.as_bytes() || 0x00 || signature`.
/// 6. `[HASH_TAG_DEPENDENCIES]` then, for each entry of `dependencies`
///    sorted lexicographically by `canonical_id.as_str()`:
///    `canonical_id.as_bytes() || 0x00 || dep_interface_hash`
///    where `dep_interface_hash = linked_against.1`.
///
/// Sorting is performed inside this function — callers do not need
/// to pre-sort the slices (linkme distributed-slice iteration order
/// isn't stable).
///
/// An empty plugin (no declarations, no deps) still produces a
/// well-defined non-zero hash because the six block-separator bytes
/// are always fed.
pub fn compute_interface_hash(
    declared_types: &[TypeDeclaration],
    declared_handlers: &[HandlerDeclaration],
    declared_capabilities: &[CapabilityDeclaration],
    declared_phases: &[PhaseDeclaration],
    declared_wire_frames: &[WireFrameDeclaration],
    dependencies: &[PluginDependency],
    declared_scenarios: &[ScenarioDeclaration],
) -> Blake3 {
    let mut hasher = blake3::Hasher::new();

    // 1. Types — sort by type_name.
    hasher.update(&[HASH_TAG_TYPES]);
    let mut types: Vec<&TypeDeclaration> = declared_types.iter().collect();
    types.sort_by(|a, b| a.type_name.cmp(&b.type_name));
    for t in &types {
        hasher.update(t.type_name.as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&t.trait_data_signature);
    }

    // 2. Handlers — sort by name.
    hasher.update(&[HASH_TAG_HANDLERS]);
    let mut handlers: Vec<&HandlerDeclaration> = declared_handlers.iter().collect();
    handlers.sort_by(|a, b| a.name.cmp(&b.name));
    for h in &handlers {
        hasher.update(h.name.as_bytes());
        hasher.update(&[0x00]);
        // EventKind / PipelineRole are Copy + Debug simple C-style
        // enums. Their {:?} format is the variant identifier, which
        // is stable across rustc versions for unit variants.
        hasher.update(format!("{:?}", h.event_kind).as_bytes());
        hasher.update(&[0x00]);
        hasher.update(format!("{:?}", h.role).as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&h.signature);
    }

    // 3. Capabilities — sort by name.
    hasher.update(&[HASH_TAG_CAPABILITIES]);
    let mut caps: Vec<&CapabilityDeclaration> = declared_capabilities.iter().collect();
    caps.sort_by(|a, b| a.name.cmp(&b.name));
    for c in &caps {
        hasher.update(c.name.as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&c.signature);
    }

    // 4. Phases — sort by phase_id string.
    hasher.update(&[HASH_TAG_PHASES]);
    let mut phases: Vec<&PhaseDeclaration> = declared_phases.iter().collect();
    phases.sort_by(|a, b| a.phase_id.as_str().cmp(b.phase_id.as_str()));
    for p in &phases {
        hasher.update(p.phase_id.as_str().as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&p.signature);
    }

    // 5. Wire frames — sort by frame_name.
    hasher.update(&[HASH_TAG_WIRE_FRAMES]);
    let mut frames: Vec<&WireFrameDeclaration> = declared_wire_frames.iter().collect();
    frames.sort_by(|a, b| a.frame_name.cmp(&b.frame_name));
    for f in &frames {
        hasher.update(f.frame_name.as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&f.signature);
    }

    // 6. Dependencies — sort by canonical_id. Contribute each dep's
    //    interface_hash (linked_against.1), NOT its content_hash.
    hasher.update(&[HASH_TAG_DEPENDENCIES]);
    let mut deps: Vec<&PluginDependency> = dependencies.iter().collect();
    deps.sort_by(|a, b| a.canonical_id.as_str().cmp(b.canonical_id.as_str()));
    for d in &deps {
        hasher.update(d.canonical_id.as_str().as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&d.linked_against.1);
    }

    // 7. Scenarios — sort by name. Each entry contributes
    //    `name || 0x00 || description || 0x00 || entity_count_le_bytes`.
    //    The signature this adds to interface_hash is cosmetic in the
    //    initial Phase-1 cut (no plugin populates declared_scenarios
    //    yet), but the canonical-form surface needs to nail down the
    //    encoding so a future scenario-bearing plugin's hash is
    //    stable across rustc versions.
    hasher.update(&[HASH_TAG_SCENARIOS]);
    let mut scenarios: Vec<&ScenarioDeclaration> = declared_scenarios.iter().collect();
    scenarios.sort_by(|a, b| a.name.cmp(&b.name));
    for s in &scenarios {
        hasher.update(s.name.as_bytes());
        hasher.update(&[0x00]);
        hasher.update(s.description.as_bytes());
        hasher.update(&[0x00]);
        hasher.update(&s.entity_count.to_le_bytes());
    }

    let mut out = [0u8; 32];
    out.copy_from_slice(hasher.finalize().as_bytes());
    out
}

/// Parse a `"MAJOR.MINOR.PATCH"` string into a `(u32, u32, u32)` tuple,
/// returning `(0, 1, 0)` on any parse failure. Used by the `#[plugin]`
/// macro to translate `env!("CARGO_PKG_VERSION")` into the manifest's
/// `version` field at the plugin's compile time.
pub fn parse_semver_or_default(s: &str) -> (u32, u32, u32) {
    let mut parts = s.splitn(3, '.');
    let maj = parts.next().and_then(|p| p.parse::<u32>().ok());
    let min = parts.next().and_then(|p| p.parse::<u32>().ok());
    let pat = parts.next().and_then(|p| p.parse::<u32>().ok());
    match (maj, min, pat) {
        (Some(a), Some(b), Some(c)) => (a, b, c),
        _ => (0, 1, 0),
    }
}
