#![allow(unsafe_op_in_unsafe_fn)]

use crate::error::{Error, check};
use crate::ffi;
use crate::merkle::MerkleEntry;
use crate::types::NodeId;

/// `LogIndexEntry` records the current identity state of a node in the log index.
///
/// Each entry captures the node's most recently confirmed active public key, the
/// position of the corresponding entry in the Merkle log, and whether the node has
/// been revoked.
#[derive(Clone, Debug)]
pub struct LogIndexEntry {
    /// The 32-byte node identifier.
    pub node_id: NodeId,
    /// The node's current active Ed25519 public key, 32 bytes.
    pub pubkey: [u8; 32],
    /// The Merkle log index of the entry that last updated this record.
    pub log_index: u64,
    /// `true` if the node's identity has been revoked.
    pub revoked: bool,
}

impl LogIndexEntry {
    pub(crate) fn to_ffi(&self) -> ffi::nwep_log_index_entry {
        ffi::nwep_log_index_entry {
            nodeid: ffi::nwep_nodeid {
                data: self.node_id.0,
            },
            pubkey: self.pubkey,
            log_index: self.log_index,
            revoked: self.revoked as i32,
        }
    }

    pub(crate) fn from_ffi(e: &ffi::nwep_log_index_entry) -> Self {
        LogIndexEntry {
            node_id: NodeId(e.nodeid.data),
            pubkey: e.pubkey,
            log_index: e.log_index,
            revoked: e.revoked != 0,
        }
    }
}

/// `LogIndexStorage` is the persistence backend for [`LogIndex`].
///
/// Implement this trait to store index entries in any backend (in-memory `HashMap`,
/// LevelDB, SQLite, etc.). The trait is object-safe and requires `Send + 'static`
/// so it can be moved into the [`LogIndex`] and accessed from the event loop thread.
pub trait LogIndexStorage: Send + 'static {
    /// `get` retrieves the current [`LogIndexEntry`] for a node.
    ///
    /// Returns `Err` if no entry exists for `node_id`.
    fn get(&self, node_id: &NodeId) -> Result<LogIndexEntry, Error>;
    /// `put` stores or updates the [`LogIndexEntry`] for a node.
    ///
    /// Called by [`LogIndex::update`] whenever the log advances for a node.
    fn put(&mut self, entry: &LogIndexEntry) -> Result<(), Error>;
}

struct StorageCallbacks {
    storage: Box<dyn LogIndexStorage>,
}

unsafe extern "C" fn index_get_cb(
    user_data: *mut std::ffi::c_void,
    nodeid: *const ffi::nwep_nodeid,
    entry: *mut ffi::nwep_log_index_entry,
) -> std::ffi::c_int {
    let cb = &*(user_data as *mut StorageCallbacks);
    let nid = NodeId((*nodeid).data);
    match cb.storage.get(&nid) {
        Ok(e) => {
            *entry = e.to_ffi();
            0
        }
        Err(e) => e.code,
    }
}

unsafe extern "C" fn index_put_cb(
    user_data: *mut std::ffi::c_void,
    entry: *const ffi::nwep_log_index_entry,
) -> std::ffi::c_int {
    let cb = &mut *(user_data as *mut StorageCallbacks);
    let e = LogIndexEntry::from_ffi(&*entry);
    match cb.storage.put(&e) {
        Ok(()) => 0,
        Err(e) => e.code,
    }
}

/// `LogIndex` provides O(1) lookup of the current identity state for any node in the Merkle log.
///
/// `LogIndex` wraps the C library's log index and delegates persistence to a
/// user-supplied [`LogIndexStorage`] backend. It must be kept in sync with the Merkle
/// log by calling [`update`](LogIndex::update) for each new entry appended to the log.
pub struct LogIndex {
    ptr: *mut ffi::nwep_log_index,
    _storage: Box<StorageCallbacks>,
}

unsafe impl Send for LogIndex {}

impl LogIndex {
    /// `new` creates a `LogIndex` backed by the given storage implementation.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the underlying C allocation fails.
    pub fn new(storage: impl LogIndexStorage) -> Result<Self, Error> {
        let mut cb = Box::new(StorageCallbacks {
            storage: Box::new(storage),
        });
        let ffi_storage = ffi::nwep_log_index_storage {
            get: Some(index_get_cb),
            put: Some(index_put_cb),
            user_data: cb.as_mut() as *mut _ as *mut std::ffi::c_void,
        };
        let mut ptr: *mut ffi::nwep_log_index = std::ptr::null_mut();
        check(unsafe { ffi::nwep_log_index_new(&mut ptr, &ffi_storage) })?;
        Ok(LogIndex { ptr, _storage: cb })
    }

    /// `lookup` retrieves the current identity state for a node.
    ///
    /// # Errors
    ///
    /// Returns `Err` if no entry exists for `node_id` or if the storage backend fails.
    pub fn lookup(&mut self, node_id: &NodeId) -> Result<LogIndexEntry, Error> {
        let ffi_nid = ffi::nwep_nodeid { data: node_id.0 };
        let mut entry = unsafe { std::mem::zeroed::<ffi::nwep_log_index_entry>() };
        check(unsafe { ffi::nwep_log_index_lookup(self.ptr, &ffi_nid, &mut entry) })?;
        Ok(LogIndexEntry::from_ffi(&entry))
    }

    /// `update` processes a new Merkle log entry and updates the index accordingly.
    ///
    /// `log_idx` is the position of `entry` in the Merkle log (0-based). Call this
    /// for every entry appended to the log in order to keep the index in sync.
    ///
    /// # Errors
    ///
    /// Returns `Err` if the entry is malformed or the storage backend fails.
    pub fn update(&mut self, entry: &MerkleEntry, log_idx: u64) -> Result<(), Error> {
        let ffi_entry = entry.to_ffi();
        check(unsafe { ffi::nwep_log_index_update(self.ptr, &ffi_entry, log_idx) })
    }
}

impl Drop for LogIndex {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::nwep_log_index_free(self.ptr) }
        }
    }
}
