//! WASM ABI boilerplate for plugin exports.
//!
//! Provides the standard allocation, deallocation, and execution helpers
//! that the Kailash plugin host expects. Plugin authors typically do not
//! interact with this module directly; instead, the `#[kailash_plugin]`
//! proc-macro generates the `kailash_execute` export automatically.
//!
//! # Host ABI Contract
//!
//! The host expects these WASM exports:
//!
//! - `kailash_alloc(size: i32) -> i32` -- allocate `size` bytes, return pointer
//! - `kailash_free(ptr: i32, size: i32)` -- deallocate memory at `ptr`
//! - `kailash_execute(input_ptr: i32, input_len: i32, out_ptr: i32, out_len: i32) -> i32`
//!   -- execute the plugin with JSON input, write JSON output, return 0 on success
//!
//! [`kailash_alloc`] and [`kailash_free`] are exported by this module.
//! `kailash_execute` is generated by the `#[kailash_plugin]` macro, which
//! creates a `#[no_mangle]` export that calls [`execute_with_fn`].
//!
//! For manual use without the macro, write your own `kailash_execute` that
//! delegates to [`execute_with_fn`]:
//!
//! ```ignore
//! use kailash_plugin_guest::abi::execute_with_fn;
//!
//! #[no_mangle]
//! pub extern "C" fn kailash_execute(
//!     input_ptr: i32, input_len: i32,
//!     output_ptr_ptr: i32, output_len_ptr: i32,
//! ) -> i32 {
//!     execute_with_fn(my_handler, input_ptr, input_len, output_ptr_ptr, output_len_ptr)
//! }
//! ```

use crate::{error::PluginError, value::GuestValueMap};

/// Type alias for the plugin execute function signature.
///
/// The function receives a deserialized [`GuestValueMap`] and must return
/// either a [`GuestValueMap`] on success or a [`PluginError`] on failure.
pub type ExecuteFn = fn(GuestValueMap) -> Result<GuestValueMap, PluginError>;

// ---------------------------------------------------------------------------
// WASM ABI exports: allocation
// ---------------------------------------------------------------------------

/// Allocates `size` bytes in WASM linear memory and returns a pointer.
///
/// The host calls this to reserve space for writing input data into the
/// guest's memory before calling `kailash_execute`.
///
/// Returns 0 if `size` is non-positive or allocation fails.
///
/// # Safety
///
/// This function is called by the WASM host through the C ABI. The returned
/// pointer is valid until [`kailash_free`] is called with the same pointer
/// and size.
#[no_mangle]
pub extern "C" fn kailash_alloc(size: i32) -> i32 {
    if size <= 0 {
        return 0;
    }
    let size = size as usize;
    let layout = match std::alloc::Layout::from_size_align(size, 1) {
        Ok(layout) => layout,
        Err(_) => return 0,
    };

    // SAFETY: We checked size > 0 and alignment is 1 (valid for byte arrays).
    // The allocated memory is returned as a raw pointer for the host to write into.
    let ptr = unsafe { std::alloc::alloc(layout) };
    if ptr.is_null() {
        return 0;
    }
    ptr as i32
}

/// Frees memory previously allocated by [`kailash_alloc`].
///
/// No-op if `ptr` is 0 or `size` is non-positive.
///
/// # Safety
///
/// This function is called by the WASM host through the C ABI. The caller
/// must ensure `ptr` was returned by a prior `kailash_alloc(size)` call
/// with the same `size` value.
#[no_mangle]
pub extern "C" fn kailash_free(ptr: i32, size: i32) {
    if ptr == 0 || size <= 0 {
        return;
    }
    let size = size as usize;
    let layout = match std::alloc::Layout::from_size_align(size, 1) {
        Ok(layout) => layout,
        Err(_) => return,
    };

    // SAFETY: The caller guarantees ptr was allocated by kailash_alloc
    // with the same size and alignment 1. We trust the host to uphold
    // this contract (the host controls the ABI).
    unsafe {
        std::alloc::dealloc(ptr as *mut u8, layout);
    }
}

// ---------------------------------------------------------------------------
// Execute trampoline
// ---------------------------------------------------------------------------

