//! Exposed C APIs for arti-rpc-client-core.
//!
//! See top-level documentation in header file for C conventions that affect the safety of these functions.
//! (These include things like "all input pointers must be valid" and so on.)

pub mod err;
mod util;

use err::{ArtiRpcError, InvalidInput};
use std::ffi::{c_char, c_int, c_void};
use std::sync::Mutex;
use util::{
    OptOutPtrExt as _, OptOutValExt, OutBoxedPtr, OutSocketOwned, OutVal, ffi_body_raw,
    ffi_body_with_err,
};

#[cfg(not(windows))]
use std::os::fd::{AsRawFd, BorrowedFd};

#[cfg(windows)]
use std::os::windows::io::{AsRawSocket, BorrowedSocket};

use crate::{
    ObjectId, RpcConnBuilder, RpcPoll,
    conn::{AnyResponse, RequestHandle},
    util::Utf8CString,
};

/// A status code returned by an Arti RPC function.
///
/// On success, a function will return `ARTI_SUCCESS (0)`.
/// On failure, a function will return some other status code.
pub type ArtiRpcStatus = u32;

/// An open connection to Arti over an a RPC protocol.
///
/// This is a thread-safe type: you may safely use it from multiple threads at once.
///
/// Once you are no longer going to use this connection at all, you must free
/// it with [`arti_rpc_conn_free`]
pub type ArtiRpcConn = crate::RpcConn;

/// A builder object used to configure and construct
/// a connection to Arti over the RPC protocol.
///
/// This is a thread-safe type: you may safely use it from multiple threads at once.
///
/// Once you are done with this object, you must free it with [`arti_rpc_conn_builder_free`].
pub struct ArtiRpcConnBuilder(Mutex<RpcConnBuilder>);

/// An object used to poll a nonblocking RPC connection for responses.
///
/// `ArtiRpcPoll` is used to integrate an Arti RPC connection with a polling-based
/// event loop.  See [`arti_rpc_conn_builder_connect_polling`] for more information.
//
// Note: we add a mutex to ArtiRpcPoll because we do not trust the user to keep its
// use to a single thread.
pub struct ArtiRpcPoll(Mutex<RpcPoll>);

/// An owned string, returned by this library.
///
/// This string must be released with `arti_rpc_str_free`.
/// You can inspect it with `arti_rpc_str_get`, but you may not modify it.
/// The string is guaranteed to be UTF-8 and NUL-terminated.
pub type ArtiRpcStr = Utf8CString;

/// A handle to an in-progress RPC request.
///
/// This handle must eventually be freed with `arti_rpc_handle_free`.
///
/// You can wait for the next message with `arti_rpc_handle_wait`.
pub type ArtiRpcHandle = RequestHandle;

/// The type of a message returned by an RPC request.
pub type ArtiRpcResponseType = c_int;

/// The type of an entry prepended to a connect point search path.
pub type ArtiRpcBuilderEntryType = c_int;

/// The type of a data stream socket.
/// (This is always `int` on Unix-like platforms,
/// and SOCKET on Windows.)
//
// NOTE: We declare this as a separate type so that we can give it a default.
#[repr(transparent)]
pub struct ArtiRpcRawSocket(
    #[cfg(windows)] std::os::windows::raw::SOCKET,
    #[cfg(not(windows))] c_int,
);

impl Default for ArtiRpcRawSocket {
    fn default() -> Self {
        #[cfg(windows)]
        {
            Self(!0)
        }
        #[cfg(not(windows))]
        {
            Self(-1)
        }
    }
}
#[cfg(not(windows))]
impl<'a> From<BorrowedFd<'a>> for ArtiRpcRawSocket {
    fn from(value: BorrowedFd<'a>) -> Self {
        Self(value.as_raw_fd())
    }
}
#[cfg(windows)]
impl<'a> From<BorrowedSocket<'a>> for ArtiRpcRawSocket {
    fn from(value: BorrowedSocket<'a>) -> Self {
        Self(value.as_raw_socket())
    }
}

/// User-provided information used to implement [`crate::EventLoop`].
///
/// This type is crate-internal; in the API, we instead take its members as arguments.
///
/// See [`crate::EventLoop`] for semantics, and [`arti_rpc_conn_builder_connect_polling`]
/// for semantics.
struct UserEventLoop {
    /// A function to invoke with `callback_data_ptr` when the connection starts wanting to write.
    /// Returns 0 or an errno.
    start_writing_callback: unsafe extern "C" fn(*mut c_void) -> c_int,
    /// A function to invoke with `callback_data_ptr` when the connection stop wanting to write.
    /// Returns 0 or an errno.
    stop_writing_callback: unsafe extern "C" fn(*mut c_void) -> c_int,
    /// An argument to pass to one of the callbacks in this struct.
    callback_data_ptr: *mut c_void,
}

