//! The project-cdylib descriptor contract (C13/C16, cycle 0102).
//!
//! A research project compiles to a cdylib exporting ONE symbol,
//! `AURA_PROJECT`, a [`ProjectDescriptor`]. The descriptor has two ABI tiers:
//! a **C tier** (`magic`, `descriptor_version`, the version stamps, the
//! namespace — all C-compatible field types) the host validates BEFORE
//! trusting anything, and a **Rust tier** (the vocabulary resolver and the
//! enumerable type-id list) the host touches only after both stamps match.
//! The stamp cannot certify the channel it rides on, so it must not itself
//! depend on the Rust ABI it certifies.
//!
//! The stamps are baked at *aura-core compile time* — per consuming build —
//! so host and dylib each carry their own side's truth.

use crate::node::PrimitiveBuilder;

/// First 8 bytes of the C tier; ASCII "AURAPROJ".
pub const AURA_DESCRIPTOR_MAGIC: u64 = 0x4155_5241_5052_4F4A;
/// Bumped on any layout change of [`ProjectDescriptor`].
pub const AURA_DESCRIPTOR_VERSION: u32 = 1;
/// The one exported symbol name (NUL-terminated for the symbol lookup).
pub const AURA_PROJECT_SYMBOL: &[u8] = b"AURA_PROJECT\0";

/// The compiling rustc's `--version` line (via build.rs).
pub const RUSTC_VERSION: &str = env!("AURA_RUSTC_VERSION");
/// This aura-core's crate version.
pub const CORE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// C-ABI string slice: pointer + length over `'static` UTF-8 bytes.
/// No NUL convention needed; readable without trusting the Rust ABI.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct StrSlice {
    pub ptr: *const u8,
    pub len: usize,
}

impl StrSlice {
    pub const fn new(s: &'static str) -> Self {
        Self { ptr: s.as_ptr(), len: s.len() }
    }

    /// Read the slice back as UTF-8.
    ///
    /// # Safety
    /// `ptr`/`len` must describe live UTF-8 bytes — true for any descriptor
    /// built by [`aura_project!`](crate::aura_project), whose strings are
    /// `'static` and whose library is loaded-and-held (never unloaded).
    pub unsafe fn as_str(&self) -> Option<&str> {
        if self.ptr.is_null() {
            return None;
        }
        let bytes = unsafe { core::slice::from_raw_parts(self.ptr, self.len) };
        core::str::from_utf8(bytes).ok()
    }
}

/// The exported project descriptor. Field ORDER is ABI: C tier first,
/// Rust tier last; validated front-to-back.
#[repr(C)]
pub struct ProjectDescriptor {
    // ---- C tier: validated BEFORE any Rust-ABI field is touched ----
    pub magic: u64,
    pub descriptor_version: u32,
    pub rustc_version: StrSlice,
    pub aura_core_version: StrSlice,
    pub namespace: StrSlice,
    // ---- Rust tier: only after both stamps match ----
    pub vocabulary: fn(&str) -> Option<PrimitiveBuilder>,
    pub type_ids: fn() -> &'static [&'static str],
}

// The raw pointers are to 'static data; the fn pointers are Sync by nature.
unsafe impl Sync for ProjectDescriptor {}

/// Declare a project crate's export in one place. Emits the `AURA_PROJECT`
/// static with this build's stamps baked in.
#[macro_export]
macro_rules! aura_project {
    (namespace: $ns:literal, vocabulary: $vocab:path, type_ids: $ids:path $(,)?) => {
        #[unsafe(no_mangle)]
        pub static AURA_PROJECT: $crate::project::ProjectDescriptor =
            $crate::project::ProjectDescriptor {
                magic: $crate::project::AURA_DESCRIPTOR_MAGIC,
                descriptor_version: $crate::project::AURA_DESCRIPTOR_VERSION,
                rustc_version: $crate::project::StrSlice::new(
                    $crate::project::RUSTC_VERSION,
                ),
                aura_core_version: $crate::project::StrSlice::new(
                    $crate::project::CORE_VERSION,
                ),
                namespace: $crate::project::StrSlice::new($ns),
                vocabulary: $vocab,
                type_ids: $ids,
            };
    };
}

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

    fn fake_vocab(type_id: &str) -> Option<PrimitiveBuilder> {
        let _ = type_id;
        None
    }
    fn fake_ids() -> &'static [&'static str] {
        &["t::A"]
    }

    aura_project! {
        namespace: "t",
        vocabulary: fake_vocab,
        type_ids: fake_ids,
    }

    #[test]
    fn stamps_are_baked_and_nonempty() {
        assert!(RUSTC_VERSION.starts_with("rustc "));
        assert!(!CORE_VERSION.is_empty());
    }

    #[test]
    fn str_slice_round_trips() {
        let s = StrSlice::new("hello");
        assert_eq!(unsafe { s.as_str() }, Some("hello"));
    }

    /// `as_str` refuses a null pointer instead of dereferencing it — the
    /// loader's boundary-safety primitive for a descriptor field it hasn't
    /// otherwise validated yet.
    #[test]
    fn str_slice_null_ptr_is_none() {
        let s = StrSlice { ptr: std::ptr::null(), len: 0 };
        assert_eq!(unsafe { s.as_str() }, None);
    }

    /// `as_str` refuses non-UTF-8 bytes rather than producing a mangled or
    /// panicking read, so a corrupt/foreign descriptor field degrades to
    /// `None` instead of undefined behaviour downstream.
    #[test]
    fn str_slice_invalid_utf8_is_none() {
        static BAD: [u8; 2] = [0xFF, 0xFE];
        let s = StrSlice { ptr: BAD.as_ptr(), len: BAD.len() };
        assert_eq!(unsafe { s.as_str() }, None);
    }

    #[test]
    fn macro_emits_a_valid_descriptor() {
        let d = &AURA_PROJECT;
        assert_eq!(d.magic, AURA_DESCRIPTOR_MAGIC);
        assert_eq!(d.descriptor_version, AURA_DESCRIPTOR_VERSION);
        assert_eq!(unsafe { d.namespace.as_str() }, Some("t"));
        assert_eq!(unsafe { d.rustc_version.as_str() }, Some(RUSTC_VERSION));
        assert_eq!(unsafe { d.aura_core_version.as_str() }, Some(CORE_VERSION));
        assert!((d.vocabulary)("nope").is_none());
        assert_eq!((d.type_ids)(), &["t::A"]);
    }
}
