#ifndef TRANSACTION_CLIENT_H
#define TRANSACTION_CLIENT_H

#include <stdint.h>
#include <stddef.h>

/**
 * Maximum number of logical transactions that can be active at the same time
 * within a single transaction_client_t instance.
 */
#define MAX_LTXNS 0x100000

/**
 * @typedef transaction_client_t
 * @brief Opaque per-instance state for an OLMDB client.
 *
 * Every call to transaction_client_init() returns a pointer to one of these.
 * Each instance owns its own LMDB environment, shared memory region,
 * transaction table and connection to the commit worker daemon. Multiple
 * instances may safely coexist in the same process (typically one per
 * Node.js / Bun Worker thread). Each instance must only be used from
 * a single thread; instances are independent.
 */
typedef struct transaction_client_struct transaction_client_t;

/**
 * @struct commit_result_t
 * @brief Structure containing the result of a transaction commit operation.
 *
 * @field commit_seq  0 for race/failure, commit sequence number for success.
 * @field ltxn_id     The transaction ID that was committed.
 */
typedef struct {
    size_t commit_seq;
    int ltxn_id;
} commit_result_t;

/**
 * @brief Create a new transaction client, opening (or creating) the database
 *        in the specified directory.
 *
 * Each call creates an independent client with its own LMDB environment,
 * shared memory region and connection to the commit worker daemon. To run
 * the database from multiple threads in the same process, simply create one
 * client per thread. All clients in a process that point at the same
 * database directory share the same commit worker daemon.
 *
 * @param db_dir Path to the database directory, or NULL to use environment variable OLMDB_DIR or default "./.olmdb". Data from this pointer will be copied.
 * @param commit_worker_bin Path to the commit worker binary. Usually `<base_dir>/build/release/commit_worker`. Data from this pointer will be copied.
 * @param set_signal_fd Optional callback function to set and change the file descriptor for the commit worker connection.
 *   When data becomes available on this file descriptor, that's a signal that get_commit_results() should be called.
 *
 * @return  A new transaction_client_t* on success.
 *          NULL on failure; sets error_code/error_message.
 *
 * @error CREATE_DIR_FAILED  Failed to create or open database directory.
 * @error INCONSISTENT_INIT  Failed to resolve database directory.
 * @error OOM                Failed to allocate shared memory or client struct.
 * @error NO_COMMIT_WORKER   Failed to connect to commit worker after multiple attempts (see log file in db dir).
 * @error LMDB*              Various LMDB errors (where * is the negative LMDB error code).
 */
transaction_client_t *transaction_client_init(const char *db_dir, const char *commit_worker_bin, void (*set_signal_fd)(int fd));

/**
 * @brief Destroy a transaction client previously returned by
 *        transaction_client_init(), freeing all its resources (LMDB
 *        environment, shared memory, commit worker connection, etc.).
 *
 * Any open transactions and iterators owned by the client are implicitly
 * dropped. Passing NULL is a no-op.
 *
 * @param client The client to destroy.
 */
void transaction_client_destroy(transaction_client_t *client);

/**
 * @brief Start a new transaction on the given client.
 *
 * @param client  Client returned by transaction_client_init().
 *
 * @return >0: A transaction ID on success.
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error NOT_INIT   Database is not initialized.
 * @error TXN_LIMIT  Transaction limit reached.
 */
int start_transaction(transaction_client_t *client);

/**
 * @brief Commit a transaction.
 *
 * @param client  Client owning the transaction.
 * @param ltxn_id Transaction ID to commit.
 * @param reopen If non-zero, the transaction is kept open after committing.
 *   For read-only transactions, the read context is unchanged.
 *   For write transactions, the write and read logs are cleared and the read context
 *   is refreshed to a snapshot that includes the committed writes (and any concurrent changes).
 *
 * @return 0: The transaction has writes and was queued for asynchronous commit.
 *         >0: The transaction was read-only and was committed immediately, with this commit sequence number.
 *
 * @error INVALID_TRANSACTION Transaction ID not found or already closed.
 */
