#![allow(unsafe_code)]
//! quicproquo-ffi -- C FFI bindings for quicproquo messaging operations.
//!
//! Provides a synchronous C API that wraps the async quicproquo-client library.
//! Each `QpqHandle` owns a Tokio runtime; FFI functions use `runtime.block_on()`
//! to bridge from synchronous C callers to the async Rust internals.
//!
//! # Safety
//!
//! All FFI functions are `unsafe extern "C"` -- callers must ensure pointers
//! are valid and strings are null-terminated UTF-8.

use std::ffi::{CStr, CString, c_char};
use std::path::PathBuf;

use tokio::runtime::Runtime;

// Status codes returned by FFI functions.
pub const QPQ_OK: i32 = 0;
pub const QPQ_ERROR: i32 = 1;
pub const QPQ_AUTH_FAILED: i32 = 2;
pub const QPQ_TIMEOUT: i32 = 3;
pub const QPQ_NOT_CONNECTED: i32 = 4;

/// Opaque handle exposed to C callers via pointer.
pub struct QpqHandle {
    runtime: Runtime,
    server: String,
    ca_cert: PathBuf,
    server_name: String,
    state_path: PathBuf,
    state_password: Option<String>,
    logged_in: bool,
    last_error: Option<CString>,
}

impl QpqHandle {
    fn set_error(&mut self, msg: &str) {
        self.last_error = CString::new(msg).ok();
    }
}

// ---------------------------------------------------------------------------
// Error classification
// ---------------------------------------------------------------------------

/// Classify an `anyhow::Error` from `cmd_login` into an FFI status code.
///
/// Checks the error chain for typed downcasting before falling back to
/// message-based heuristics.
fn classify_login_error(err: &anyhow::Error) -> i32 {
    // Check error chain for OPAQUE-specific typed errors.
    for cause in err.chain() {
        // capnp::Error indicates transport/RPC failure.
        if cause.downcast_ref::<capnp::Error>().is_some() {
            return QPQ_ERROR;
        }
    }
    // Fall back to message inspection for OPAQUE authentication failures,
    // since opaque-ke errors are converted to anyhow strings upstream.
    let msg = format!("{err:#}");
    if msg.contains("OPAQUE") || msg.contains("bad password") || msg.contains("credential") {
        QPQ_AUTH_FAILED
    } else {
        QPQ_ERROR
    }
}