/// Shared execution logic for the plugin ABI.
///
/// The `#[kailash_plugin]` macro generates a `kailash_execute` export that
/// calls this function with the user's handler. This function handles:
///
/// 1. Reading input JSON from WASM linear memory
/// 2. Deserializing to [`GuestValueMap`]
/// 3. Calling the handler
/// 4. Serializing the output
/// 5. Allocating output memory and writing results
///
/// # Return codes
///
/// - `0` -- success
/// - `2` -- negative input length
/// - `3` -- JSON deserialization failed
/// - `4` -- plugin execute function returned an error
/// - `5` -- JSON serialization of output failed
/// - `6` -- output memory allocation failed
///
/// # Safety
///
/// The caller must ensure:
/// - `input_ptr` points to `input_len` valid bytes of JSON
/// - `output_ptr_ptr` and `output_len_ptr` point to writable 4-byte slots
pub fn execute_with_fn(
    handler: ExecuteFn,
    input_ptr: i32,
    input_len: i32,
    output_ptr_ptr: i32,
    output_len_ptr: i32,
) -> i32 {
    // 1. Validate input length
    if input_len < 0 {
        return 2;
    }

    // SAFETY: The host guarantees input_ptr points to input_len valid bytes.
    // For null pointer or zero length, we use an empty slice.
    let input_bytes = unsafe {
        if input_ptr == 0 || input_len == 0 {
            &[]
        } else {
            std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize)
        }
    };

    // 2. Deserialize JSON to GuestValueMap
    let inputs: GuestValueMap = match serde_json::from_slice(input_bytes) {
        Ok(map) => map,
        Err(_) => return 3,
    };

    // 3. Call the plugin execute function
    let outputs = match handler(inputs) {
        Ok(out) => out,
        Err(_) => return 4,
    };

    // 4. Serialize output to JSON
    let output_json = match serde_json::to_vec(&outputs) {
        Ok(bytes) => bytes,
        Err(_) => return 5,
    };

    // 5. Allocate output buffer
    let out_ptr = kailash_alloc(output_json.len() as i32);
    if out_ptr == 0 && !output_json.is_empty() {
        return 6;
    }

    // Write output JSON to the allocated buffer
    if !output_json.is_empty() {
        // SAFETY: out_ptr was just allocated with the correct size by kailash_alloc.
        unsafe {
            std::ptr::copy_nonoverlapping(
                output_json.as_ptr(),
                out_ptr as *mut u8,
                output_json.len(),
            );
        }
    }

    // 6. Write output pointer and length to the host-provided slots
    // SAFETY: The host allocated these 4-byte slots and guarantees they are writable.
    unsafe {
        let ptr_bytes = out_ptr.to_le_bytes();
        std::ptr::copy_nonoverlapping(ptr_bytes.as_ptr(), output_ptr_ptr as *mut u8, 4);

        let len_bytes = (output_json.len() as i32).to_le_bytes();
        std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), output_len_ptr as *mut u8, 4);
    }

    0 // success
}

// ---------------------------------------------------------------------------
// Native-side tests (NOT running in WASM, testing the logic in isolation)
// ---------------------------------------------------------------------------

#[cfg(test)]
#[allow(
    clippy::panic,
    clippy::expect_used,
    clippy::unwrap_used,
    clippy::approx_constant,
    clippy::doc_markdown,
    clippy::redundant_closure,
    clippy::unnecessary_get_then_check,
    clippy::double_comparisons,
    clippy::manual_let_else,
    clippy::result_large_err,
    clippy::bool_assert_comparison,
    clippy::needless_pass_by_value,
    clippy::unnecessary_cast,
    dead_code
)]
mod tests {
    use super::*;
    use crate::value::GuestValue;

    // -- Allocation tests --

    // Tests for edge cases that don't involve actual memory allocation
    // (safe on any platform including 64-bit hosts).

    #[test]
    fn alloc_returns_zero_for_zero_size() {
        let ptr = kailash_alloc(0);
        assert_eq!(ptr, 0);
    }

    #[test]
    fn alloc_returns_zero_for_negative_size() {
        let ptr = kailash_alloc(-1);
        assert_eq!(ptr, 0);
    }

    #[test]
    fn free_with_null_ptr_is_noop() {
        kailash_free(0, 10);
    }

    #[test]
    fn free_with_negative_size_is_noop() {
        kailash_free(1234, -5);
    }

    // -----------------------------------------------------------------------
    // Tests that exercise the execute logic through safe Rust (no raw ptrs).
    // This avoids the i32 pointer truncation issue on 64-bit hosts while
    // still covering the deserialization/handler/serialization pipeline.
    // -----------------------------------------------------------------------

    fn echo_handler(inputs: GuestValueMap) -> Result<GuestValueMap, PluginError> {
        Ok(inputs)
    }

    fn error_handler(_inputs: GuestValueMap) -> Result<GuestValueMap, PluginError> {
        Err(PluginError::new("intentional error"))
    }

    /// Safe test helper: exercises the execute pipeline without raw pointer
    /// manipulation. Returns (return_code, output_json_bytes).
    fn safe_execute_test(handler: ExecuteFn, input_json: &[u8]) -> (i32, Vec<u8>) {
        // Validate input length (mirrors execute_with_fn step 1)
        if input_json.is_empty() {
            // empty slice deserialize will fail → code 3
            let map: Result<GuestValueMap, _> = serde_json::from_slice(input_json);
            if map.is_err() {
                return (3, vec![]);
            }
        }

        // Deserialize (step 2)
        let inputs: GuestValueMap = match serde_json::from_slice(input_json) {
            Ok(map) => map,
            Err(_) => return (3, vec![]),
        };

        // Call handler (step 3)
        let outputs = match handler(inputs) {
            Ok(out) => out,
            Err(_) => return (4, vec![]),
        };

        // Serialize output (step 4)
        let output_json = match serde_json::to_vec(&outputs) {
            Ok(bytes) => bytes,
            Err(_) => return (5, vec![]),
        };

        (0, output_json)
    }