impl crate::EventLoop for UserEventLoop {
    fn stop_writing(&mut self) -> std::io::Result<()> {
        // SAFETY: the safety requirements for this function are documented in
        // `arti_rpc_conn_builder_connect_polling`.
        let r = unsafe { (self.stop_writing_callback)(self.callback_data_ptr) };
        if r == 0 {
            Ok(())
        } else {
            Err(std::io::Error::from_raw_os_error(r))
        }
    }

    fn start_writing(&mut self) -> std::io::Result<()> {
        // SAFETY: the safety requirements for this function are documented in
        // `arti_rpc_conn_builder_connect_polling`.
        let r = unsafe { (self.start_writing_callback)(self.callback_data_ptr) };
        if r == 0 {
            Ok(())
        } else {
            Err(std::io::Error::from_raw_os_error(r))
        }
    }
}

// SAFETY: We document the thread-safety requirements for UserEventLoop's members in
// `arti_rpc_conn_builder_connect_polling`.
unsafe impl Send for UserEventLoop {}
unsafe impl Sync for UserEventLoop {}

/// A user-provided tag used to distinguish responses for requests submitted to
/// [`arti_rpc_conn_submit()`].
///
/// This value is two pointers wide to facilitate using it to store
/// a function pointer and an argument, where appropriate.
#[derive(Clone, Copy, Default)]
#[repr(C)]
#[allow(missing_docs, clippy::exhaustive_structs)]
pub struct ArtiRpcUserTag {
    pub a: usize,
    pub b: usize,
}

impl From<crate::UserTag> for ArtiRpcUserTag {
    fn from(value: crate::UserTag) -> Self {
        Self {
            a: value.0,
            b: value.1,
        }
    }
}
impl From<ArtiRpcUserTag> for crate::UserTag {
    fn from(value: ArtiRpcUserTag) -> Self {
        Self(value.a, value.b)
    }
}

/// Try to create a new `ArtiRpcConnBuilder`, with default settings.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*builder_out`
/// to a new `ArtiRpcConnBuilder`.
/// Otherwise return some other status code, set `*builder_out` to NULL, and set
/// `*error_out` (if provided) to a newly allocated error object.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*builder_out` and `*error_out`,
/// if set, are eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_new(
    builder_out: *mut *mut ArtiRpcConnBuilder,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let builder_out: Option<OutBoxedPtr<ArtiRpcConnBuilder>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let builder = ArtiRpcConnBuilder(Mutex::new(RpcConnBuilder::new()));
            builder_out.write_boxed_value_if_ptr_set(builder);
        }
    )
}

/// Release storage held by an `ArtiRpcConnBuilder`.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_free(builder: *mut ArtiRpcConnBuilder) {
    ffi_body_raw!(
        {
            let builder: Option<Box<ArtiRpcConnBuilder>> [in_ptr_consume_opt];
        } in {
            drop(builder);
            // Safety: return value is (); trivially safe.
            ()
        }
    );
}

/// Constant to denote a literal connect point.
///
/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT: ArtiRpcBuilderEntryType = 1;

/// Constant to denote a path in which Arti configuration variables are expanded.
///
/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
pub const ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH: ArtiRpcBuilderEntryType = 2;

/// Constant to denote a literal path that is not expanded.
///
/// This constant is passed to [`arti_rpc_conn_builder_prepend_entry`].
pub const ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH: ArtiRpcBuilderEntryType = 3;

/// Prepend a single entry to the connection point path in `builder`.
///
/// This entry will be considered before any entries in `${ARTI_RPC_CONNECT_PATH}`,
/// but after any entry in `${ARTI_RPC_CONNECT_PATH_OVERRIDE}`.
///
/// The interpretation will depend on the value of `entry_type`.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
/// Otherwise return some other status code, and set
/// `*error_out` (if provided) to a newly allocated error object.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`,
/// if set, is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_prepend_entry(
    builder: *const ArtiRpcConnBuilder,
    entry_type: ArtiRpcBuilderEntryType,
    entry: *const c_char,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
            let entry: Option<&str> [in_str_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let builder = builder.ok_or(InvalidInput::NullPointer)?;
            let entry = entry.ok_or(InvalidInput::NullPointer)?;
            let mut b = builder.0.lock().expect("Poisoned lock");
            match entry_type {
                ARTI_RPC_BUILDER_ENTRY_LITERAL_CONNECT_POINT =>
                    b.prepend_literal_entry(entry.to_string()),
                ARTI_RPC_BUILDER_ENTRY_EXPANDABLE_PATH =>
                    b.prepend_path(entry.into()),
                ARTI_RPC_BUILDER_ENTRY_LITERAL_PATH =>
                    b.prepend_literal_path(entry.into()),
                _ => return Err(InvalidInput::InvalidConstValue.into()),
            }
        }
    )
}

