//! C ABI for SDK consumers. Stable, language-agnostic surface — every
//! other-language binding (Python ctypes, Node N-API, Swift, JNI, etc.)
//! talks to this. The Rust crate is built as `cdylib`/`staticlib` so it
//! drops in as `libwai.so`/`libwai.dylib`/`wai.dll`.
//!
//! This module (feature `ffi`, which `codecs` implies) is the envelope surface:
//! buffers, errors, packing and unpacking `WAI1`/`WAI2`, and SPEC §7.1 selection.
//! It needs no codec library. The codec wrappers are [`crate::ffi_codecs`]
//! (feature `codecs`, re-exported here) and the neural sink is
//! [`crate::ffi_neural`] (features `neural` and `ffi`).
//!
//! ## Status codes (returned by every wai_* function)
//! - `0` = ok
//! - `1` = invalid argument (null pointer where one shouldn't be, etc.)
//! - `2` = encode/decode failed (codec-level error; details in
//!   `wai_last_error`)
//! - `3` = container/envelope error (bad magic, truncated, bad JSON)
//! - `4` = internal panic (a bug in the wrapper — we catch it instead
//!   of unwinding into C, which would be UB)
//! - `5` = a pin was refused (SPEC §3.1); `wai_last_error` starts with the reason
//!   (`prior_not_held`, `form_required`, …)
//! - `6` = `intent = replicate` is not sound for the capability decoded through
//!   (SPEC §7); `wai_last_error` starts with the reason
//! - `7` = no rendition of a `WAI2` envelope can be picked (SPEC §7.1);
//!   `wai_last_error` gives each entry's index and reason
//!
//! ## Memory model
//! All byte buffers crossing the boundary are heap-allocated here and
//! must be freed with `wai_buffer_free`. Errors are returned as a
//! non-zero status code; `wai_last_error` returns a null-terminated
//! UTF-8 string of the most recent error per thread.

use std::cell::RefCell;
use std::ffi::{c_char, c_int, CString};
use std::panic;
use std::slice;


pub const WAI_OK: c_int = 0;
pub const WAI_ERR_INVALID_ARG: c_int = 1;
pub const WAI_ERR_CODEC: c_int = 2;
pub const WAI_ERR_CONTAINER: c_int = 3;
pub const WAI_ERR_PANIC: c_int = 4;
/// A pin was refused (SPEC §3.1); `wai_last_error` starts with the reason.
pub const WAI_ERR_PRIOR: c_int = 5;
/// `intent = replicate` is not sound for the capability decoded through (SPEC §7).
pub const WAI_ERR_UNSOUND_REPLICATE: c_int = 6;
/// No rendition of a `WAI2` envelope can be picked (SPEC §7.1).
pub const WAI_ERR_NO_RENDITION: c_int = 7;

#[cfg(feature = "codecs")]
pub use crate::ffi_codecs::*;

thread_local! {
    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}

pub(crate) fn set_err(e: impl ToString) {
    let cs = CString::new(e.to_string())
        .unwrap_or_else(|_| CString::new("invalid utf-8 error").unwrap());
    LAST_ERROR.with(|c| *c.borrow_mut() = Some(cs));
}

#[repr(C)]
pub struct WaiBuffer {
    pub data: *mut u8,
    pub len: usize,
    pub cap: usize,
}

impl WaiBuffer {
    pub(crate) fn from_vec(mut v: Vec<u8>) -> Self {
        let data = v.as_mut_ptr();
        let len = v.len();
        let cap = v.capacity();
        std::mem::forget(v);
        WaiBuffer { data, len, cap }
    }
    pub(crate) fn empty() -> Self { WaiBuffer { data: std::ptr::null_mut(), len: 0, cap: 0 } }
}

