use guerrillamail_client::{Client, ClientBuilder, EmailDetails, Error, Message};
use std::cell::RefCell;
use std::ffi::{CStr, c_char};
use std::mem;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr;
use std::time::Duration;
use tokio::runtime::{Builder as RuntimeBuilder, Runtime};

thread_local! {
    static LAST_ERROR: RefCell<Option<Vec<u8>>> = const { RefCell::new(None) };
}

#[repr(C)]
pub struct gm_builder_t {
    _private: [u8; 0],
}

#[repr(C)]
pub struct gm_client_t {
    _private: [u8; 0],
}

#[repr(C)]
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum gm_status_t {
    GM_OK = 0,
    GM_ERR_NULL = 1,
    GM_ERR_INVALID_ARGUMENT = 2,
    GM_ERR_REQUEST = 3,
    GM_ERR_RESPONSE_PARSE = 4,
    GM_ERR_TOKEN_PARSE = 5,
    GM_ERR_JSON = 6,
    GM_ERR_INTERNAL = 7,
}

#[repr(C)]
#[derive(Default, Debug)]
pub struct gm_string_t {
    pub ptr: *mut c_char,
    pub len: usize,
}

#[repr(C)]
#[derive(Debug)]
pub struct gm_message_t {
    pub mail_id: gm_string_t,
    pub mail_from: gm_string_t,
    pub mail_subject: gm_string_t,
    pub mail_excerpt: gm_string_t,
    pub mail_timestamp: gm_string_t,
}

#[repr(C)]
#[derive(Default, Debug)]
pub struct gm_message_list_t {
    pub ptr: *mut gm_message_t,
    pub len: usize,
}

#[repr(C)]
#[derive(Debug)]
pub struct gm_email_details_t {
    pub mail_id: gm_string_t,
    pub mail_from: gm_string_t,
    pub mail_subject: gm_string_t,
    pub mail_body: gm_string_t,
    pub mail_timestamp: gm_string_t,
    pub attachment_count: u32,
    pub has_attachment_count: bool,
}

type BuilderState = ClientBuilder;

struct ClientHandle {
    runtime: Runtime,
    client: Client,
}

fn with_ffi_boundary<F>(f: F) -> gm_status_t
where
    F: FnOnce() -> Result<(), gm_status_t>,
{
    clear_last_error();
    match catch_unwind(AssertUnwindSafe(f)) {
        Ok(Ok(())) => gm_status_t::GM_OK,
        Ok(Err(status)) => status,
        Err(_) => {
            set_last_error("panic while executing FFI function");
            gm_status_t::GM_ERR_INTERNAL
        }
    }
}

fn ignore_panic<F: FnOnce()>(f: F) {
    let _ = catch_unwind(AssertUnwindSafe(f));
}

fn set_last_error(message: impl Into<String>) {
    let mut bytes = message.into().into_bytes();
    bytes.push(0);
    LAST_ERROR.with(|cell| {
        *cell.borrow_mut() = Some(bytes);
    });
}

fn clear_last_error() {
    LAST_ERROR.with(|cell| {
        *cell.borrow_mut() = None;
    });
}

fn status_from_error(error: Error) -> gm_status_t {
    let status = match error {
        Error::Request(_) => gm_status_t::GM_ERR_REQUEST,
        Error::ResponseParse(_) => gm_status_t::GM_ERR_RESPONSE_PARSE,
        Error::TokenParse => gm_status_t::GM_ERR_TOKEN_PARSE,
        Error::Json(_) => gm_status_t::GM_ERR_JSON,
        Error::Regex(_) | Error::HeaderValue(_) | Error::DomainParse => {
            gm_status_t::GM_ERR_INTERNAL
        }
    };
    set_last_error(error.to_string());
    status
}

fn runtime_new() -> Result<Runtime, gm_status_t> {
    RuntimeBuilder::new_multi_thread()
        .enable_all()
        .build()
        .map_err(|error| {
            set_last_error(format!("failed to create Tokio runtime: {error}"));
            gm_status_t::GM_ERR_INTERNAL
        })
}

fn null_error(message: &str) -> gm_status_t {
    set_last_error(message);
    gm_status_t::GM_ERR_NULL
}