/// Instruct `builder` to prefer connection points that grant superuser permission.
///
/// If no such connect points are found,
/// and `required` is true,
/// then the connection will fail with an error.
/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
/// Otherwise return some other status code, and set
/// `*error_out` (if provided) to a newly allocated error object.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`,
/// if set, is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_prefer_superuser_permission(
    builder: *const ArtiRpcConnBuilder,
    required: c_int,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let builder = builder.ok_or(InvalidInput::NullPointer)?;
            let mut b = builder.0.lock().expect("Poisoned lock");
            b.prefer_superuser_permission(required != 0);
        }
    )
}

/// Use `builder` to open a new RPC connection to Arti.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`,
/// and set `conn_out` to a new ArtiRpcConn.
/// Otherwise return some other status code, set *conn_out to NULL, and set
/// `*error_out` (if provided) to a newly allocated error object.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*rpc_conn_out` and `*error_out`,
/// if set, are eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_connect(
    builder: *const ArtiRpcConnBuilder,
    rpc_conn_out: *mut *mut ArtiRpcConn,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
            let rpc_conn_out: Option<OutBoxedPtr<ArtiRpcConn>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let builder = builder.ok_or(InvalidInput::NullPointer)?;
            let b = builder.0.lock().expect("Poisoned lock");
            let conn = b.connect()?;
            rpc_conn_out.write_boxed_value_if_ptr_set(conn);
        }
    )
}

/// Use `builder` to open a new RPC connection to Arti,
/// with support to integrate with an event-driven IO loop.
///
/// If you are not integrating with poll() or select()-style loop,
/// you do not need to use this function.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`,
/// set `rpc_conn_out` to a new ArtiRpcConn,
/// and set `poll_out` to a new ArtiRpcPoll.
/// Otherwise return some other status code, set *conn_out and *poll_out to NULL,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// # Callbacks, data, and requirements.
///
/// The user code must provide an event loop
/// that can monitor an underlying connection for readability and writability.
///
/// The RPC library provides the user code with an OS handle to monitor.
/// The user code should always monitor this handle for readability.
/// It should monitor the handle for writability whenever the connection
/// "wants to write".
/// Whenever one of these events occurs, the user code should invoke
/// `arti_rpc_poll_poll` until it indicates that it would block.
///
/// The `start_writing_callback` and `start_reading_callback` functions
/// must be provided.  They will be invoked (respectively) whenever the connection
/// starts wanting to write, or stops wanting to write.
/// They should return 0 on success, and _return_ an `errno` value on failure.
/// (Any `errno` value that they _set_ will be ignored.)
/// They will be passed `callback_data_ptr` as an argument.
///
/// If your program invokes `arti_rpc_*` from multiple threads,
/// these functions must be thread-safe.
///
/// There are additional requirements for these functions.
/// For full information, see the documentation for [`EventLoop`](crate::EventLoop`).
///
/// # Ownership
///
/// The caller is responsible for making sure that `*rpc_conn_out`,
/// `*rpc_poll_out`, and `*error_out`,
/// if set, are eventually freed.
///
/// The caller is responsible for making sure that `callback_data_ptr`,
/// and the callback functions,
/// live for at least as long as the returned `ArtiRpcPoll`.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_builder_connect_polling(
    builder: *const ArtiRpcConnBuilder,
    start_writing_callback: Option<unsafe extern "C" fn(*mut c_void) -> c_int>,
    stop_writing_callback: Option<unsafe extern "C" fn(*mut c_void) -> c_int>,
    callback_data_ptr: *mut c_void,
    rpc_conn_out: *mut *mut ArtiRpcConn,
    rpc_poll_out: *mut *mut ArtiRpcPoll,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let builder: Option<&ArtiRpcConnBuilder> [in_ptr_opt];
            let rpc_conn_out: Option<OutBoxedPtr<ArtiRpcConn>> [out_ptr_opt];
            let rpc_poll_out: Option<OutBoxedPtr<ArtiRpcPoll>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let builder = builder.ok_or(InvalidInput::NullPointer)?;
            let start_writing_callback = start_writing_callback.ok_or(InvalidInput::NullPointer)?;
            let stop_writing_callback = stop_writing_callback.ok_or(InvalidInput::NullPointer)?;

            let event_loop = Box::new(UserEventLoop {
                start_writing_callback, stop_writing_callback,
                callback_data_ptr,
            });

            let b = builder.0.lock().expect("Poisoned lock");
            let (conn, poll) = b.connect_polling(event_loop)?;
            let poll = ArtiRpcPoll(Mutex::new(poll));
            rpc_conn_out.write_boxed_value_if_ptr_set(conn);
            rpc_poll_out.write_boxed_value_if_ptr_set(poll);
        }
    )
}