size_t commit_transaction(transaction_client_t *client, int ltxn_id, int reopen);

/**
 * @brief Abort a transaction.
 *
 * @param client  Client owning the transaction.
 * @param ltxn_id Transaction ID to abort.
 *
 * @return 0: On success.
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_TRANSACTION Transaction ID not found or already closed.
 */
int abort_transaction(transaction_client_t *client, int ltxn_id);

/**
 * @brief Get value for a key within a transaction.
 *
 * @param client      Client owning the transaction.
 * @param ltxn_id     Transaction ID.
 * @param key_data    Pointer to key data.
 * @param key_size    Size of key data in bytes.
 * @param value_data  Pointer to store the value pointer (output parameter). This will point within the
 *   LMDB memory map, so it should not and cannot be modified or freed. This data is guaranteed to be valid
 *   until the transaction ends.
 * @param value_size  Pointer to store the size of retrieved value (output parameter).
 *
 * @return 1: Key was found.
 *         0: Key was not found (not a failure; value_data will be NULL and value_size will be 0).
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_TRANSACTION  Transaction ID not found or already closed.
 * @error KEY_TOO_LONG         Key size exceeds maximum allowed length.
 * @error OOM                  Failed to allocate memory for the operation.
 * @error LMDB*                Various LMDB errors.
 */
int get(transaction_client_t *client, int ltxn_id, const void *key_data, size_t key_size, void **value_data, size_t *value_size);

/**
 * @brief Put a key-value pair within a transaction.
 *
 * @param client      Client owning the transaction.
 * @param ltxn_id     Transaction ID.
 * @param key_data    Pointer to key data.
 * @param key_size    Size of key data in bytes.
 * @param value_data  Pointer to value data.
 * @param value_size  Size of value data in bytes.
 *
 * @return 0: On success.
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_TRANSACTION  Transaction ID not found or already closed.
 * @error KEY_TOO_LONG         Key size exceeds maximum allowed length.
 * @error OOM                  Failed to allocate memory for the operation.
 * @error LMDB*                Various LMDB errors.
 */
int put(transaction_client_t *client, int ltxn_id, const void *key_data, size_t key_size, const void *value_data, size_t value_size);

/**
 * @brief Delete a key within a transaction.
 *
 * @param client      Client owning the transaction.
 * @param ltxn_id     Transaction ID.
 * @param key_data    Pointer to key data.
 * @param key_size    Size of key data in bytes.
 *
 * @return 1: On success.
 *         0: On failure, see error_code/error_message for details.
 *
 * @error INVALID_TRANSACTION  Transaction ID not found or already closed.
 * @error KEY_TOO_LONG         Key size exceeds maximum allowed length.
 * @error OOM                  Failed to allocate memory for the operation.
 * @error LMDB*                Various LMDB errors.
 */
int del(transaction_client_t *client, int ltxn_id, const void *key_data, size_t key_size);

/**
 * @brief Create an iterator for a range of keys.
 *
 * @param client          Client owning the transaction.
 * @param ltxn_id         Transaction ID.
 * @param start_key_data  Pointer to start key data, or NULL to start from the beginning.
 * @param start_key_size  Size of start key data in bytes.
 * @param end_key_data    Pointer to end key data, or NULL for no upper bound.
 * @param end_key_size    Size of end key data in bytes.
 * @param reverse         If non-zero, iterate in reverse order.
 *
 * @return >0: An iterator ID, on success.
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_TRANSACTION  Transaction ID not found or already closed.
 * @error KEY_TOO_LONG         Start or end key size exceeds maximum allowed length.
 * @error OOM                  Failed to allocate memory for the iterator.
 * @error LMDB*                Various LMDB errors.
 */
int create_iterator(transaction_client_t *client, int ltxn_id,
                    const void *start_key_data, size_t start_key_size,
                    const void *end_key_data, size_t end_key_size,
                    int reverse);