fn invalid_arg(message: &str) -> gm_status_t {
    set_last_error(message);
    gm_status_t::GM_ERR_INVALID_ARGUMENT
}

unsafe fn builder_state_mut<'a>(
    builder: *mut gm_builder_t,
) -> Result<&'a mut BuilderState, gm_status_t> {
    if builder.is_null() {
        return Err(null_error("builder is null"));
    }
    Ok(unsafe { &mut *builder.cast::<BuilderState>() })
}

unsafe fn client_handle_ref<'a>(client: *mut gm_client_t) -> Result<&'a ClientHandle, gm_status_t> {
    if client.is_null() {
        return Err(null_error("client is null"));
    }
    Ok(unsafe { &*client.cast::<ClientHandle>() })
}

unsafe fn read_required_str(ptr: *const c_char, name: &str) -> Result<String, gm_status_t> {
    if ptr.is_null() {
        return Err(null_error(&format!("{name} is null")));
    }
    let value = unsafe { CStr::from_ptr(ptr) };
    value
        .to_str()
        .map(str::to_owned)
        .map_err(|_| invalid_arg(&format!("{name} is not valid UTF-8")))
}

fn into_gm_string(value: String) -> gm_string_t {
    let mut bytes = value.into_bytes();
    let len = bytes.len();
    bytes.push(0);
    let ptr = bytes.as_mut_ptr().cast::<c_char>();
    mem::forget(bytes);
    gm_string_t { ptr, len }
}

fn free_gm_string(value: &mut gm_string_t) {
    if value.ptr.is_null() {
        value.len = 0;
        return;
    }
    let len_with_nul = value.len.saturating_add(1);
    unsafe {
        let _ = Vec::from_raw_parts(value.ptr.cast::<u8>(), len_with_nul, len_with_nul);
    }
    value.ptr = ptr::null_mut();
    value.len = 0;
}

fn into_gm_message(message: Message) -> gm_message_t {
    gm_message_t {
        mail_id: into_gm_string(message.mail_id),
        mail_from: into_gm_string(message.mail_from),
        mail_subject: into_gm_string(message.mail_subject),
        mail_excerpt: into_gm_string(message.mail_excerpt),
        mail_timestamp: into_gm_string(message.mail_timestamp),
    }
}

fn free_gm_message(message: &mut gm_message_t) {
    free_gm_string(&mut message.mail_id);
    free_gm_string(&mut message.mail_from);
    free_gm_string(&mut message.mail_subject);
    free_gm_string(&mut message.mail_excerpt);
    free_gm_string(&mut message.mail_timestamp);
}

fn into_gm_email_details(details: EmailDetails) -> gm_email_details_t {
    gm_email_details_t {
        mail_id: into_gm_string(details.mail_id),
        mail_from: into_gm_string(details.mail_from),
        mail_subject: into_gm_string(details.mail_subject),
        mail_body: into_gm_string(details.mail_body),
        mail_timestamp: into_gm_string(details.mail_timestamp),
        attachment_count: details.attachment_count.unwrap_or(0),
        has_attachment_count: details.attachment_count.is_some(),
    }
}

fn free_gm_email_details_fields(details: &mut gm_email_details_t) {
    free_gm_string(&mut details.mail_id);
    free_gm_string(&mut details.mail_from);
    free_gm_string(&mut details.mail_subject);
    free_gm_string(&mut details.mail_body);
    free_gm_string(&mut details.mail_timestamp);
    details.attachment_count = 0;
    details.has_attachment_count = false;
}

