use std::{
    os::unix::process::CommandExt,
    process::Command,
    sync::{LazyLock, Mutex},
};

const SUCCESS: i32 = 0;
const INVALID_CHUNK_SIZE: i32 = -1;
const INVALID_UTF8: i32 = -2;
const MISSING_ARGUMENTS: i32 = -3;
const UNFINISHED_ARGUMENT: i32 = -4;
const STATE_UNAVAILABLE: i32 = -5;
const UNKNOWN_OS_ERROR: i32 = -6;
const MAX_CHUNK_BYTES: u32 = u64::BITS / u8::BITS;

#[derive(Default)]
struct Invocation {
    arguments: Vec<String>,
    argument_bytes: Vec<u8>,
}

static INVOCATION: LazyLock<Mutex<Invocation>> =
    LazyLock::new(|| Mutex::new(Invocation::default()));

// Java's Foreign Function and Memory API resolves these functions by their
// exact exported symbol names.
#[expect(
    unsafe_code,
    reason = "native linking requires unmangled exported symbols"
)]
#[unsafe(no_mangle)]
pub extern "C" fn jar_wrapper_exec_reset() -> i32 {
    let Ok(mut invocation) = INVOCATION.lock() else {
        return STATE_UNAVAILABLE;
    };
    invocation.arguments.clear();
    invocation.argument_bytes.clear();
    SUCCESS
}

#[expect(
    unsafe_code,
    reason = "native linking requires unmangled exported symbols"
)]
#[unsafe(no_mangle)]
pub extern "C" fn jar_wrapper_exec_append(chunk: u64, byte_count: u32) -> i32 {
    if byte_count == 0 || byte_count > MAX_CHUNK_BYTES {
        return INVALID_CHUNK_SIZE;
    }
    let Ok(mut invocation) = INVOCATION.lock() else {
        return STATE_UNAVAILABLE;
    };
    let Ok(byte_count) = usize::try_from(byte_count) else {
        return INVALID_CHUNK_SIZE;
    };
    invocation
        .argument_bytes
        .extend_from_slice(&chunk.to_le_bytes()[..byte_count]);
    SUCCESS
}

#[expect(
    unsafe_code,
    reason = "native linking requires unmangled exported symbols"
)]
#[unsafe(no_mangle)]
pub extern "C" fn jar_wrapper_exec_end_argument() -> i32 {
    let Ok(mut invocation) = INVOCATION.lock() else {
        return STATE_UNAVAILABLE;
    };
    let Ok(argument) = String::from_utf8(std::mem::take(&mut invocation.argument_bytes)) else {
        return INVALID_UTF8;
    };
    invocation.arguments.push(argument);
    SUCCESS
}

#[expect(
    unsafe_code,
    reason = "native linking requires unmangled exported symbols"
)]
#[unsafe(no_mangle)]
pub extern "C" fn jar_wrapper_exec_run() -> i32 {
    let Ok(mut invocation) = INVOCATION.lock() else {
        return STATE_UNAVAILABLE;
    };
    if !invocation.argument_bytes.is_empty() {
        return UNFINISHED_ARGUMENT;
    }
    if invocation.arguments.is_empty() {
        return MISSING_ARGUMENTS;
    }
    let arguments = std::mem::take(&mut invocation.arguments);
    drop(invocation);

    let error = Command::new(&arguments[0]).args(&arguments[1..]).exec();
    error.raw_os_error().unwrap_or(UNKNOWN_OS_ERROR)
}

#[cfg(test)]
mod tests {
    use super::{
        INVALID_CHUNK_SIZE, INVOCATION, SUCCESS, jar_wrapper_exec_append,
        jar_wrapper_exec_end_argument, jar_wrapper_exec_reset,
    };

    #[test]
    fn primitive_chunks_preserve_utf8_arguments() {
        assert_eq!(jar_wrapper_exec_reset(), SUCCESS);
        assert_eq!(jar_wrapper_exec_append(0xac_82_e2, 3), SUCCESS);
        assert_eq!(jar_wrapper_exec_end_argument(), SUCCESS);
        assert_eq!(INVOCATION.lock().unwrap().arguments, ["€"]);
    }

    #[test]
    fn chunks_must_contain_one_to_eight_bytes() {
        assert_eq!(jar_wrapper_exec_append(0, 0), INVALID_CHUNK_SIZE);
        assert_eq!(jar_wrapper_exec_append(0, 9), INVALID_CHUNK_SIZE);
    }
}