/// Given a pointer to an RPC connection, return the object ID for its negotiated session.
///
/// (The session was negotiated as part of establishing the connection.
/// Its object ID is necessary to invoke most other functionality on Arti.)
///
/// The caller should be prepared for a possible NULL return, in case somehow
/// no session was negotiated.
///
/// # Ownership
///
/// The resulting string is a reference to part of the `ArtiRpcConn`.
/// It lives for no longer than the underlying `ArtiRpcConn` object.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_get_session_id(
    rpc_conn: *const ArtiRpcConn,
) -> *const c_char {
    ffi_body_raw! {
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
        } in {
            rpc_conn.and_then(crate::RpcConn::session)
                .map(|s| s.as_ptr())
                .unwrap_or(std::ptr::null())
            // Safety: returned pointer is null, or semantically borrowed from `rpc_conn`.
            // It is only null if `rpc_conn` was null or its session was null.
            // The caller is not allowed to modify it.
        }
    }
}

/// Run an RPC request over `rpc_conn` and wait for a successful response.
///
/// The message `msg` should be a valid RPC request in JSON format.
/// If you omit its `id` field, one will be generated: this is typically the best way to use this function.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*response_out` to a newly allocated string
/// containing the JSON response to your request (including `id` and `response` fields).
///
/// Otherwise return some other status code,  set `*response_out` to NULL,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// (If response_out is set to NULL, then any successful response will be ignored.)
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
///
/// The caller is responsible for making sure that `*response_out`, if set, is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_execute(
    rpc_conn: *const ArtiRpcConn,
    msg: *const c_char,
    response_out: *mut *mut ArtiRpcStr,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let msg: Option<&str> [in_str_opt];
            let response_out: Option<OutBoxedPtr<ArtiRpcStr>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
            let msg = msg.ok_or(InvalidInput::NullPointer)?;

            let success = rpc_conn.execute(msg)??;
            response_out.write_boxed_value_if_ptr_set(Utf8CString::from(success));
        }
    )
}

/// Send an RPC request over `rpc_conn`, and return a handle that can wait for a successful response.
///
/// The message `msg` should be a valid RPC request in JSON format.
/// If you omit its `id` field, one will be generated: this is typically the best way to use this function.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS` and set `*handle_out` to a newly allocated `ArtiRpcHandle`.
///
/// Otherwise return some other status code,  set `*handle_out` to NULL,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// (If `handle_out` is set to NULL, the request will not be sent, and an error will be returned.)
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
///
/// The caller is responsible for making sure that `*handle_out`, if set, is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_execute_with_handle(
    rpc_conn: *const ArtiRpcConn,
    msg: *const c_char,
    handle_out: *mut *mut ArtiRpcHandle,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let msg: Option<&str> [in_str_opt];
            let handle_out: Option<OutBoxedPtr<ArtiRpcHandle>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
            let msg = msg.ok_or(InvalidInput::NullPointer)?;
            let handle_out = handle_out.ok_or(InvalidInput::NullPointer)?;

            let handle = rpc_conn.execute_with_handle(msg)?;
            handle_out.write_value_boxed(handle);
        }
    )
}

/// Attempt to cancel the request on `rpc_conn` with the provided `handle`.
///
/// Note that cancellation _will_ fail if the handle has already been cancelled,
/// or has already succeeded or failed.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
///
/// Otherwise return some other status code,
/// and set `*error_out` (if provided) to a newly allocated error object.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_cancel_handle(
    rpc_conn: *const ArtiRpcConn,
    handle: *const ArtiRpcHandle,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let handle: Option<&ArtiRpcHandle> [in_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
            let handle = handle.ok_or(InvalidInput::NullPointer)?;
            let id = handle.id();
            rpc_conn.cancel(id)?;
        }
    )
}