/// Free a buffer previously returned by any `wai_*_encode` /
/// `wai_*_decode` / `wai_envelope_*` call. Safe to call with a null
/// or zero-cap buffer.
///
/// # Safety
/// `buf.data` must have been produced by this crate.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_buffer_free(buf: WaiBuffer) {
    if !buf.data.is_null() && buf.cap > 0 {
        drop(unsafe { Vec::from_raw_parts(buf.data, buf.len, buf.cap) });
    }
}

/// Returns a pointer to the most recent error message for this thread,
/// or null if there is none. The pointer is valid until the next FFI
/// call on this thread that sets an error.
#[unsafe(no_mangle)]
pub extern "C" fn wai_last_error() -> *const c_char {
    LAST_ERROR.with(|c| match &*c.borrow() {
        Some(cs) => cs.as_ptr(),
        None => std::ptr::null(),
    })
}

// ---- generic helpers ------------------------------------------------
pub(crate) unsafe fn input_slice<'a>(data: *const u8, len: usize) -> Option<&'a [u8]> {
    match (data.is_null(), len) {
        (true, 0) => Some(&[]),
        (true, _) => None,                          // null but non-zero len
        (false, _) => Some(unsafe { slice::from_raw_parts(data, len) }),
    }
}

pub(crate) unsafe fn write_buf(out: *mut WaiBuffer, v: Vec<u8>) -> bool {
    if out.is_null() { return false; }
    unsafe { *out = WaiBuffer::from_vec(v); }
    true
}

pub(crate) unsafe fn clear_buf(out: *mut WaiBuffer) {
    if !out.is_null() {
        unsafe { *out = WaiBuffer::empty(); }
    }
}

