use crate::error::{ErrorKind, Result, SyncularError};
use crate::native::{
    native_runtime_manifest_json, NativeClientConfig, NativeClientOpenTask, NativeClientOptions,
    NativeErrorInfo, NativeEventSubscription, NativeSyncularClient,
};
use std::any::Any;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;

pub struct SyncularNativeHandle {
    client: Mutex<NativeSyncularClient>,
}

pub struct SyncularNativeOpenHandle {
    task: Mutex<NativeClientOpenTask>,
}

pub struct SyncularNativeEventSubscription {
    subscription: Arc<NativeEventSubscription>,
    join: Mutex<Option<JoinHandle<()>>>,
}

pub struct SyncularNativePresenceHandle {
    client: *mut SyncularNativeHandle,
    scope_key: String,
    active: Mutex<bool>,
}

pub type SyncularNativeEventCallback =
    extern "C" fn(event_json: *const c_char, user_data: *mut c_void);
pub type SyncularNativeEventErrorCallback =
    extern "C" fn(error_json: *const c_char, user_data: *mut c_void);

#[no_mangle]
pub extern "C" fn syncular_string_free(value: *mut c_char) {
    if value.is_null() {
        return;
    }

    unsafe {
        let _ = CString::from_raw(value);
    }
}

#[no_mangle]
pub extern "C" fn syncular_native_runtime_manifest_json(
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, native_runtime_manifest_json)
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open(
    config_json: *const c_char,
    auto_sync_local_writes: bool,
    error_out: *mut *mut c_char,
) -> *mut SyncularNativeHandle {
    clear_error(error_out);
    ffi_catch_ptr(error_out, || {
        let config: NativeClientConfig = serde_json::from_str(&read_c_string(config_json)?)?;
        let client = NativeSyncularClient::open_native_with_options(
            config,
            NativeClientOptions {
                auto_sync_local_writes,
            },
        )?;
        Ok(Box::into_raw(Box::new(SyncularNativeHandle {
            client: Mutex::new(client),
        })))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_async(
    config_json: *const c_char,
    auto_sync_local_writes: bool,
    error_out: *mut *mut c_char,
) -> *mut SyncularNativeOpenHandle {
    clear_error(error_out);
    ffi_catch_ptr(error_out, || {
        let config: NativeClientConfig = serde_json::from_str(&read_c_string(config_json)?)?;
        let task = NativeSyncularClient::open_native_async_with_options(
            config,
            NativeClientOptions {
                auto_sync_local_writes,
            },
        );
        Ok(Box::into_raw(Box::new(SyncularNativeOpenHandle {
            task: Mutex::new(task),
        })))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_async_command_id(
    handle: *mut SyncularNativeOpenHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_open_task(handle, |task| Ok(task.command_id().to_string()))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_async_is_finished(
    handle: *mut SyncularNativeOpenHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool_value(error_out, || {
        with_open_task(handle, |task| Ok(task.is_finished()))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_async_finish_timeout(
    handle: *mut SyncularNativeOpenHandle,
    timeout_ms: u64,
    error_out: *mut *mut c_char,
) -> *mut SyncularNativeHandle {
    clear_error(error_out);
    ffi_catch_ptr(error_out, || {
        with_open_task(handle, |task| {
            match task.take_client_timeout(Duration::from_millis(timeout_ms)) {
                Some(Ok(client)) => Ok(Box::into_raw(Box::new(SyncularNativeHandle {
                    client: Mutex::new(client),
                }))),
                Some(Err(error)) => Err(error),
                None => Ok(ptr::null_mut()),
            }
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_async_close(
    handle: *mut SyncularNativeOpenHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        if handle.is_null() {
            return Ok(());
        }

        let _ = unsafe { Box::from_raw(handle) };
        Ok(())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_close(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        if handle.is_null() {
            return Ok(());
        }

        let boxed = unsafe { Box::from_raw(handle) };
        let mut client = boxed.client.into_inner().map_err(|_| {
            SyncularError::message(ErrorKind::Internal, "native handle is poisoned")
        })?;
        client.close()
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_trigger_sync(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.trigger_sync())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_trigger_sync_websocket(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.trigger_sync_websocket())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_pause_sync_worker(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.pause_sync_worker())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_resume_sync_worker(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.resume_sync_worker())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_resume_from_background(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.resume_from_background())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_sync_worker_running(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool_value(error_out, || {
        with_client(handle, |client| Ok(client.sync_worker_running()))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_start_realtime_worker(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.start_realtime_worker())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_stop_realtime_worker(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.stop_realtime_worker())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_join_presence(
    handle: *mut SyncularNativeHandle,
    scope_key: *const c_char,
    metadata_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let scope_key = read_c_string(scope_key)?;
        let metadata = read_optional_c_string(metadata_json)?
            .map(|json| serde_json::from_str(&json))
            .transpose()?;
        with_client(handle, |client| client.join_presence(&scope_key, metadata))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_leave_presence(
    handle: *mut SyncularNativeHandle,
    scope_key: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let scope_key = read_c_string(scope_key)?;
        with_client(handle, |client| client.leave_presence(&scope_key))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_update_presence_metadata(
    handle: *mut SyncularNativeHandle,
    scope_key: *const c_char,
    metadata_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let scope_key = read_c_string(scope_key)?;
        let metadata_json = read_c_string(metadata_json)?;
        let metadata = serde_json::from_str(&metadata_json)?;
        with_client(handle, |client| {
            client.update_presence_metadata(&scope_key, metadata)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_presence_json(
    handle: *mut SyncularNativeHandle,
    scope_key: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let scope_key = read_c_string(scope_key)?;
        with_client(handle, |client| client.presence_json(&scope_key))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_join_presence_handle(
    handle: *mut SyncularNativeHandle,
    scope_key: *const c_char,
    metadata_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut SyncularNativePresenceHandle {
    clear_error(error_out);
    ffi_catch_ptr(error_out, || {
        let scope_key = read_c_string(scope_key)?;
        let metadata = read_optional_c_string(metadata_json)?
            .map(|json| serde_json::from_str(&json))
            .transpose()?;
        with_client(handle, |client| client.join_presence(&scope_key, metadata))?;
        Ok(Box::into_raw(Box::new(SyncularNativePresenceHandle {
            client: handle,
            scope_key,
            active: Mutex::new(true),
        })))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_presence_handle_scope_key(
    handle: *mut SyncularNativePresenceHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_presence_handle(handle, |presence| Ok(presence.scope_key.clone()))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_presence_handle_update_metadata(
    handle: *mut SyncularNativePresenceHandle,
    metadata_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let metadata_json = read_c_string(metadata_json)?;
        let metadata = serde_json::from_str(&metadata_json)?;
        with_presence_handle(handle, |presence| {
            let active = presence.active.lock().map_err(|_| {
                SyncularError::message(ErrorKind::Internal, "native presence handle is poisoned")
            })?;
            if !*active {
                return Err(SyncularError::message(
                    ErrorKind::Config,
                    "native presence handle is inactive",
                ));
            }
            with_client(presence.client, |client| {
                client.update_presence_metadata(&presence.scope_key, metadata)
            })
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_presence_handle_leave(
    handle: *mut SyncularNativePresenceHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool_value(error_out, || {
        with_presence_handle(handle, leave_presence_handle_inner)
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_presence_handle_close(
    handle: *mut SyncularNativePresenceHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        if handle.is_null() {
            return Ok(());
        }

        let presence = unsafe { Box::from_raw(handle) };
        let _ = leave_presence_handle_inner(&presence)?;
        Ok(())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_auth_headers_json(
    handle: *mut SyncularNativeHandle,
    headers_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let headers_json = read_c_string(headers_json)?;
        with_client(handle, |client| client.set_auth_headers_json(&headers_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_subscriptions_json(
    handle: *mut SyncularNativeHandle,
    subscriptions_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let subscriptions_json = read_c_string(subscriptions_json)?;
        with_client(handle, |client| {
            client.set_subscriptions_json(&subscriptions_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_force_subscriptions_bootstrap_json(
    handle: *mut SyncularNativeHandle,
    subscription_ids_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let subscription_ids_json = read_c_string(subscription_ids_json)?;
        with_client(handle, |client| {
            client.force_subscriptions_bootstrap_json(&subscription_ids_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_field_encryption_json(
    handle: *mut SyncularNativeHandle,
    config_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let config_json = read_c_string(config_json)?;
        with_client(handle, |client| {
            client.set_field_encryption_json(&config_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_encrypted_crdt_json(
    handle: *mut SyncularNativeHandle,
    config_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let config_json = read_c_string(config_json)?;
        with_client(handle, |client| {
            client.set_encrypted_crdt_json(&config_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_blob_encryption_json(
    handle: *mut SyncularNativeHandle,
    config_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let config_json = read_c_string(config_json)?;
        with_client(handle, |client| {
            client.set_blob_encryption_json(&config_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_encryption_helper_json(
    method: *const c_char,
    args_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let method = read_c_string(method)?;
        let args_json = read_c_string(args_json)?;
        crate::encryption::encryption_helpers_json(&method, &args_json)
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_mutation_json(
    handle: *mut SyncularNativeHandle,
    mutation_json: *const c_char,
    local_row_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let mutation_json = read_c_string(mutation_json)?;
        let local_row_json = read_optional_c_string(local_row_json)?;
        with_client(handle, |client| {
            client.apply_mutation_json(&mutation_json, local_row_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_leased_mutation_json(
    handle: *mut SyncularNativeHandle,
    mutation_json: *const c_char,
    local_row_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let mutation_json = read_c_string(mutation_json)?;
        let local_row_json = read_optional_c_string(local_row_json)?;
        with_client(handle, |client| {
            client.apply_leased_mutation_json(&mutation_json, local_row_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_mutation_json(
    handle: *mut SyncularNativeHandle,
    mutation_json: *const c_char,
    local_row_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let mutation_json = read_c_string(mutation_json)?;
        let local_row_json = read_optional_c_string(local_row_json)?;
        with_client(handle, |client| {
            client.enqueue_mutation_json(&mutation_json, local_row_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_leased_mutation_json(
    handle: *mut SyncularNativeHandle,
    mutation_json: *const c_char,
    local_row_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let mutation_json = read_c_string(mutation_json)?;
        let local_row_json = read_optional_c_string(local_row_json)?;
        with_client(handle, |client| {
            client.enqueue_leased_mutation_json(&mutation_json, local_row_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_yjs_update_json(
    handle: *mut SyncularNativeHandle,
    update_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let update_json = read_c_string(update_json)?;
        with_client(handle, |client| {
            client.enqueue_yjs_update_json(&update_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_open_crdt_field_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| client.open_crdt_field_json(&request_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_crdt_field_text_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.apply_crdt_field_text_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_crdt_field_yjs_update_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.apply_crdt_field_yjs_update_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_crdt_field_yjs_update_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_crdt_field_yjs_update_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_crdt_field_text_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_crdt_field_text_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_crdt_field_compaction_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_crdt_field_compaction_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_materialize_crdt_field_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.materialize_crdt_field_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_crdt_document_snapshot_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.crdt_document_snapshot_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_crdt_update_log_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| client.crdt_update_log_json(&request_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_snapshot_crdt_field_state_vector_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.snapshot_crdt_field_state_vector_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_compact_crdt_field_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.compact_crdt_field_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_encrypted_crdt_update_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.apply_encrypted_crdt_update_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_encrypted_crdt_update_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_encrypted_crdt_update_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_apply_encrypted_crdt_checkpoint_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.apply_encrypted_crdt_checkpoint_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_encrypted_crdt_checkpoint_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_encrypted_crdt_checkpoint_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_sync_now(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.enqueue_sync_now())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_sync_websocket(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.enqueue_sync_websocket())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_app_tables_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.app_tables_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_app_table_metadata_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.app_table_metadata_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_app_schema_state_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.app_schema_state_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_list_table_json(
    handle: *mut SyncularNativeHandle,
    table: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let table = read_c_string(table)?;
        with_client(handle, |client| client.list_table_json(&table))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_query_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| client.query_json(&request_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_refresh_snapshot_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| {
            client.enqueue_refresh_snapshot_json(&request_json)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_store_blob_file_json(
    handle: *mut SyncularNativeHandle,
    file_path: *const c_char,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let file_path = read_c_string(file_path)?;
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.store_blob_file_json(&file_path, options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_store_blob_file_json(
    handle: *mut SyncularNativeHandle,
    file_path: *const c_char,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let file_path = read_c_string(file_path)?;
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.enqueue_store_blob_file_json(&file_path, options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_retrieve_blob_file(
    handle: *mut SyncularNativeHandle,
    ref_json: *const c_char,
    file_path: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let ref_json = read_c_string(ref_json)?;
        let file_path = read_c_string(file_path)?;
        with_client(handle, |client| {
            client.retrieve_blob_file(&ref_json, &file_path)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_retrieve_blob_file_with_options(
    handle: *mut SyncularNativeHandle,
    ref_json: *const c_char,
    file_path: *const c_char,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let ref_json = read_c_string(ref_json)?;
        let file_path = read_c_string(file_path)?;
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.retrieve_blob_file_with_options(&ref_json, &file_path, options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_retrieve_blob_file_json(
    handle: *mut SyncularNativeHandle,
    ref_json: *const c_char,
    file_path: *const c_char,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let ref_json = read_c_string(ref_json)?;
        let file_path = read_c_string(file_path)?;
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.enqueue_retrieve_blob_file_json(&ref_json, &file_path, options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_is_blob_local(
    handle: *mut SyncularNativeHandle,
    hash: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool_value(error_out, || {
        let hash = read_c_string(hash)?;
        with_client(handle, |client| client.is_blob_local(&hash))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_process_blob_upload_queue_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.process_blob_upload_queue_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_process_blob_upload_queue(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.enqueue_process_blob_upload_queue())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_blob_upload_queue_stats_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.blob_upload_queue_stats_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_blob_cache_stats_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.blob_cache_stats_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_prune_blob_cache(
    handle: *mut SyncularNativeHandle,
    max_bytes: i64,
    error_out: *mut *mut c_char,
) -> i64 {
    clear_error(error_out);
    ffi_catch_i64(error_out, || {
        with_client(handle, |client| client.prune_blob_cache(max_bytes))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_prune_blob_cache(
    handle: *mut SyncularNativeHandle,
    max_bytes: i64,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.enqueue_prune_blob_cache(max_bytes))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_clear_blob_cache(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        with_client(handle, |client| client.clear_blob_cache())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_clear_blob_cache(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.enqueue_clear_blob_cache())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_compact_storage_json(
    handle: *mut SyncularNativeHandle,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.compact_storage_json(options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_compact_storage_json(
    handle: *mut SyncularNativeHandle,
    options_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let options_json = read_optional_c_string(options_json)?;
        with_client(handle, |client| {
            client.enqueue_compact_storage_json(options_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_register_query_json(
    handle: *mut SyncularNativeHandle,
    query_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let query_json = read_c_string(query_json)?;
        with_client(handle, |client| client.register_query_json(&query_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_unregister_query(
    handle: *mut SyncularNativeHandle,
    query_id: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let query_id = read_c_string(query_id)?;
        with_client(handle, |client| client.unregister_query(&query_id))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_observed_queries_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.observed_queries_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_diagnostic_snapshot_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.diagnostic_snapshot_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_outbox_summaries_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.outbox_summaries_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_upsert_auth_lease_json(
    handle: *mut SyncularNativeHandle,
    lease_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let lease_json = read_c_string(lease_json)?;
        with_client(handle, |client| client.upsert_auth_lease_json(&lease_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_issue_auth_lease_json(
    handle: *mut SyncularNativeHandle,
    request_json: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let request_json = read_c_string(request_json)?;
        with_client(handle, |client| client.issue_auth_lease_json(&request_json))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_auth_lease_json(
    handle: *mut SyncularNativeHandle,
    lease_id: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let lease_id = read_c_string(lease_id)?;
        with_client(handle, |client| client.auth_lease_json(&lease_id))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_active_auth_leases_json(
    handle: *mut SyncularNativeHandle,
    actor_id: *const c_char,
    now_ms: i64,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let actor_id = read_optional_c_string(actor_id)?;
        with_client(handle, |client| {
            client.active_auth_leases_json(actor_id.as_deref(), now_ms)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_set_outbox_auth_lease_json(
    handle: *mut SyncularNativeHandle,
    client_commit_id: *const c_char,
    provenance_json: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let client_commit_id = read_c_string(client_commit_id)?;
        let provenance_json = read_optional_c_string(provenance_json)?;
        with_client(handle, |client| {
            client.set_outbox_auth_lease_json(&client_commit_id, provenance_json.as_deref())
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_conflict_summaries_json(
    handle: *mut SyncularNativeHandle,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        with_client(handle, |client| client.conflict_summaries_json())
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_resolve_conflict(
    handle: *mut SyncularNativeHandle,
    conflict_id: *const c_char,
    resolution: *const c_char,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        let conflict_id = read_c_string(conflict_id)?;
        let resolution = read_c_string(resolution)?;
        with_client(handle, |client| {
            client.resolve_conflict(&conflict_id, &resolution)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_enqueue_resolve_conflict(
    handle: *mut SyncularNativeHandle,
    id: *const c_char,
    resolution: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let id = read_c_string(id)?;
        let resolution = read_c_string(resolution)?;
        with_client(handle, |client| {
            client.enqueue_resolve_conflict(&id, &resolution)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_retry_conflict_keep_local(
    handle: *mut SyncularNativeHandle,
    conflict_id: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char {
    clear_error(error_out);
    ffi_catch_string(error_out, || {
        let conflict_id = read_c_string(conflict_id)?;
        with_client(handle, |client| {
            client.retry_conflict_keep_local(&conflict_id)
        })
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_client_subscribe_events_json(
    handle: *mut SyncularNativeHandle,
    capacity: u32,
    callback: Option<SyncularNativeEventCallback>,
    error_callback: Option<SyncularNativeEventErrorCallback>,
    user_data: *mut c_void,
    error_out: *mut *mut c_char,
) -> *mut SyncularNativeEventSubscription {
    clear_error(error_out);
    ffi_catch_ptr(error_out, || {
        let callback = callback.ok_or_else(|| {
            SyncularError::message(ErrorKind::Config, "native event callback is null")
        })?;
        let subscription = Arc::new(with_client(handle, |client| {
            Ok(client.subscribe_events(capacity as usize))
        })?);
        let thread_subscription = Arc::clone(&subscription);
        let user_data = user_data as usize;
        let join = thread::spawn(move || {
            while let Some(event) = thread_subscription.next_event_json() {
                match event {
                    Ok(event_json) => call_event_callback(callback, &event_json, user_data),
                    Err(error) => {
                        if let Some(error_callback) = error_callback {
                            let error_json = serde_json::to_string(&NativeErrorInfo::from_error(
                                &error,
                            ))
                            .unwrap_or_else(|_| {
                                r#"{"kind":"Internal","message":"failed to serialize native event error"}"#
                                    .to_string()
                            });
                            call_error_callback(error_callback, &error_json, user_data);
                        }
                    }
                }
            }
        });

        Ok(Box::into_raw(Box::new(SyncularNativeEventSubscription {
            subscription,
            join: Mutex::new(Some(join)),
        })))
    })
}

#[no_mangle]
pub extern "C" fn syncular_native_event_subscription_close(
    handle: *mut SyncularNativeEventSubscription,
    error_out: *mut *mut c_char,
) -> bool {
    clear_error(error_out);
    ffi_catch_bool(error_out, || {
        if handle.is_null() {
            return Ok(());
        }

        let subscription = unsafe { Box::from_raw(handle) };
        subscription.subscription.close();
        if let Ok(mut join) = subscription.join.lock() {
            if let Some(join) = join.take() {
                let _ = join.join();
            }
        }
        Ok(())
    })
}

fn call_event_callback(callback: SyncularNativeEventCallback, event_json: &str, user_data: usize) {
    let value = callback_c_string(event_json);
    callback(value.as_ptr(), user_data as *mut c_void);
}

fn call_error_callback(
    callback: SyncularNativeEventErrorCallback,
    error_json: &str,
    user_data: usize,
) {
    let value = callback_c_string(error_json);
    callback(value.as_ptr(), user_data as *mut c_void);
}

fn callback_c_string(value: &str) -> CString {
    CString::new(value.replace('\0', "\\u0000")).expect("sanitized string should not contain nul")
}

fn with_client<T>(
    handle: *mut SyncularNativeHandle,
    f: impl FnOnce(&mut NativeSyncularClient) -> Result<T>,
) -> Result<T> {
    if handle.is_null() {
        return Err(SyncularError::message(
            ErrorKind::Config,
            "native handle is null",
        ));
    }

    let handle = unsafe { &*handle };
    let mut client = handle
        .client
        .lock()
        .map_err(|_| SyncularError::message(ErrorKind::Internal, "native handle is poisoned"))?;
    f(&mut client)
}

fn with_presence_handle<T>(
    handle: *mut SyncularNativePresenceHandle,
    f: impl FnOnce(&SyncularNativePresenceHandle) -> Result<T>,
) -> Result<T> {
    if handle.is_null() {
        return Err(SyncularError::message(
            ErrorKind::Config,
            "native presence handle is null",
        ));
    }

    let handle = unsafe { &*handle };
    f(handle)
}

fn leave_presence_handle_inner(presence: &SyncularNativePresenceHandle) -> Result<bool> {
    let mut active = presence.active.lock().map_err(|_| {
        SyncularError::message(ErrorKind::Internal, "native presence handle is poisoned")
    })?;
    if !*active {
        return Ok(false);
    }
    with_client(presence.client, |client| {
        client.leave_presence(&presence.scope_key)
    })?;
    *active = false;
    Ok(true)
}

fn with_open_task<T>(
    handle: *mut SyncularNativeOpenHandle,
    f: impl FnOnce(&mut NativeClientOpenTask) -> Result<T>,
) -> Result<T> {
    if handle.is_null() {
        return Err(SyncularError::message(
            ErrorKind::Internal,
            "native async open handle is null",
        ));
    }

    let task = unsafe { &*handle };
    let mut task = task.task.lock().map_err(|_| {
        SyncularError::message(ErrorKind::Internal, "native async open handle is poisoned")
    })?;
    f(&mut task)
}

fn read_c_string(value: *const c_char) -> Result<String> {
    if value.is_null() {
        return Err(SyncularError::config("required string pointer is null"));
    }

    let value = unsafe { CStr::from_ptr(value) };
    value
        .to_str()
        .map(str::to_string)
        .map_err(|err| SyncularError::config(format!("string is not valid UTF-8: {err}")))
}

fn read_optional_c_string(value: *const c_char) -> Result<Option<String>> {
    if value.is_null() {
        return Ok(None);
    }

    read_c_string(value).map(Some)
}

fn ffi_catch_bool(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<()>) -> bool {
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(Ok(())) => true,
        Ok(Err(error)) => {
            write_error(error_out, error);
            false
        }
        Err(payload) => {
            write_error(error_out, panic_error(payload));
            false
        }
    }
}

fn ffi_catch_bool_value(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<bool>) -> bool {
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(Ok(value)) => value,
        Ok(Err(error)) => {
            write_error(error_out, error);
            false
        }
        Err(payload) => {
            write_error(error_out, panic_error(payload));
            false
        }
    }
}

fn ffi_catch_i64(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<i64>) -> i64 {
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(Ok(value)) => value,
        Ok(Err(error)) => {
            write_error(error_out, error);
            0
        }
        Err(payload) => {
            write_error(error_out, panic_error(payload));
            0
        }
    }
}

fn ffi_catch_ptr<T>(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<*mut T>) -> *mut T {
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(Ok(value)) => value,
        Ok(Err(error)) => {
            write_error(error_out, error);
            ptr::null_mut()
        }
        Err(payload) => {
            write_error(error_out, panic_error(payload));
            ptr::null_mut()
        }
    }
}

fn ffi_catch_string(
    error_out: *mut *mut c_char,
    f: impl FnOnce() -> Result<String>,
) -> *mut c_char {
    ffi_catch_ptr(error_out, || f().map(alloc_c_string))
}

fn panic_error(payload: Box<dyn Any + Send>) -> SyncularError {
    let message = payload
        .downcast_ref::<&str>()
        .map(|value| (*value).to_string())
        .or_else(|| payload.downcast_ref::<String>().cloned())
        .unwrap_or_else(|| "unknown panic payload".to_string());
    SyncularError::message(
        ErrorKind::Internal,
        format!("panic crossed FFI boundary: {message}"),
    )
}

fn clear_error(error_out: *mut *mut c_char) {
    if !error_out.is_null() {
        unsafe {
            *error_out = ptr::null_mut();
        }
    }
}

fn write_error(error_out: *mut *mut c_char, error: SyncularError) {
    if error_out.is_null() {
        return;
    }

    let value = serde_json::to_string(&NativeErrorInfo::from_error(&error)).unwrap_or_else(|_| {
        r#"{"kind":"Internal","message":"failed to serialize native error"}"#.to_string()
    });

    unsafe {
        *error_out = alloc_c_string(value);
    }
}

fn alloc_c_string(value: String) -> *mut c_char {
    let sanitized = value.replace('\0', "\\u0000");
    CString::new(sanitized)
        .expect("sanitized string should not contain nul")
        .into_raw()
}

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

    #[test]
    fn ffi_boundary_converts_panics_to_structured_errors() {
        let previous_hook = std::panic::take_hook();
        std::panic::set_hook(Box::new(|_| {}));
        let mut error = ptr::null_mut();
        let ok = ffi_catch_bool(&mut error, || -> Result<()> {
            panic!("ffi boundary test panic");
        });
        std::panic::set_hook(previous_hook);

        assert!(!ok);
        assert!(!error.is_null());
        let error_json = unsafe { CStr::from_ptr(error) }
            .to_str()
            .expect("utf8 error")
            .to_string();
        syncular_string_free(error);

        let value: Value = serde_json::from_str(&error_json).expect("error json");
        assert_eq!(value["kind"], "Internal");
        assert!(value["message"]
            .as_str()
            .unwrap_or_default()
            .contains("panic crossed FFI boundary: ffi boundary test panic"));
    }
}