/// A constant indicating that a message is a final result.
///
/// After a result has been received, a handle will not return any more responses,
/// and should be freed.
pub const ARTI_RPC_RESPONSE_TYPE_RESULT: ArtiRpcResponseType = 1;
/// A constant indicating that a message is a non-final update.
///
/// After an update has been received, the handle may return additional responses.
pub const ARTI_RPC_RESPONSE_TYPE_UPDATE: ArtiRpcResponseType = 2;
/// A constant indicating that a message is a final error.
///
/// After an error has been received, a handle will not return any more responses,
/// and should be freed.
pub const ARTI_RPC_RESPONSE_TYPE_ERROR: ArtiRpcResponseType = 3;

impl AnyResponse {
    /// Return an appropriate `ARTI_RPC_RESPONSE_TYPE_*` for this response.
    fn response_type(&self) -> ArtiRpcResponseType {
        match self {
            Self::Success(_) => ARTI_RPC_RESPONSE_TYPE_RESULT,
            Self::Update(_) => ARTI_RPC_RESPONSE_TYPE_UPDATE,
            Self::Error(_) => ARTI_RPC_RESPONSE_TYPE_ERROR,
        }
    }
}

/// Wait until some response arrives on an arti_rpc_handle, or until an error occurs.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`; set `*response_out`, if present, to a
/// newly allocated string, and set `*response_type_out`, if present, to the type of the response.
/// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result,
/// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error,
/// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.)
///
/// Otherwise return some other status code, set `*response_out` to NULL,
/// set `*response_type_out` to zero,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// Note that receiving an error reply from Arti is _not_ treated as an error in this function.
/// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`,
/// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to
/// `ARTI_RPC_RESPONSE_TYPE_ERROR`.
///
/// It is safe to call this function on the same handle from multiple threads at once.
/// If you do, each response will be sent to exactly one thread.
/// It is unspecified which thread will receive which response or which error.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
///
/// The caller is responsible for making sure that `*response_out`, if set, is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_handle_wait(
    handle: *const ArtiRpcHandle,
    response_out: *mut *mut ArtiRpcStr,
    response_type_out: *mut ArtiRpcResponseType,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err! {
        {
            let handle: Option<&ArtiRpcHandle> [in_ptr_opt];
            let response_out: Option<OutBoxedPtr<ArtiRpcStr>> [out_ptr_opt];
            let response_type_out: Option<OutVal<ArtiRpcResponseType>> [out_val_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let handle = handle.ok_or(InvalidInput::NullPointer)?;

            let response = handle.wait_with_updates()?;

            let rtype = response.response_type();
            response_type_out.write_value_if_ptr_set(rtype);
            response_out.write_boxed_value_if_ptr_set(response.into_string());
        }
    }
}

/// Release storage held by an `ArtiRpcHandle`.
///
/// NOTE: This does not cancel the underlying request if it is still running.
/// To cancel a request, use `arti_rpc_conn_cancel_handle`.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_handle_free(handle: *mut ArtiRpcHandle) {
    ffi_body_raw!(
        {
            let handle: Option<Box<ArtiRpcHandle>> [in_ptr_consume_opt];
        } in {
            drop(handle);
            // Safety: Return value is (); trivially safe.
            ()
        }
    );
}

/// Submit an RPC request to `rpc_conn`, but do not wait for a response.
///
/// The message `msg` should be a valid RPC request in JSON format.
/// If you omit its `id` field, one will be generated:
/// this is typically the best way to use this function.
///
/// The `tag` value should be a value to identify this request;
/// it will be returned later along with any responses to this request.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
///
/// Otherwise return some other status code,  set `*response_out` to NULL,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// After calling this function, the caller must later make sure
/// that [`arti_rpc_conn_wait()`] is called on the connection to wait for responses
/// to _any_ submitted request.
/// (If the  connection was crated with [`arti_rpc_conn_builder_connect_polling`],
/// the user must call [`arti_rpc_poll_poll()`] instead.)
///
/// (If nobody is running [`arti_rpc_conn_wait()`] or [`arti_rpc_poll_poll()`],
/// then responses will never be handled,
/// and can potentially fill up memory.)
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
///
/// # Thread safety
///
/// It is safe to call this function from multiple threads at once;
/// it is not specified which thread will receive notifications for which request.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_submit(
    rpc_conn: *const ArtiRpcConn,
    msg: *const c_char,
    tag: *const ArtiRpcUserTag,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let msg: Option<&str> [in_str_opt];
            let tag: Option<&ArtiRpcUserTag> [in_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
            let msg = msg.ok_or(InvalidInput::NullPointer)?;
            let tag = tag.ok_or(InvalidInput::NullPointer)?;

            let () = rpc_conn.submit((*tag).into(), msg)?;
        }
    )
}