/// Wraps an FFI body. Validates the `out` buffer pointer is non-null,
/// runs the closure inside `catch_unwind` (so a Rust panic returns
/// `WAI_ERR_PANIC` instead of unwinding into C — which would be UB),
/// and clears `out` to an empty buffer on any error path.
pub(crate) fn ffi_buf_call<F>(out: *mut WaiBuffer, f: F) -> c_int
where F: FnOnce() -> std::result::Result<Vec<u8>, (c_int, String)> + panic::UnwindSafe
{
    if out.is_null() {
        set_err("output buffer pointer is null");
        return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(f) {
        Ok(Ok(v)) => unsafe {
            if write_buf(out, v) { WAI_OK }
            else { set_err("write to out failed"); WAI_ERR_INVALID_ARG }
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe { clear_buf(out); }
            code
        }
        Err(p) => {
            let msg = if let Some(s) = p.downcast_ref::<&str>() { (*s).to_string() }
                      else if let Some(s) = p.downcast_ref::<String>() { s.clone() }
                      else { "panic with non-string payload".into() };
            set_err(format!("internal panic: {msg}"));
            unsafe { clear_buf(out); }
            WAI_ERR_PANIC
        }
    }
}

// ---- envelope: wrap an existing payload in a WAI container ----------
/// The C error for an envelope the emit path refused: a candidate is the
/// caller's argument (`WAI_ERR_INVALID_ARG`), a serialisation failure the
/// container's (`WAI_ERR_CONTAINER`).
fn emit_err(e: crate::container::EmitError) -> (c_int, String) {
    match e {
        crate::container::EmitError::Candidate(_) | crate::container::EmitError::IntPayload { .. } => (WAI_ERR_INVALID_ARG, e.to_string()),
        _ => (WAI_ERR_CONTAINER, e.to_string()),
    }
}

/// What decoding an integer payload will cost (`wai_int_payload_declared`,
/// `extensions/integer-payloads.md` §7, *Memory and work*): the decoded output's
/// bytes (`output_known` 1 where they are filled), the symbols its streams code, and,
/// where `cost_known` is 1, the most bytes the reference decode holds at once beyond
/// the payload and the parsed set (`working_bytes`) and the networks'
/// multiply-accumulates, `macs_hi·2^64 + macs_lo`.
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct WaiIntDeclared {
    pub output_bytes: u64,
    pub output_known: u8,
    pub symbols: u64,
    pub working_bytes: u64,
    pub macs_lo: u64,
    pub macs_hi: u64,
    pub cost_known: u8,
}

impl WaiIntDeclared {
    /// The figures `container::declared_size` reads from the header alone.
    fn from_size(d: crate::container::DeclaredSize) -> Self {
        let macs = d.macs.unwrap_or(0);
        WaiIntDeclared {
            output_bytes: d.output_bytes.unwrap_or(0),
            output_known: d.output_bytes.is_some() as u8,
            symbols: d.symbols,
            working_bytes: d.working_bytes.unwrap_or(0),
            macs_lo: macs as u64,
            macs_hi: (macs >> 64) as u64,
            cost_known: (d.working_bytes.is_some() && d.macs.is_some()) as u8,
        }
    }
}

/// The members of `capability`'s parameter set read from `set_path`: inside it where
/// it is a directory, by the names the set lists them under; the file itself for a set
/// of one file; else the file named for the member beside it. A member that cannot be
/// read is absent, and the cost then refuses with `set_incomplete`.
#[cfg(feature = "neural_int")]
fn read_set(capability: &str, set_path: &std::path::Path) -> std::collections::BTreeMap<String, Vec<u8>> {
    let members = crate::int_payload::set_members(capability);
    members.iter()
        .filter_map(|m| {
            let f = if set_path.is_dir() {
                set_path.join(m)
            } else if members.len() == 1 {
                set_path.to_path_buf()
            } else {
                set_path.parent().map(|d| d.join(m)).unwrap_or_else(|| std::path::PathBuf::from(m))
            };
            std::fs::read(f).ok().map(|b| (m.to_string(), b))
        })
        .collect()
}

/// Every figure of the declared cost, over the set read from `set_path`.
#[cfg(feature = "neural_int")]
fn cost_with_set(capability: &str, payload: &[u8], set_path: &str) -> std::result::Result<WaiIntDeclared, (c_int, String)> {
    use crate::int_payload::{declared_cost, resolve};
    let files = read_set(capability, std::path::Path::new(set_path));
    let view: std::collections::BTreeMap<String, &[u8]> = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect();
    let params = resolve(capability, None, &[crate::pin::HeldBytes::Set { listing: b"", files: &view }], None)
        .map_err(|r| (WAI_ERR_INVALID_ARG, format!("{}: {r}", r.code())))?;
    let c = declared_cost(capability, payload, &params).map_err(|e| (WAI_ERR_INVALID_ARG, e.to_string()))?;
    Ok(WaiIntDeclared {
        output_bytes: c.output_bytes, output_known: 1, symbols: c.symbols, working_bytes: c.working_bytes,
        macs_lo: c.macs as u64, macs_hi: (c.macs >> 64) as u64, cost_known: 1,
    })
}

#[cfg(not(feature = "neural_int"))]
fn cost_with_set(capability: &str, _: &[u8], _: &str) -> std::result::Result<WaiIntDeclared, (c_int, String)> {
    Err((WAI_ERR_INVALID_ARG, format!("{capability}: this build reads no parameter set (feature neural_int)")))
}

/// Read what decoding the payload of an integer capability will cost before decoding
/// it, so a sink can refuse a payload past its own size policy first
/// (`extensions/integer-payloads.md` §7, *Memory and work*). `capability` is the
/// envelope's capability (or the `WAI2` component's). The payload is parsed as a sink
/// parses it: one that is not the format SPEC §5 pins `capability` to is
/// `WAI_ERR_INVALID_ARG`, with its code in `wai_last_error`, and so is a capability
/// with no pinned integer format.
///
/// With `set_path` null, only the header is read: a `wai.neural.int_hyper` image's
/// size is its synthesis network's, so `output_known` is 0 for it, and `cost_known` is
/// 1 only for a `WIV1` with keyframe 0, which runs no network. With `set_path` naming
/// the capability's parameter set — a directory holding its members by the names §9
/// gives, or the file of a one-file set — every figure is filled
/// (`int_payload::declared_cost`), and the set is checked against the payload as a
/// decode checks it first (`set_incomplete`, `model_mismatch`, `geometry_mismatch`,
/// …). The set is read as given, unverified: a pin is verified when the envelope is
/// decoded. A build without `neural_int` reads no set.
///
/// # Safety
/// `capability` NUL-terminated; `payload` valid for `payload_len` bytes; `set_path`
/// null or NUL-terminated UTF-8; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_int_payload_declared(
    capability: *const c_char,
    payload: *const u8, payload_len: usize,
    set_path: *const c_char,
    out: *mut WaiIntDeclared,
) -> c_int {
    if capability.is_null() || out.is_null() {
        set_err("int_payload_declared: capability/out null");
        return WAI_ERR_INVALID_ARG;
    }
    let r = panic::catch_unwind(|| -> std::result::Result<WaiIntDeclared, (c_int, String)> {
        let cap = unsafe { std::ffi::CStr::from_ptr(capability) }.to_string_lossy().to_string();
        let p = unsafe { input_slice(payload, payload_len) }
            .ok_or((WAI_ERR_INVALID_ARG, "payload null + len>0".into()))?;
        let header = match crate::container::declared_size(&cap, p) {
            Ok(Some(d)) => WaiIntDeclared::from_size(d),
            Ok(None) => return Err((WAI_ERR_INVALID_ARG, format!("{cap} has no pinned integer payload format"))),
            Err(e) => return Err((WAI_ERR_INVALID_ARG, e.to_string())),
        };
        if set_path.is_null() {
            return Ok(header);
        }
        let set = unsafe { std::ffi::CStr::from_ptr(set_path) }.to_str()
            .map_err(|_| (WAI_ERR_INVALID_ARG, "set_path is not UTF-8".to_string()))?;
        cost_with_set(&cap, p, set)
    });
    match r {
        Ok(Ok(d)) => { unsafe { *out = d; } WAI_OK }
        Ok(Err((code, msg))) => { set_err(msg); code }
        Err(_) => { set_err("internal panic in int_payload_declared"); WAI_ERR_PANIC }
    }
}