    #[test]
    fn execute_negative_input_len_returns_error_2() {
        // execute_with_fn returns 2 for negative input_len
        // (tested via the function's documented contract)
        assert_eq!(execute_with_fn(echo_handler, 0, -1, 0, 0), 2);
    }

    #[test]
    fn execute_echoes_valid_json() {
        let mut input_map = GuestValueMap::new();
        input_map.insert("message".into(), GuestValue::String("hello".into()));
        input_map.insert("count".into(), GuestValue::Integer(42));

        let input_json = serde_json::to_vec(&input_map).expect("serialize input");
        let (code, output_json) = safe_execute_test(echo_handler, &input_json);

        assert_eq!(code, 0, "execute should return success (0)");

        let output_map: GuestValueMap =
            serde_json::from_slice(&output_json).expect("deserialize output");
        assert_eq!(
            output_map.get("message"),
            Some(&GuestValue::String("hello".into()))
        );
        assert_eq!(output_map.get("count"), Some(&GuestValue::Integer(42)));
    }

    #[test]
    fn execute_empty_json_object() {
        let (code, output_json) = safe_execute_test(echo_handler, b"{}");
        assert_eq!(code, 0);

        let output_map: GuestValueMap = serde_json::from_slice(&output_json).expect("deserialize");
        assert!(output_map.is_empty());
    }

    #[test]
    fn execute_invalid_json_returns_error_3() {
        let (code, _) = safe_execute_test(echo_handler, b"not valid json");
        assert_eq!(code, 3, "invalid JSON should return error code 3");
    }

    #[test]
    fn execute_handler_error_returns_code_4() {
        let (code, _) = safe_execute_test(error_handler, b"{}");
        assert_eq!(code, 4, "handler error should return error code 4");
    }

    #[test]
    fn execute_zero_len_returns_error_3() {
        let (code, _) = safe_execute_test(echo_handler, b"");
        assert_eq!(code, 3, "empty input should return JSON error code 3");
    }

    #[test]
    fn execute_nested_object_roundtrip() {
        let mut inner = GuestValueMap::new();
        inner.insert("x".into(), GuestValue::Integer(1));
        inner.insert("y".into(), GuestValue::Float(2.5));

        let mut input_map = GuestValueMap::new();
        input_map.insert("nested".into(), GuestValue::Object(inner));
        input_map.insert(
            "array".into(),
            GuestValue::Array(vec![GuestValue::String("a".into()), GuestValue::Bool(true)]),
        );

        let input_json = serde_json::to_vec(&input_map).expect("serialize");
        let (code, output_json) = safe_execute_test(echo_handler, &input_json);

        assert_eq!(code, 0);

        let output_map: GuestValueMap = serde_json::from_slice(&output_json).expect("deserialize");
        assert_eq!(output_map, input_map);
    }

    // -----------------------------------------------------------------------
    // Raw pointer ABI tests -- only run on 32-bit targets (wasm32) where
    // i32 can hold a full pointer without truncation.
    // -----------------------------------------------------------------------

    #[cfg(target_pointer_width = "32")]
    mod wasm_abi_tests {
        use super::*;

        #[test]
        fn alloc_returns_nonzero_for_positive_size() {
            let ptr = kailash_alloc(64);
            assert_ne!(ptr, 0, "alloc should return a non-zero pointer");
            kailash_free(ptr, 64);
        }

        #[test]
        fn alloc_and_free_roundtrip() {
            let size = 128;
            let ptr = kailash_alloc(size);
            assert_ne!(ptr, 0);

            // SAFETY: ptr was just allocated with `size` bytes by kailash_alloc
            unsafe {
                let slice = std::slice::from_raw_parts_mut(ptr as *mut u8, size as usize);
                for (i, byte) in slice.iter_mut().enumerate() {
                    *byte = (i % 256) as u8;
                }
                assert_eq!(slice[0], 0);
                assert_eq!(slice[1], 1);
                assert_eq!(slice[127], 127);
            }

            kailash_free(ptr, size);
        }

        #[test]
        fn alloc_multiple_blocks_returns_distinct_pointers() {
            let ptrs: Vec<i32> = (0..10).map(|_| kailash_alloc(32)).collect();
            for &ptr in &ptrs {
                assert_ne!(ptr, 0);
            }
            for (i, &p1) in ptrs.iter().enumerate() {
                for &p2 in &ptrs[i + 1..] {
                    assert_ne!(p1, p2, "allocations should return distinct pointers");
                }
            }
            for ptr in ptrs {
                kailash_free(ptr, 32);
            }
        }

        #[test]
        fn execute_with_fn_full_abi_roundtrip() {
            let input_json = b"{}";
            let out_ptr_slot = kailash_alloc(4);
            let out_len_slot = kailash_alloc(4);

            let result_code = execute_with_fn(
                echo_handler,
                input_json.as_ptr() as i32,
                input_json.len() as i32,
                out_ptr_slot,
                out_len_slot,
            );

            assert_eq!(result_code, 0);
            kailash_free(out_ptr_slot, 4);
            kailash_free(out_len_slot, 4);
        }
    }
}