/// Wait for responses to arrive for requests sent via [`arti_rpc_conn_submit`].
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`;
/// set `*tag_out`, if present,  to the `ArtiRpcUserTag` originally provided with the request;
/// set `*response_out`, if present, to a newly allocated string;
/// and set `*response_type_out`,  if present, to the type of the response.
/// (The type will be `ARTI_RPC_RESPONSE_TYPE_RESULT` if the response is a final result,
/// or `ARTI_RPC_RESPONSE_TYPE_ERROR` if the response is a final error,
/// or `ARTI_RPC_RESPONSE_TYPE_UPDATE` if the response is a non-final update.)
///
/// Otherwise return some other status code, set `*response_out` to NULL,
/// set `*response_type_out` and `*tag_out` to zero,
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// Note that receiving an error reply from Arti is _not_ treated as an error in this function.
/// That is to say, if Arti sends back an error, this function will return `ARTI_SUCCESS`,
/// and deliver the error from Arti in `*response_out`, setting `*response_type_out` to
/// `ARTI_RPC_RESPONSE_TYPE_ERROR`.
///
/// # Thread safety
///
/// It is safe to call this function from multiple threads at once;
/// it is not specified which thread will receive notifications for which request.
///
/// # Ownership
///
/// The caller is responsible for making sure that `*error_out`, if set, is eventually freed.
///
/// The caller is responsible for making sure that `*response_out`, if set,
/// is eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_wait(
    rpc_conn: *const ArtiRpcConn,
    tag_out: *mut ArtiRpcUserTag,
    response_out: *mut *mut ArtiRpcStr,
    response_type_out: *mut ArtiRpcResponseType,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!(
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let tag_out: Option<OutVal<ArtiRpcUserTag>> [out_val_opt];
            let response_out: Option<OutBoxedPtr<ArtiRpcStr>> [out_ptr_opt];
            let response_type_out: Option<OutVal<ArtiRpcResponseType>> [out_val_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;

            let (tag, response) = rpc_conn.wait()?;
            tag_out.write_value_if_ptr_set(tag.into());

            let rtype = response.response_type();
            response_type_out.write_value_if_ptr_set(rtype);
            response_out.write_boxed_value_if_ptr_set(response.into_string());
        }
    )
}

/// Handle IO events for the associated RPC connection, without blocking.
///
/// This method requires that the connection was created with
/// [`arti_rpc_conn_builder_connect_polling()`].
/// It reads and writes data from the RPC server, until either:
///
/// * A response is available to a request created with [`arti_rpc_conn_submit`].
///   In this case,
///   `*tag_out`, if present, is set to the `ArtiRpcUserTag` originally provided with the request;
///   `*response_out`, if present, is set to a newly allocated string;
///   `*response_type_out`, if present, is set to the type of the response.
///   The `*would_block_out` flag, if present, is set to 0.
///   Other output pointers, if present, are set to NULL or 0.
///
/// * No further progress can be made without blocking.
///   In this case,
///   `*would_block_out` is set to 1.
///   Other output pointers, if present, are set to NULL or 0.
///
/// * An error occurs.
///   (This does not include receiving an error response from the RPC server.)
///   In this case, `*error_out`, if present, is set to that error.
///   Other output pointers, if present, are set to NULL or 0.
///
/// Returns `ARTI_RPC_SUCCESS` in the first two cases,
/// and an error code on failure.
///
/// # Thread safety
///
/// While it is not unsafe to call this function at once,
/// it is generally pointless:
/// only one thread can make progress at a time.
///
/// # Ownership
///
/// The caller is responsible for making sure
/// that `*tag_out`, *response_out`, and `*error_out`,
/// if set, are eventually freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_poll_poll(
    rpc_poll: *const ArtiRpcPoll,
    tag_out: *mut ArtiRpcUserTag,
    response_out: *mut *mut ArtiRpcStr,
    response_type_out: *mut ArtiRpcResponseType,
    would_block_out: *mut c_int,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err!({
        let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt];
        let tag_out: Option<OutVal<ArtiRpcUserTag>> [out_val_opt];
        let response_out: Option<OutBoxedPtr<ArtiRpcStr>> [out_ptr_opt];
        let response_type_out: Option<OutVal<ArtiRpcResponseType>> [out_val_opt];
        let would_block_out: Option<OutVal<c_int>> [out_val_opt];
        err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
    } in {
        let rpc_poll = rpc_poll.ok_or(InvalidInput::NullPointer)?;

        let (tag, response)  = match rpc_poll.0.lock().expect("Lock poisoned").poll()? {
            Ok(v) => v,
            Err(crate::WouldBlock) => {
                would_block_out.write_value_if_ptr_set(1);
                return Ok(());
            }
        };

        tag_out.write_value_if_ptr_set(tag.into());
        let rtype = response.response_type();
        response_type_out.write_value_if_ptr_set(rtype);
        response_out.write_boxed_value_if_ptr_set(response.into_string());
    })
}