/// Builds a complete WAI file from a codec-produced payload + manifest
/// fields. `manifest_json` is a UTF-8 JSON object overriding/supplying
/// any fields; `payload` is the bytes from one of the codec encoders.
/// Refuses (`WAI_ERR_INVALID_ARG`) a manifest that names a candidate capability
/// as its capability or fallback, which an encoder MUST NOT emit (SPEC §5,
/// *Candidate capabilities*), and a payload that is not the format SPEC §5 pins its
/// integer capability or fallback to (*Payloads of the WAI-native integer codecs*):
/// it emits through `Wai::to_emit_bytes`.
///
/// # Safety
/// `manifest_json` NUL-terminated; `payload` valid for `payload_len` bytes; `out` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_pack(
    manifest_json: *const c_char,
    payload: *const u8, payload_len: usize,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        if manifest_json.is_null() {
            return Err((WAI_ERR_INVALID_ARG, "null manifest_json".into()));
        }
        let s = unsafe { std::ffi::CStr::from_ptr(manifest_json) }
            .to_string_lossy().to_string();
        let manifest: crate::container::Manifest = serde_json::from_str(&s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("manifest parse: {e}")))?;
        let pl = unsafe { input_slice(payload, payload_len) }
            .ok_or((WAI_ERR_INVALID_ARG, "payload null + len>0".into()))?
            .to_vec();
        crate::container::Wai::new(manifest, pl).to_emit_bytes().map_err(emit_err)
    })
}