fn build_client_handle(state: &BuilderState) -> Result<*mut gm_client_t, gm_status_t> {
    let runtime = runtime_new()?;
    let client = runtime
        .block_on(state.clone().build())
        .map_err(status_from_error)?;
    let handle = ClientHandle { runtime, client };
    Ok(Box::into_raw(Box::new(handle)).cast::<gm_client_t>())
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_new(out_builder: *mut *mut gm_builder_t) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_builder.is_null() {
            return Err(null_error("out_builder is null"));
        }
        let builder = Box::new(Client::builder());
        unsafe {
            *out_builder = Box::into_raw(builder).cast::<gm_builder_t>();
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_free(builder: *mut gm_builder_t) {
    ignore_panic(|| {
        if builder.is_null() {
            return;
        }
        unsafe {
            let _ = Box::from_raw(builder.cast::<BuilderState>());
        }
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_set_proxy(
    builder: *mut gm_builder_t,
    proxy: *const c_char,
) -> gm_status_t {
    with_ffi_boundary(|| {
        let state = unsafe { builder_state_mut(builder)? };
        let proxy = unsafe { read_required_str(proxy, "proxy")? };
        *state = state.clone().proxy(proxy);
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_set_danger_accept_invalid_certs(
    builder: *mut gm_builder_t,
    value: bool,
) -> gm_status_t {
    with_ffi_boundary(|| {
        let state = unsafe { builder_state_mut(builder)? };
        *state = state.clone().danger_accept_invalid_certs(value);
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_set_user_agent(
    builder: *mut gm_builder_t,
    user_agent: *const c_char,
) -> gm_status_t {
    with_ffi_boundary(|| {
        let state = unsafe { builder_state_mut(builder)? };
        let user_agent = unsafe { read_required_str(user_agent, "user_agent")? };
        *state = state.clone().user_agent(user_agent);
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_set_timeout_ms(
    builder: *mut gm_builder_t,
    timeout_ms: u64,
) -> gm_status_t {
    with_ffi_boundary(|| {
        let state = unsafe { builder_state_mut(builder)? };
        if timeout_ms == 0 {
            return Err(invalid_arg("timeout_ms must be greater than zero"));
        }
        *state = state.clone().timeout(Duration::from_millis(timeout_ms));
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_builder_build(
    builder: *mut gm_builder_t,
    out_client: *mut *mut gm_client_t,
) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_client.is_null() {
            return Err(null_error("out_client is null"));
        }
        let state = unsafe { builder_state_mut(builder)? };
        let client = build_client_handle(state)?;
        unsafe {
            *out_client = client;
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_new_default(out_client: *mut *mut gm_client_t) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_client.is_null() {
            return Err(null_error("out_client is null"));
        }
        let client = build_client_handle(&Client::builder())?;
        unsafe {
            *out_client = client;
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_free(client: *mut gm_client_t) {
    ignore_panic(|| {
        if client.is_null() {
            return;
        }
        unsafe {
            let _ = Box::from_raw(client.cast::<ClientHandle>());
        }
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_create_email(
    client: *mut gm_client_t,
    alias: *const c_char,
    out_email: *mut gm_string_t,
) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_email.is_null() {
            return Err(null_error("out_email is null"));
        }
        let handle = unsafe { client_handle_ref(client)? };
        let alias = unsafe { read_required_str(alias, "alias")? };
        let email = handle
            .runtime
            .block_on(handle.client.create_email(&alias))
            .map_err(status_from_error)?;
        unsafe {
            *out_email = into_gm_string(email);
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_get_messages(
    client: *mut gm_client_t,
    email: *const c_char,
    out_messages: *mut gm_message_list_t,
) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_messages.is_null() {
            return Err(null_error("out_messages is null"));
        }
        let handle = unsafe { client_handle_ref(client)? };
        let email = unsafe { read_required_str(email, "email")? };
        let messages = handle
            .runtime
            .block_on(handle.client.get_messages(&email))
            .map_err(status_from_error)?;
        let boxed = messages
            .into_iter()
            .map(into_gm_message)
            .collect::<Vec<_>>()
            .into_boxed_slice();
        let len = boxed.len();
        let ptr = Box::into_raw(boxed).cast::<gm_message_t>();
        unsafe {
            (*out_messages).ptr = ptr;
            (*out_messages).len = len;
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_fetch_email(
    client: *mut gm_client_t,
    email: *const c_char,
    mail_id: *const c_char,
    out_details: *mut *mut gm_email_details_t,
) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_details.is_null() {
            return Err(null_error("out_details is null"));
        }
        let handle = unsafe { client_handle_ref(client)? };
        let email = unsafe { read_required_str(email, "email")? };
        let mail_id = unsafe { read_required_str(mail_id, "mail_id")? };
        let details = handle
            .runtime
            .block_on(handle.client.fetch_email(&email, &mail_id))
            .map_err(status_from_error)?;
        let details = Box::new(into_gm_email_details(details));
        unsafe {
            *out_details = Box::into_raw(details);
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_client_delete_email(
    client: *mut gm_client_t,
    email: *const c_char,
    out_deleted: *mut bool,
) -> gm_status_t {
    with_ffi_boundary(|| {
        if out_deleted.is_null() {
            return Err(null_error("out_deleted is null"));
        }
        let handle = unsafe { client_handle_ref(client)? };
        let email = unsafe { read_required_str(email, "email")? };
        let deleted = handle
            .runtime
            .block_on(handle.client.delete_email(&email))
            .map_err(status_from_error)?;
        unsafe {
            *out_deleted = deleted;
        }
        Ok(())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_string_free(value: *mut gm_string_t) {
    ignore_panic(|| {
        if value.is_null() {
            return;
        }
        let value = unsafe { &mut *value };
        free_gm_string(value);
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_message_list_free(messages: *mut gm_message_list_t) {
    ignore_panic(|| {
        if messages.is_null() {
            return;
        }
        let messages = unsafe { &mut *messages };
        if !messages.ptr.is_null() {
            let slice_ptr = ptr::slice_from_raw_parts_mut(messages.ptr, messages.len);
            let mut boxed = unsafe { Box::from_raw(slice_ptr) };
            for message in boxed.iter_mut() {
                free_gm_message(message);
            }
        }
        messages.ptr = ptr::null_mut();
        messages.len = 0;
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_email_details_free(details: *mut gm_email_details_t) {
    ignore_panic(|| {
        if details.is_null() {
            return;
        }
        let mut details = unsafe { Box::from_raw(details) };
        free_gm_email_details_fields(&mut details);
    });
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_last_error_message() -> *const c_char {
    LAST_ERROR.with(|cell| {
        cell.borrow()
            .as_ref()
            .map(|bytes| bytes.as_ptr().cast::<c_char>())
            .unwrap_or(ptr::null())
    })
}

#[unsafe(no_mangle)]
pub extern "C" fn gm_last_error_clear() {
    clear_last_error();
}

#[cfg(test)]
mod tests {
    use super::*;
    fn c_string_ptr(value: &str) -> Vec<u8> {
        let mut bytes = value.as_bytes().to_vec();
        bytes.push(0);
        bytes
    }

    #[test]
    fn gm_string_free_is_null_safe() {
        gm_string_free(ptr::null_mut());

        let mut value = gm_string_t::default();
        gm_string_free(&mut value);
        assert!(value.ptr.is_null());
        assert_eq!(value.len, 0);
    }

    #[test]
    fn gm_builder_set_timeout_rejects_zero() {
        let mut builder = ptr::null_mut();
        assert_eq!(gm_builder_new(&mut builder), gm_status_t::GM_OK);

        let status = gm_builder_set_timeout_ms(builder, 0);
        assert_eq!(status, gm_status_t::GM_ERR_INVALID_ARGUMENT);
        let error = unsafe { CStr::from_ptr(gm_last_error_message()) }
            .to_str()
            .expect("utf8 error");
        assert!(error.contains("timeout_ms"));

        gm_builder_free(builder);
    }

    #[test]
    fn gm_builder_set_proxy_rejects_null() {
        let mut builder = ptr::null_mut();
        assert_eq!(gm_builder_new(&mut builder), gm_status_t::GM_OK);

        let status = gm_builder_set_proxy(builder, ptr::null());
        assert_eq!(status, gm_status_t::GM_ERR_NULL);

        gm_builder_free(builder);
    }

    #[test]
    fn null_client_reports_error() {
        let alias = c_string_ptr("demoalias");
        let mut email = gm_string_t::default();
        let status =
            gm_client_create_email(ptr::null_mut(), alias.as_ptr().cast::<c_char>(), &mut email);
        assert_eq!(status, gm_status_t::GM_ERR_NULL);
        let error = unsafe { CStr::from_ptr(gm_last_error_message()) }
            .to_str()
            .expect("utf8 error");
        assert!(error.contains("client"));
    }
}