/// Classify an `anyhow::Error` from receive operations into an FFI status code.
fn classify_receive_error(err: &anyhow::Error) -> i32 {
    let msg = format!("{err:#}");
    if msg.contains("timeout") || msg.contains("Timeout") || msg.contains("timed out") {
        QPQ_TIMEOUT
    } else {
        QPQ_ERROR
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Convert a `*const c_char` to `&str`, returning `None` on null or invalid UTF-8.
unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
    if ptr.is_null() {
        return None;
    }
    CStr::from_ptr(ptr).to_str().ok()
}

// ---------------------------------------------------------------------------
// FFI functions
// ---------------------------------------------------------------------------

/// Create a new handle and connect to the quicproquo server.
///
/// Returns a heap-allocated `QpqHandle` pointer on success, or null on failure.
///
/// # Parameters
/// - `server`: server address as `host:port` (null-terminated UTF-8).
/// - `ca_cert`: path to the CA certificate file (null-terminated UTF-8).
/// - `server_name`: TLS server name (null-terminated UTF-8).
///
/// # Safety
/// All pointer arguments must be valid, non-null, null-terminated C strings.
#[no_mangle]
pub unsafe extern "C" fn qpq_connect(
    server: *const c_char,
    ca_cert: *const c_char,
    server_name: *const c_char,
) -> *mut QpqHandle {
    let server_str = match cstr_to_str(server) {
        Some(s) => s,
        None => return std::ptr::null_mut(),
    };
    let ca_cert_str = match cstr_to_str(ca_cert) {
        Some(s) => s,
        None => return std::ptr::null_mut(),
    };
    let server_name_str = match cstr_to_str(server_name) {
        Some(s) => s,
        None => return std::ptr::null_mut(),
    };

    let rt = match Runtime::new() {
        Ok(r) => r,
        Err(_) => return std::ptr::null_mut(),
    };

    // Verify connectivity by performing a health check.
    let ca_path = PathBuf::from(ca_cert_str);
    let connected = rt.block_on(async {
        quicproquo_client::cmd_health(server_str, &ca_path, server_name_str).await
    });

    if let Err(e) = connected {
        // Cannot store error in handle since we failed to build one.
        eprintln!("qpq_connect: health check failed: {e}");
        return std::ptr::null_mut();
    }

    // Derive a default state path from the server address.
    let state_path = PathBuf::from(format!("qpq-ffi-{server_str}.bin"));

    let handle = Box::new(QpqHandle {
        runtime: rt,
        server: server_str.to_string(),
        ca_cert: ca_path,
        server_name: server_name_str.to_string(),
        state_path,
        state_password: None,
        logged_in: false,
        last_error: None,
    });
    Box::into_raw(handle)
}

/// Authenticate with the server using OPAQUE (username + password).
///
/// On success the handle is marked as logged-in and subsequent send/receive
/// calls will use the authenticated session.
///
/// Returns `QPQ_OK` on success, `QPQ_AUTH_FAILED` on bad credentials,
/// or `QPQ_ERROR` on other failures.
///
/// # Safety
/// - `handle` must be a valid pointer from `qpq_connect`.
/// - `username` and `password` must be valid null-terminated C strings.
#[no_mangle]
pub unsafe extern "C" fn qpq_login(
    handle: *mut QpqHandle,
    username: *const c_char,
    password: *const c_char,
) -> i32 {
    let h = match handle.as_mut() {
        Some(h) => h,
        None => return QPQ_NOT_CONNECTED,
    };

    let user = match cstr_to_str(username) {
        Some(s) => s,
        None => {
            h.set_error("invalid username pointer");
            return QPQ_ERROR;
        }
    };
    let pass = match cstr_to_str(password) {
        Some(s) => s,
        None => {
            h.set_error("invalid password pointer");
            return QPQ_ERROR;
        }
    };

    // Update state path to be username-specific.
    h.state_path = PathBuf::from(format!("qpq-ffi-{user}.bin"));

    let result = h.runtime.block_on(async {
        quicproquo_client::cmd_login(
            &h.server,
            &h.ca_cert,
            &h.server_name,
            user,
            pass,
            None,                          // identity_key_hex
            Some(h.state_path.as_path()),  // state_path
            h.state_password.as_deref(),   // state_password
        )
        .await
    });

    match result {
        Ok(()) => {
            h.logged_in = true;
            QPQ_OK
        }
        Err(e) => {
            let msg = format!("{e:#}");
            let code = classify_login_error(&e);
            h.set_error(&msg);
            code
        }
    }
}

/// Send a message to a recipient (by username).
///
/// The message is encrypted via MLS before delivery. The `message` buffer
/// does not need to be null-terminated; `message_len` specifies its length.
///
/// Returns `QPQ_OK` on success.
///
/// # Safety
/// - `handle` must be a valid pointer from `qpq_connect`.
/// - `recipient` must be a valid null-terminated C string.
/// - `message` must point to at least `message_len` readable bytes.
#[no_mangle]
pub unsafe extern "C" fn qpq_send(
    handle: *mut QpqHandle,
    recipient: *const c_char,
    message: *const u8,
    message_len: usize,
) -> i32 {
    let h = match handle.as_mut() {
        Some(h) => h,
        None => return QPQ_NOT_CONNECTED,
    };

    if !h.logged_in {
        h.set_error("not logged in");
        return QPQ_NOT_CONNECTED;
    }

    let rcpt = match cstr_to_str(recipient) {
        Some(s) => s,
        None => {
            h.set_error("invalid recipient pointer");
            return QPQ_ERROR;
        }
    };

    if message.is_null() || message_len == 0 {
        h.set_error("empty message");
        return QPQ_ERROR;
    }
    let msg_bytes = std::slice::from_raw_parts(message, message_len);
    let msg_str = match std::str::from_utf8(msg_bytes) {
        Ok(s) => s,
        Err(e) => {
            h.set_error(&format!("message is not valid UTF-8: {e}"));
            return QPQ_ERROR;
        }
    };

    // Resolve recipient username to identity key, then send.
    let result = h.runtime.block_on(async {
        let node_client =
            quicproquo_client::connect_node(&h.server, &h.ca_cert, &h.server_name).await?;
        let peer_key = quicproquo_client::resolve_user(&node_client, rcpt)
            .await?
            .ok_or_else(|| anyhow::anyhow!("recipient '{rcpt}' not found"))?;
        let peer_key_hex = hex::encode(&peer_key);

        quicproquo_client::cmd_send(
            &h.state_path,
            &h.server,
            &h.ca_cert,
            &h.server_name,
            Some(&peer_key_hex),
            false,    // send_to_all
            msg_str,
            h.state_password.as_deref(),
        )
        .await
    });

    match result {
        Ok(()) => QPQ_OK,
        Err(e) => {
            h.set_error(&format!("{e:#}"));
            QPQ_ERROR
        }
    }
}

/// Receive pending messages, blocking up to `timeout_ms` milliseconds.
///
/// On success, `*out_json` is set to a heap-allocated null-terminated JSON
/// string containing an array of received message objects. The caller must
/// free this string with `qpq_free_string`.
///
/// Returns `QPQ_OK` on success (even if the array is empty),
/// `QPQ_TIMEOUT` if the wait expires with no messages.
///
/// # Safety
/// - `handle` must be a valid pointer from `qpq_connect`.
/// - `out_json` must be a valid pointer to a `*mut c_char`.
#[no_mangle]
pub unsafe extern "C" fn qpq_receive(
    handle: *mut QpqHandle,
    timeout_ms: u32,
    out_json: *mut *mut c_char,
) -> i32 {
    let h = match handle.as_mut() {
        Some(h) => h,
        None => return QPQ_NOT_CONNECTED,
    };

    if !h.logged_in {
        h.set_error("not logged in");
        return QPQ_NOT_CONNECTED;
    }

    if out_json.is_null() {
        h.set_error("out_json is null");
        return QPQ_ERROR;
    }

    let result = h.runtime.block_on(async {
        quicproquo_client::receive_pending_plaintexts(
            &h.state_path,
            &h.server,
            &h.ca_cert,
            &h.server_name,
            timeout_ms as u64,
            h.state_password.as_deref(),
        )
        .await
    });

    match result {
        Ok(plaintexts) => {
            // Convert raw byte payloads to a JSON array of base64 or lossy-UTF-8 strings.
            let messages: Vec<String> = plaintexts
                .iter()
                .map(|pt| String::from_utf8_lossy(pt).into_owned())
                .collect();

            let json = match serde_json::to_string(&messages) {
                Ok(j) => j,
                Err(e) => {
                    h.set_error(&format!("JSON serialisation failed: {e}"));
                    return QPQ_ERROR;
                }
            };

            match CString::new(json) {
                Ok(cs) => {
                    *out_json = cs.into_raw();
                    QPQ_OK
                }
                Err(e) => {
                    h.set_error(&format!("CString conversion failed: {e}"));
                    QPQ_ERROR
                }
            }
        }
        Err(e) => {
            let msg = format!("{e:#}");
            let code = classify_receive_error(&e);
            h.set_error(&msg);
            code
        }
    }
}

/// Disconnect and free the handle.
///
/// After this call, `handle` must not be used again.
///
/// # Safety
/// `handle` must be a valid pointer from `qpq_connect`, or null (no-op).
#[no_mangle]
pub unsafe extern "C" fn qpq_disconnect(handle: *mut QpqHandle) {
    if !handle.is_null() {
        let _ = Box::from_raw(handle);
    }
}

/// Return the last error message, or null if no error has been recorded.
///
/// The returned pointer is valid until the next FFI call on this handle.
/// Do **not** free the returned pointer; it is owned by the handle.
///
/// # Safety
/// `handle` must be a valid pointer from `qpq_connect`, or null (returns null).
#[no_mangle]
pub unsafe extern "C" fn qpq_last_error(handle: *const QpqHandle) -> *const c_char {
    match handle.as_ref() {
        Some(h) => match &h.last_error {
            Some(cs) => cs.as_ptr(),
            None => std::ptr::null(),
        },
        None => std::ptr::null(),
    }
}

/// Free a string previously returned by `qpq_receive` (via `out_json`).
///
/// # Safety
/// `ptr` must have been allocated by this library (via `CString::into_raw`),
/// or null (no-op).
#[no_mangle]
pub unsafe extern "C" fn qpq_free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        let _ = CString::from_raw(ptr);
    }
}