// ---- v1.1: multi-rendition envelope FFI ------------------------
// Multi-rendition is a separate magic ("WAI2") with multiple payloads
// in one envelope. The C ABI keeps it consumer-friendly by serializing
// the rendition metadata (capability + kind + payload offset/length)
// as a JSON sidecar and packing every rendition payload contiguously
// into a single buffer — the caller indexes into the buffer using the
// JSON. No array-of-structs ownership games.

/// Detect WAI version from a byte stream. Returns 1 (WAI1), 2 (WAI2),
/// or 0 with an error message in `wai_last_error` for unknown magic.
///
/// # Safety
/// `bytes` valid for `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_detect_version(
    bytes: *const u8, len: usize,
) -> c_int {
    let s = match unsafe { input_slice(bytes, len) } {
        Some(s) => s,
        None => { set_err("bytes null + len>0"); return 0; }
    };
    match crate::container::detect_version(s) {
        Ok(v) => v as c_int,
        Err(e) => { set_err(e); 0 }
    }
}

/// Pack a WAI2 multi-rendition envelope.
///
/// - `manifest_json` must declare a `renditions` array of length `n`.
/// - `payload_data` is an array of `n` byte-buffer pointers.
/// - `payload_lens` is the matching array of `n` lengths.
/// - The i-th payload pairs with `manifest.renditions[i]`.
///
/// Refuses (`WAI_ERR_INVALID_ARG`) a table with a component, in either role,
/// whose capability is a candidate (SPEC §5, *Candidate capabilities*), or whose
/// payload is not the format SPEC §5 pins its integer capability to: it emits
/// through `WaiMulti::to_emit_bytes`.
///
/// # Safety
/// All array pointers must point to valid memory of the declared length.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_pack_multi(
    manifest_json: *const c_char,
    payload_data: *const *const u8,
    payload_lens: *const usize,
    n_payloads: usize,
    out: *mut WaiBuffer,
) -> c_int {
    ffi_buf_call(out, || {
        if manifest_json.is_null() {
            return Err((WAI_ERR_INVALID_ARG, "null manifest_json".into()));
        }
        let mj = unsafe { std::ffi::CStr::from_ptr(manifest_json) }
            .to_string_lossy().to_string();
        let manifest: crate::container::ManifestV2 = serde_json::from_str(&mj)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("manifest parse: {e}")))?;
        if manifest.renditions.len() != n_payloads {
            return Err((WAI_ERR_INVALID_ARG, format!(
                "manifest has {} renditions but n_payloads={n_payloads}",
                manifest.renditions.len())));
        }
        if n_payloads > 0 && (payload_data.is_null() || payload_lens.is_null()) {
            return Err((WAI_ERR_INVALID_ARG,
                "payload_data/payload_lens null with n_payloads>0".into()));
        }
        // Collect rendition payloads. Every member of the manifest — the component
        // identity members, and any a later revision defines — is kept as written.
        let mut payloads: Vec<Vec<u8>> = Vec::with_capacity(n_payloads);
        for i in 0..n_payloads {
            let ptr = unsafe { *payload_data.add(i) };
            let len = unsafe { *payload_lens.add(i) };
            let s = unsafe { input_slice(ptr, len) }
                .ok_or((WAI_ERR_INVALID_ARG,
                        format!("payload {i} null with len>0")))?;
            payloads.push(s.to_vec());
        }
        crate::container::WaiMulti::from_parts(manifest, payloads)
            .map_err(|e| (WAI_ERR_INVALID_ARG, e.to_string()))?
            .to_emit_bytes().map_err(emit_err)
    })
}