/// Return true if `rpc_poll` wants to write,
/// and false otherwise.
///
/// See [`arti_rpc_conn_builder_connect_polling`] for more information.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_poll_wants_to_write(rpc_poll: *const ArtiRpcPoll) -> c_int {
    ffi_body_raw!({
        let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt];
    } in {
        let Some(rpc_poll) = rpc_poll else {
            return 0;
        };
        let wants_to_write: bool = rpc_poll.0.lock()
            .expect("Lock poisoned")
            .wants_to_write();
        // Safety: return type is c_int; trivially safe.
        c_int::from(wants_to_write)
    })
}

/// Return the raw OS socket associated with the provided `ArtiRpcPoll`.
///
/// This function returns a SOCKET on windows, and a file descriptor elsewhere.
///
/// On failure, returns INVALID_SOCKET on windows, and -1 elsewhere.
///
/// # Ownership
///
/// The returned socket is owned by the `ArtiRpcPoll`.
/// The caller must not close it, read from it, or write to it.
/// It should _only_ be polled for readiness events.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_poll_get_socket(
    rpc_poll: *const ArtiRpcPoll,
) -> ArtiRpcRawSocket {
    ffi_body_raw!({
        let rpc_poll: Option<&ArtiRpcPoll> [in_ptr_opt];
    } in {
        let Some(rpc_poll) = rpc_poll else {
            return ArtiRpcRawSocket::default();
        };
        let raw_socket: Result<ArtiRpcRawSocket, _> = {
            let rpc_poll = rpc_poll.0.lock().expect("Lock poisoned");
            #[cfg(windows)]
            {
                rpc_poll.try_as_socket().map(ArtiRpcRawSocket::from)
            }
            #[cfg(not(windows))]
            {
                rpc_poll.try_as_fd().map(ArtiRpcRawSocket::from)
            }
        };

        // Safety: return type is trivially safe.
        raw_socket.unwrap_or_default()
    })
}

/// Free the provided `ArtiRpcPoll`.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_poll_free(rpc_poll: *mut ArtiRpcPoll) {
    ffi_body_raw!(
        {
            let rpc_poll: Option<Box<ArtiRpcPoll>> [in_ptr_consume_opt];
        } in {
            drop(rpc_poll);
            // Safety: Return value is (); trivially safe.
            ()
        }
    );
}

/// Free a string returned by the Arti RPC API.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_str_free(string: *mut ArtiRpcStr) {
    ffi_body_raw!(
        {
            let string: Option<Box<ArtiRpcStr>> [in_ptr_consume_opt];
        } in {
            drop(string);
            // Safety: Return value is (); trivially safe.
            ()
        }
    );
}

/// Return a const pointer to the underlying nul-terminated string from an `ArtiRpcStr`.
///
/// The resulting string is guaranteed to be valid UTF-8.
///
/// (Returns NULL if the input is NULL.)
///
/// # Correctness requirements
///
/// The resulting string pointer is valid only for as long as the input `string` is not freed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_str_get(string: *const ArtiRpcStr) -> *const c_char {
    ffi_body_raw!(
        {
            let string: Option<&ArtiRpcStr> [in_ptr_opt];
        } in {
            // Safety: returned pointer is null, or semantically borrowed from `string`.
            // It is only null if `string` was null.
            // The caller is not allowed to modify it.
            match string {
                Some(s) => s.as_ptr(),
                None => std::ptr::null(),
            }

        }
    )
}