/**
 * @brief Read next key-value pair from an iterator.
 *
 * @param client       Client owning the iterator.
 * @param iterator_id  Iterator ID.
 * @param key_data     Pointer to store the key pointer (output parameter). This will point within the
 *   LMDB memory map, so it should not and cannot be modified or freed. This data is guaranteed to be valid
 *   until the transaction ends.
 * @param key_size     Pointer to store the key size (output parameter).
 * @param value_data   Pointer to store the value pointer (output parameter). This will point within the
 *   LMDB memory map, so it should not and cannot be modified or freed. This data is guaranteed to be valid
 *   until the transaction ends.
 * @param value_size   Pointer to store the value size (output parameter).
 *
 * @return 1: A key-value pair was read.
 *         0: Iterator has no more items (not a failure; data pointers will be NULL and sizes will be 0).
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_ITERATOR  Iterator ID not found or already closed.
 * @error LMDB*             Various LMDB errors.
 */
int read_iterator(transaction_client_t *client, int iterator_id,
                  void **key_data, size_t *key_size,
                  void **value_data, size_t *value_size);

/**
 * @brief Close an iterator.
 *
 * @param client       Client owning the iterator.
 * @param iterator_id  Iterator ID to close.
 *
 * @return 0: On success.
 *         -1: On failure, see error_code/error_message for details.
 *
 * @error INVALID_ITERATOR    Iterator ID not found or already closed.
 * @error INVALID_TRANSACTION Transaction ID associated with iterator not found or closed.
 */
int close_iterator(transaction_client_t *client, int iterator_id);

/**
 * @brief Receive and discard all messages available on the commit worker file descriptor.
 *
 * When using something like `poll()` or `select()` to monitor the commit worker file descriptor,
 * this function should be called to drain the file descriptor and to make sure the worker
 * connection is still healthy (and reconnect if necessary).
 *
 * Alternatively, this function can be used in blocking mode (presumably in a secondary thread)
 * to wait until data is available on the commit worker file descriptor.
 *
 * When this functions signals that data has been read, you would normally call `get_commit_results()`
 * to retrieve the results of completed transaction commits.
 *
 * @param client    Client whose commit worker connection to drain.
 * @param blocking  If non-zero, block until results are available. Otherwise, return immediately if
 *   no results are available.
 * @return 1: Data was read from the commit worker file descriptor.
 *         0: No data was available for immediate read in non-blocking mode, or the commit worker
 *   connection was lost and a new connection could not be established.
 */
int drain_signal_fd(transaction_client_t *client, int blocking);

/**
 * @brief Get results of completed asynchronous transaction commits.
 *
 * This function should be called periodically or when data is available on the commit worker file descriptor
 * to retrieve the status of completed transaction commits.
 *
 * @param client       Client whose commit results to drain.
 * @param results      Array to store commit results.
 * @param result_count Input: Maximum number of results to return. Output: Actual number of results returned.
 *
 * @return  0: No more transactions are being committed.
 *          1: Call again later, as more transactions are being committed.
 *          2: Call again now, as there are more results ready than fit in the buffer.
 */
int get_commit_results(transaction_client_t *client, commit_result_t *results, int *result_count);

/**
 * @brief Convert a transaction ID to a slot index for callback arrays.
 *
 * Transaction IDs encode both an index and a nonce. This function extracts
 * the index portion, which can be used as a slot in fixed-size arrays.
 *
 * @param ltxn_id The transaction ID.
 * @return The slot index (0 to MAX_LTXNS-1).
 */
static inline int ltxn_id_to_slot(int ltxn_id) {
    return ltxn_id >> 12;
}

/**
 * Per-thread error message buffer, set when functions return failure codes.
 * Contains a human-readable description of the last error in the calling thread.
 */
extern __thread char error_message[2048];

/**
 * Per-thread error code buffer, set when functions return failure codes.
 * Contains a short string identifier for the last error type in the calling thread.
 * This is useful for programmatic error handling.
 */
extern __thread char error_code[32];

#endif // TRANSACTION_CLIENT_H