/// Parse a WAI2 envelope. Writes three outputs:
///
/// - `out_manifest` — the manifest JSON exactly as the envelope carries it
///   (UTF-8 bytes, no NUL), so members this library does not model survive
/// - `out_renditions_json` — a JSON array with one object per component:
///   `capability`, `kind`, `offset` (into `out_payload_block`), `length`, and
///   `selectable` (whether SPEC §7.1 may pick it: a companion, or a companion-only
///   capability, never is), plus `id`, `sha256`, `prior`, `role`, `companion_of`
///   and `companion_of_id` when the entry carries them. A C sink selects with
///   `wai_envelope_select` rather than by hand.
/// - `out_payload_block` — every rendition's payload bytes, packed
///   contiguously. The caller indexes via the JSON above.
///
/// This avoids exposing a C struct array (and the ownership pain that
/// comes with it). Any JSON parser the consumer already has decodes
/// the rendition table in two lines.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out_manifest` writable; `out_renditions_json` writable; `out_payload_block` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_unpack_multi(
    bytes: *const u8, len: usize,
    out_manifest: *mut WaiBuffer,
    out_renditions_json: *mut WaiBuffer,
    out_payload_block: *mut WaiBuffer,
) -> c_int {
    if out_manifest.is_null() || out_renditions_json.is_null()
        || out_payload_block.is_null()
    {
        set_err("unpack_multi: one of the out pointers is null");
        return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(|| -> std::result::Result<(Vec<u8>, Vec<u8>, Vec<u8>), (c_int, String)> {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        let m = crate::container::WaiMulti::from_bytes(s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
        let mb = crate::container::manifest_bytes(s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?
            .to_vec();
        // build rendition table JSON + concatenated payload block
        let mut table: Vec<serde_json::Value> = Vec::with_capacity(m.renditions.len());
        let mut block: Vec<u8> = Vec::new();
        for (meta, payload) in m.manifest.renditions.iter().zip(m.renditions.iter()) {
            let mut e = serde_json::json!({
                "capability": meta.capability,
                "kind":       meta.kind,
                "offset":     block.len(),
                "length":     payload.len(),
                "selectable": meta.is_selectable(),
            });
            let o = e.as_object_mut().unwrap();
            let mut put = |k: &str, v: Option<serde_json::Value>| if let Some(v) = v { o.insert(k.into(), v); };
            put("id", meta.id.clone().map(Into::into));
            put("sha256", meta.sha256.clone().map(Into::into));
            put("prior", meta.prior.as_ref().and_then(|p| serde_json::to_value(p).ok()));
            put("role", meta.role.clone().map(Into::into));
            put("companion_of", meta.companion_of.clone().map(Into::into));
            put("companion_of_id", meta.companion_of_id.clone().map(Into::into));
            table.push(e);
            block.extend_from_slice(payload);
        }
        let tj = serde_json::to_vec(&serde_json::Value::Array(table))
            .map_err(|e| (WAI_ERR_CONTAINER, format!("table serialize: {e}")))?;
        Ok((mb, tj, block))
    }) {
        Ok(Ok((mb, tj, block))) => unsafe {
            *out_manifest = WaiBuffer::from_vec(mb);
            *out_renditions_json = WaiBuffer::from_vec(tj);
            *out_payload_block = WaiBuffer::from_vec(block);
            WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe {
                clear_buf(out_manifest);
                clear_buf(out_renditions_json);
                clear_buf(out_payload_block);
            }
            code
        }
        Err(_) => {
            set_err("internal panic in envelope_unpack_multi");
            unsafe {
                clear_buf(out_manifest);
                clear_buf(out_renditions_json);
                clear_buf(out_payload_block);
            }
            WAI_ERR_PANIC
        }
    }
}

/// Parses a WAI envelope. Writes the manifest JSON exactly as the envelope
/// carries it to `out_manifest` (so `security`, and members this library does not
/// model, survive) and the raw payload to `out_payload`.
///
/// # Safety
/// `bytes` valid for `len` bytes; `out_manifest` writable; `out_payload` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_unpack(
    bytes: *const u8, len: usize,
    out_manifest: *mut WaiBuffer, out_payload: *mut WaiBuffer,
) -> c_int {
    if out_manifest.is_null() || out_payload.is_null() {
        set_err("envelope_unpack: out_manifest/out_payload null");
        return WAI_ERR_INVALID_ARG;
    }
    match panic::catch_unwind(|| -> std::result::Result<(Vec<u8>, Vec<u8>), (c_int, String)> {
        let s = unsafe { input_slice(bytes, len) }
            .ok_or((WAI_ERR_INVALID_ARG, "bytes null + len>0".into()))?;
        let wai = crate::container::Wai::from_bytes(s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
        let mj = crate::container::manifest_bytes(s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?
            .to_vec();
        Ok((mj, wai.payload))
    }) {
        Ok(Ok((mj, pl))) => unsafe {
            *out_manifest = WaiBuffer::from_vec(mj);
            *out_payload = WaiBuffer::from_vec(pl);
            WAI_OK
        }
        Ok(Err((code, msg))) => {
            set_err(msg);
            unsafe { clear_buf(out_manifest); clear_buf(out_payload); }
            code
        }
        Err(_) => {
            set_err("internal panic in envelope_unpack");
            unsafe { clear_buf(out_manifest); clear_buf(out_payload); }
            WAI_ERR_PANIC
        }
    }
}

/// SPEC §7.1 selection for a `WAI2` envelope at a sink that advertises the
/// capabilities in `advertised_json` (a NUL-terminated JSON array of capability
/// strings). Writes the index of the picked rendition to `out_index`: the first
/// entry, in SPEC §4's energy order (table order, but an entry whose capability is
/// learned and carries no energy budget after every other), that is selectable (never a companion, nor a capability that is not
/// dispatchable under SPEC §4: companion-only, a candidate, a record or a
/// derivation), advertised, and whose payload matches its `sha256`. This call holds no
/// parameter set, so an entry whose pin applies is skipped (`prior_not_held`); a
/// sink that holds parameter sets decodes through `wai_neural_decode_ex`, which
/// verifies each pin. Returns `WAI_ERR_NO_RENDITION` when nothing is picked, with
/// each entry's index and reason in `wai_last_error`.
///
/// # Safety
/// `envelope` valid for `envelope_len` bytes; `advertised_json` NUL-terminated;
/// `out_index` writable.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wai_envelope_select(
    envelope: *const u8, envelope_len: usize,
    advertised_json: *const c_char,
    out_index: *mut u32,
) -> c_int {
    if out_index.is_null() || advertised_json.is_null() {
        set_err("envelope_select: out_index/advertised_json null");
        return WAI_ERR_INVALID_ARG;
    }
    let r = panic::catch_unwind(|| -> std::result::Result<u32, (c_int, String)> {
        let s = unsafe { input_slice(envelope, envelope_len) }
            .ok_or((WAI_ERR_INVALID_ARG, "envelope null + len>0".into()))?;
        let adv = unsafe { std::ffi::CStr::from_ptr(advertised_json) }.to_string_lossy().to_string();
        let advertised: Vec<String> = serde_json::from_str(&adv)
            .map_err(|e| (WAI_ERR_INVALID_ARG, format!("advertised_json: {e}")))?;
        let m = crate::container::WaiMulti::from_bytes(s)
            .map_err(|e| (WAI_ERR_CONTAINER, format!("envelope parse: {e}")))?;
        match m.pick(|c| advertised.iter().any(|a| a == c), crate::container::no_parameter_sets()) {
            Ok(p) => Ok(p.index as u32),
            Err(skips) => Err((WAI_ERR_NO_RENDITION, skips.iter()
                .map(|(i, k)| format!("{i}: {k}")).collect::<Vec<_>>().join("; "))),
        }
    });
    match r {
        Ok(Ok(i)) => { unsafe { *out_index = i; } WAI_OK }
        Ok(Err((code, msg))) => { set_err(msg); code }
        Err(_) => { set_err("internal panic in envelope_select"); WAI_ERR_PANIC }
    }
}