/// Close and free an open Arti RPC connection.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_free(rpc_conn: *mut ArtiRpcConn) {
    ffi_body_raw!(
        {
            let rpc_conn: Option<Box<ArtiRpcConn>> [in_ptr_consume_opt];
        } in {
            drop(rpc_conn);
            // Safety: Return value is (); trivially safe.
            ()

        }
    );
}

/// Try to open an anonymized data stream over Arti.
///
/// Use the proxy information associated with `rpc_conn` to make the stream,
/// and store the resulting fd (or `SOCKET` on Windows) into `*socket_out`.
///
/// The stream will target the address `hostname`:`port`.
///
/// If `on_object` is provided, it is an `ObjectId` for client-like object
/// (such as a Session or a Client)
/// that should be used to make the stream.
///
/// The resulting stream will be configured
/// not to share a circuit with any other stream
/// having a different `isolation`.
/// (If your application doesn't care about isolating its streams from one another,
/// it is acceptable to leave `isolation` as an empty string.)
///
/// If `stream_id_out` is provided,
/// the resulting stream will have an identifier within the RPC system,
/// so that you can run other RPC commands on it.
///
/// On success, return `ARTI_RPC_STATUS_SUCCESS`.
/// Otherwise return some other status code, set `*socket_out` to -1
/// (or `INVALID_SOCKET` on Windows),
/// and set `*error_out` (if provided) to a newly allocated error object.
///
/// # Caveats
///
/// When possible, use a hostname rather than an IP address.
/// If you *must* use an IP address, make sure that you have not gotten it
/// by a non-anonymous DNS lookup.
/// (Calling `gethostname()` or `getaddrinfo()` directly
/// would lose anonymity: they inform the user's DNS server,
/// and possibly many other parties, about the target address
/// you are trying to visit.)
///
/// The resulting socket will actually be a TCP connection to Arti,
/// not directly to your destination.
/// Therefore, passing it to functions like `getpeername()`
/// may give unexpected results.
///
/// If `stream_id_out` is provided,
/// the caller is responsible for releasing the ObjectId;
/// Arti will not deallocate it even when the stream is closed.
///
/// # Ownership
///
/// The caller is responsible for making sure that
/// `*stream_id_out` and `*error_out`, if set,
/// are eventually freed.
///
/// The caller is responsible for making sure that `*socket_out`, if set,
/// is eventually closed.
#[allow(clippy::missing_safety_doc)]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn arti_rpc_conn_open_stream(
    rpc_conn: *const ArtiRpcConn,
    hostname: *const c_char,
    port: c_int,
    on_object: *const c_char,
    isolation: *const c_char,
    socket_out: *mut ArtiRpcRawSocket,
    stream_id_out: *mut *mut ArtiRpcStr,
    error_out: *mut *mut ArtiRpcError,
) -> ArtiRpcStatus {
    ffi_body_with_err! {
        {
            let rpc_conn: Option<&ArtiRpcConn> [in_ptr_opt];
            let on_object: Option<&str> [in_str_opt];
            let hostname: Option<&str> [in_str_opt];
            let isolation: Option<&str> [in_str_opt];
            let socket_out: Option<OutSocketOwned<'_>> [out_socket_owned_opt];
            let stream_id_out: Option<OutBoxedPtr<ArtiRpcStr>> [out_ptr_opt];
            err error_out: Option<OutBoxedPtr<ArtiRpcError>>;
        } in {
            let rpc_conn = rpc_conn.ok_or(InvalidInput::NullPointer)?;
            let hostname = hostname.ok_or(InvalidInput::NullPointer)?;
            let socket_out = socket_out.ok_or(InvalidInput::NullPointer)?;
            let isolation = isolation.ok_or(InvalidInput::NullPointer)?;

            let port: u16 = port.try_into().map_err(|_| InvalidInput::BadPort)?;
            if port == 0 {
                return Err(InvalidInput::BadPort.into());
            }

            let on_object = on_object.map(|o| ObjectId::try_from(o.to_owned()))
                .transpose()
                .expect("C string somehow contained NUL.");

            let stream = match stream_id_out {
                Some(stream_id_out) => {
                    let (stream_id, stream) = rpc_conn.open_stream_as_object(
                        on_object.as_ref(),
                        (hostname, port),
                        isolation)?;
                    stream_id_out.write_value_boxed(stream_id.into());
                    stream
                }
                None => {
                    rpc_conn.open_stream(on_object.as_ref(), (hostname, port), isolation)?
                }
            };

            // We call this last so that the stream will definitely be converted to an fd, or
            // dropped.
            socket_out.write_socket(stream);
        }
    }
}
