
/*
 *   Copyright 2016-2026. Couchbase, Inc.
 *   All Rights Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

#include "transactions.hxx"
#include "../cpp_core_enums_autogen.hxx"
#include "../exceptions.hxx"
#include "../operations_autogen.hxx"
#include "../pytype_utils.hxx"
#include "../utils.hxx"
#include "cpp_core_transactions_types.hxx"
#include <core/cluster.hxx>
#include <core/operations.hxx>
#include <core/transactions/durability_level.hxx>
#include <core/transactions/internal/exceptions_internal.hxx>
#include <core/transactions/transaction_get_result.hxx>
#include <core/utils/json.hxx>
#include <couchbase/query_scan_consistency.hxx>
#include <sstream>

void
pycbc::txns::dealloc_transactions(PyObject* obj)
{
  auto txns = reinterpret_cast<pycbc::txns::transactions*>(PyCapsule_GetPointer(obj, "txns_"));
  txns->txns->close();
  txns->txns.reset();
  CB_LOG_DEBUG("dealloc transactions");
}

void
pycbc::txns::dealloc_transaction_context(PyObject* obj)
{
  auto ctx = reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(obj, "ctx_"));
  delete ctx;
  CB_LOG_DEBUG("dealloc transaction_context");
}

/* pycbc::txns::transaction_config type methods */

void
pycbc::txns::transaction_config__dealloc__(pycbc::txns::transaction_config* cfg)
{
  delete cfg->cfg;
  Py_TYPE(cfg)->tp_free((PyObject*)cfg);
  CB_LOG_DEBUG("dealloc transaction_config");
}

PyObject*
pycbc::txns::transaction_config__to_dict__(PyObject* self)
{
  auto conf = reinterpret_cast<pycbc::txns::transaction_config*>(self);
  PyObject* retval = PyDict_New();
  add_field(retval, "durability_level", static_cast<int64_t>(conf->cfg->durability_level()));
  add_field(retval,
            "cleanup_window",
            static_cast<int64_t>(conf->cfg->cleanup_config().cleanup_window().count()));
  add_field(retval, "timeout", static_cast<int64_t>(conf->cfg->timeout().count()));
  add_bool_field(
    retval, "cleanup_lost_attempts", conf->cfg->cleanup_config().cleanup_lost_attempts());
  add_bool_field(
    retval, "cleanup_client_attempts", conf->cfg->cleanup_config().cleanup_client_attempts());
  add_field(retval, "scan_consistency", cbpp_to_py(conf->cfg->query_config().scan_consistency()));
  if (conf->cfg->metadata_collection()) {
    std::string meta = fmt::format("{}.{}.{}",
                                   conf->cfg->metadata_collection()->bucket,
                                   conf->cfg->metadata_collection()->scope,
                                   conf->cfg->metadata_collection()->collection);
    add_field(retval, "metadata_collection", meta);
  }
  return retval;
}

static PyMethodDef transaction_config_methods[] = {
  { "to_dict",
    (PyCFunction)pycbc::txns::transaction_config__to_dict__,
    METH_NOARGS,
    PyDoc_STR("transaction_config as a dict") },
  { NULL, NULL, 0, NULL }
};

PyObject*
pycbc::txns::transaction_config__new__(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
  PyObject* durability_level = nullptr;
  PyObject* cleanup_window = nullptr;
  PyObject* timeout = nullptr;
  char* scan_consistency = nullptr;
  PyObject* cleanup_lost_attempts = nullptr;
  PyObject* cleanup_client_attempts = nullptr;
  char* metadata_bucket = nullptr;
  char* metadata_scope = nullptr;
  char* metadata_collection = nullptr;

  const char* kw_list[] = { "durability_level",
                            "cleanup_window",
                            "timeout",
                            "cleanup_lost_attempts",
                            "cleanup_client_attempts",
                            "metadata_bucket",
                            "metadata_scope",
                            "metadata_collection",
                            "scan_consistency",
                            nullptr };
  const char* kw_format = "|OOOOOssss";
  auto self = reinterpret_cast<pycbc::txns::transaction_config*>(type->tp_alloc(type, 0));

  self->cfg = new cbtxns::transactions_config();

  if (!PyArg_ParseTupleAndKeywords(args,
                                   kwargs,
                                   kw_format,
                                   const_cast<char**>(kw_list),
                                   &durability_level,
                                   &cleanup_window,
                                   &timeout,
                                   &cleanup_lost_attempts,
                                   &cleanup_client_attempts,
                                   &metadata_bucket,
                                   &metadata_scope,
                                   &metadata_collection,
                                   &scan_consistency)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (nullptr != durability_level) {
    self->cfg->durability_level(
      static_cast<couchbase::durability_level>(PyLong_AsUnsignedLong(durability_level)));
  }
  if (nullptr != cleanup_window) {
    self->cfg->cleanup_config().cleanup_window(
      std::chrono::microseconds(PyLong_AsUnsignedLongLong(cleanup_window)));
  }
  if (nullptr != timeout) {
    self->cfg->timeout(std::chrono::microseconds(PyLong_AsUnsignedLongLong(timeout)));
  }
  if (nullptr != cleanup_lost_attempts) {
    self->cfg->cleanup_config().cleanup_lost_attempts(!!PyObject_IsTrue(cleanup_lost_attempts));
  }
  if (nullptr != cleanup_client_attempts) {
    self->cfg->cleanup_config().cleanup_client_attempts(!!PyObject_IsTrue(cleanup_client_attempts));
  }
  if (nullptr != metadata_bucket && nullptr != metadata_scope && nullptr != metadata_collection) {
    auto keyspace =
      cbtxns::transaction_keyspace{ metadata_bucket, metadata_scope, metadata_collection };
    self->cfg->metadata_collection(keyspace);
  }
  if (nullptr != scan_consistency) {
    PyObject* pyObj_str = PyUnicode_FromString(scan_consistency);
    self->cfg->query_config().scan_consistency(
      pycbc::py_to_cbpp<couchbase::query_scan_consistency>(pyObj_str));
    Py_DECREF(pyObj_str);
  }
  return reinterpret_cast<PyObject*>(self);
}

static PyTypeObject
init_transaction_config_type()
{
  PyTypeObject r = {};
  r.tp_name = "pycbc_core.transaction_config";
  r.tp_doc = "Transaction configuration";
  r.tp_basicsize = sizeof(pycbc::txns::transaction_config);
  r.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  r.tp_new = pycbc::txns::transaction_config__new__;
  r.tp_dealloc = (destructor)pycbc::txns::transaction_config__dealloc__;
  r.tp_methods = transaction_config_methods;
  return r;
}

static PyTypeObject transaction_config_type = init_transaction_config_type();

/* pycbc::txns::transaction_options type methods */

void
pycbc::txns::transaction_options__dealloc__(pycbc::txns::transaction_options* opts)
{
  delete opts->opts;
  Py_TYPE(opts)->tp_free((PyObject*)opts);
  CB_LOG_DEBUG("dealloc transaction_options");
}

PyObject*
pycbc::txns::transaction_options__to_dict__(PyObject* self)
{
  auto opts = reinterpret_cast<pycbc::txns::transaction_options*>(self);
  PyObject* retval = PyDict_New();
  if (opts->opts->timeout()) {
    add_field(retval, "timeout", static_cast<int64_t>(opts->opts->timeout()->count()));
  }
  if (opts->opts->durability_level()) {
    add_field(
      retval, "durability_level", static_cast<int64_t>(opts->opts->durability_level().value()));
  }
  if (opts->opts->scan_consistency()) {
    add_field(retval, "scan_consistency", cbpp_to_py(*opts->opts->scan_consistency()));
  }
  if (opts->opts->metadata_collection()) {
    std::string meta = fmt::format("{}.{}.{}",
                                   opts->opts->metadata_collection()->bucket,
                                   opts->opts->metadata_collection()->scope,
                                   opts->opts->metadata_collection()->collection);
    add_field(retval, "metadata_collection", meta);
  }
  return retval;
}

PyObject*
pycbc::txns::transaction_options__str__(PyObject* self)
{
  auto opts = reinterpret_cast<pycbc::txns::transaction_options*>(self)->opts;
  std::stringstream stream;
  stream << "transaction_options{";
  if (nullptr != opts) {
    if (opts->durability_level()) {
      stream << "durability: " << cbcoretxns::durability_level_to_string(*opts->durability_level())
             << ", ";
    }
    if (opts->timeout()) {
      stream << "timeout: " << opts->timeout()->count() << "ns, ";
    }
    if (opts->scan_consistency()) {
      PyObject* sc_py = cbpp_to_py(*opts->scan_consistency());
      stream << "scan_consistency: " << PyUnicode_AsUTF8(sc_py);
      Py_DECREF(sc_py);
    }
  }
  stream << "}";
  return PyUnicode_FromString(stream.str().c_str());
}

static PyMethodDef transaction_options_methods[] = {
  { "to_dict",
    (PyCFunction)pycbc::txns::transaction_options__to_dict__,
    METH_NOARGS,
    PyDoc_STR("transaction_options as a dict") },
  { NULL, NULL, 0, NULL }
};

PyObject*
pycbc::txns::transaction_options__new__(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
  PyObject* durability_level = nullptr;
  PyObject* timeout = nullptr;
  char* scan_consistency = nullptr;
  char* metadata_bucket = nullptr;
  char* metadata_scope = nullptr;
  char* metadata_collection = nullptr;

  const char* kw_list[] = {
    "durability_level",    "timeout", "scan_consistency", "metadata_bucket", "metadata_scope",
    "metadata_collection", nullptr
  };
  const char* kw_format = "|OOssss";
  auto self = reinterpret_cast<pycbc::txns::transaction_options*>(type->tp_alloc(type, 0));

  self->opts = new cbtxns::transaction_options();
  CB_LOG_DEBUG("transaction_options__new__ called");
  if (!PyArg_ParseTupleAndKeywords(args,
                                   kwargs,
                                   kw_format,
                                   const_cast<char**>(kw_list),
                                   &durability_level,
                                   &timeout,
                                   &scan_consistency,
                                   &metadata_bucket,
                                   &metadata_scope,
                                   &metadata_collection)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (nullptr != durability_level) {
    self->opts->durability_level(
      static_cast<couchbase::durability_level>(PyLong_AsUnsignedLong(durability_level)));
  }
  if (nullptr != timeout) {
    self->opts->timeout(std::chrono::microseconds(PyLong_AsUnsignedLongLong(timeout)));
  }
  if (nullptr != scan_consistency) {
    PyObject* pyObj_str = PyUnicode_FromString(scan_consistency);
    self->opts->scan_consistency(pycbc::py_to_cbpp<couchbase::query_scan_consistency>(pyObj_str));
    Py_DECREF(pyObj_str);
  }
  if (nullptr != metadata_bucket && nullptr != metadata_scope && nullptr != metadata_collection) {
    auto keyspace =
      cbtxns::transaction_keyspace{ metadata_bucket, metadata_scope, metadata_collection };
    self->opts->metadata_collection(keyspace);
  }

  return reinterpret_cast<PyObject*>(self);
}

static PyTypeObject
init_transaction_options_type()
{
  PyTypeObject r = {};
  r.tp_name = "pycbc_core.transaction_options";
  r.tp_doc = "Transaction options";
  r.tp_basicsize = sizeof(pycbc::txns::transaction_options);
  r.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  r.tp_new = pycbc::txns::transaction_options__new__;
  r.tp_str = (reprfunc)pycbc::txns::transaction_options__str__;
  r.tp_dealloc = (destructor)pycbc::txns::transaction_options__dealloc__;
  r.tp_methods = transaction_options_methods;
  return r;
}

static PyTypeObject transaction_options_type = init_transaction_options_type();

/* pycbc::txns::transaction_query_options type methods */

void
pycbc::txns::transaction_query_options__dealloc__(pycbc::txns::transaction_query_options* opts)
{
  delete opts->opts;
  Py_TYPE(opts)->tp_free((PyObject*)opts);
  CB_LOG_DEBUG("dealloc transaction_query_options");
}

PyObject*
pycbc::txns::transaction_query_options__to_dict__(PyObject* self)
{
  auto opts = reinterpret_cast<pycbc::txns::transaction_query_options*>(self);
  return pycbc::cbpp_to_py(*opts->opts);
}

static PyMethodDef transaction_query_options_methods[] = {
  { "to_dict",
    (PyCFunction)pycbc::txns::transaction_query_options__to_dict__,
    METH_NOARGS,
    PyDoc_STR("transaction_query_options as a dict") },
  { NULL, NULL, 0, NULL }
};

PyObject*
pycbc::txns::transaction_query_options__new__(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_query_args = nullptr;
  const char* kw_list[] = { "query_args", nullptr };
  const char* kw_format = "|O";
  if (!PyArg_ParseTupleAndKeywords(
        args, kwargs, kw_format, const_cast<char**>(kw_list), &pyObj_query_args)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  auto self = reinterpret_cast<pycbc::txns::transaction_query_options*>(type->tp_alloc(type, 0));
  auto opts =
    pycbc::py_to_cbpp<couchbase::transactions::transaction_query_options>(pyObj_query_args);
  self->opts = new couchbase::transactions::transaction_query_options(std::move(opts));
  return reinterpret_cast<PyObject*>(self);
}

static PyTypeObject
init_transaction_query_options_type()
{
  PyTypeObject r = {};
  r.tp_name = "pycbc_core.transaction_query_options";
  r.tp_doc = "Transaction query options";
  r.tp_basicsize = sizeof(pycbc::txns::transaction_query_options);
  r.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  r.tp_new = pycbc::txns::transaction_query_options__new__;
  r.tp_dealloc = (destructor)pycbc::txns::transaction_query_options__dealloc__;
  r.tp_methods = transaction_query_options_methods;
  return r;
}

static PyTypeObject transaction_query_options_type = init_transaction_query_options_type();

/* pycbc::txns::transaction_get_result type methods */

void
pycbc::txns::transaction_get_result__dealloc__(pycbc::txns::transaction_get_result* result)
{
  result->res.reset();
  Py_TYPE(result)->tp_free((PyObject*)result);
  CB_LOG_DEBUG("dealloc transaction_get_result");
}

PyObject*
pycbc::txns::transaction_get_result__str__(pycbc::txns::transaction_get_result* result)
{
  if (result->res->content().data.size() != 0) {
    auto value = reinterpret_cast<const char*>(result->res->content().data.data());
    auto flags = result->res->content().flags;
    return PyUnicode_FromFormat("transaction_get_result:{key=%s, cas=%llu, value=%s, flags=%lu}",
                                result->res->id().key().c_str(),
                                result->res->cas(),
                                value,
                                flags);
  } else {
    return PyUnicode_FromFormat("transaction_get_result:{key=%s, cas=%llu}",
                                result->res->id().key().c_str(),
                                result->res->cas());
  }
}

// TODO: a better way later, perhaps an exposed enum like operations
const std::string ID{ "id" };
const std::string CAS{ "cas" };
const std::string VALUE{ "value" };

PyObject*
pycbc::txns::transaction_get_result__get__(pycbc::txns::transaction_get_result* result,
                                           PyObject* args)
{
  const char* field_name = nullptr;
  PyObject* default_value = nullptr;
  if (!PyArg_ParseTuple(args, "s|O", &field_name, &default_value)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (ID == field_name) {
    return PyUnicode_FromString(result->res->id().key().c_str());
  }
  if (CAS == field_name) {
    return PyLong_FromUnsignedLongLong(result->res->cas().value());
  }
  if (VALUE == field_name) {
    PyObject* pyObj_value = nullptr;
    PyObject* pyObj_flags = PyLong_FromUnsignedLong(result->res->content().flags);
    try {
      pyObj_value = pycbc::cbpp_to_py(result->res->content().data);
    } catch (const std::exception& e) {
      PyErr_SetString(PyExc_TypeError, e.what());
      Py_RETURN_NONE;
    }
    PyObject* pyObj_result = PyTuple_Pack(2, pyObj_value, pyObj_flags);
    Py_DECREF(pyObj_value);
    Py_DECREF(pyObj_flags);
    return pyObj_result;
  }
  PyErr_SetString(PyExc_ValueError, fmt::format("unknown field_name {}", field_name).c_str());
  Py_RETURN_NONE;
}

static PyMethodDef transaction_get_result_methods[] = {
  { "get",
    (PyCFunction)pycbc::txns::transaction_get_result__get__,
    METH_VARARGS,
    PyDoc_STR("get field in result object") },
  { NULL, NULL, 0, NULL }
};

PyObject*
pycbc::txns::transaction_get_result__new__(PyTypeObject* type, PyObject*, PyObject*)
{
  auto self = reinterpret_cast<pycbc::txns::transaction_get_result*>(type->tp_alloc(type, 0));
  return reinterpret_cast<PyObject*>(self);
}

static PyTypeObject
init_transaction_get_result_type()
{
  PyTypeObject r = {};
  r.tp_name = "pycbc_core.transaction_get_result";
  r.tp_doc = "Result of transaction operation on client";
  r.tp_basicsize = sizeof(pycbc::txns::transaction_get_result);
  r.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  r.tp_new = pycbc::txns::transaction_get_result__new__;
  r.tp_dealloc = (destructor)pycbc::txns::transaction_get_result__dealloc__;
  r.tp_methods = transaction_get_result_methods;
  r.tp_repr = (reprfunc)pycbc::txns::transaction_get_result__str__;
  return r;
}

static PyTypeObject transaction_get_result_type = init_transaction_get_result_type();

/* pycbc::txns::transaction_get_multi_result type methods */

void
pycbc::txns::transaction_get_multi_result__dealloc__(
  pycbc::txns::transaction_get_multi_result* result)
{
  if (result->content) {
    PyList_SetSlice(result->content, 0, PY_SSIZE_T_MAX, NULL);
    Py_DECREF(result->content);
  }
  Py_TYPE(result)->tp_free((PyObject*)result);
  CB_LOG_DEBUG("dealloc transaction_get_multi_result");
}

static PyMethodDef transaction_get_multi_methods[] = { { NULL } };

static PyMemberDef transaction_get_multi_members[] = {
  { "content",
    T_OBJECT_EX,
    offsetof(pycbc::txns::transaction_get_multi_result, content),
    0,
    PyDoc_STR("Transaction get_multi content list.\n") },
  { NULL }
};

PyObject*
pycbc::txns::transaction_get_multi_result__new__(PyTypeObject* type, PyObject*, PyObject*)
{
  auto self = reinterpret_cast<pycbc::txns::transaction_get_multi_result*>(type->tp_alloc(type, 0));
  self->content = PyList_New(static_cast<Py_ssize_t>(0));
  return reinterpret_cast<PyObject*>(self);
}

static PyTypeObject
init_transaction_get_multi_result_type()
{
  PyTypeObject r = {};
  r.tp_name = "pycbc_core.transaction_get_multi_result";
  r.tp_doc = "Result of transaction get_multi operation on client";
  r.tp_basicsize = sizeof(pycbc::txns::transaction_get_multi_result);
  r.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  r.tp_new = pycbc::txns::transaction_get_multi_result__new__;
  r.tp_dealloc = (destructor)pycbc::txns::transaction_get_multi_result__dealloc__;
  r.tp_methods = transaction_get_multi_methods;
  r.tp_members = transaction_get_multi_members;
  return r;
}

static PyTypeObject transaction_get_multi_result_type = init_transaction_get_multi_result_type();

PyObject*
pycbc::txns::add_transaction_objects(PyObject* pyObj_module)
{
  PyObject* pyObj_enum_module = PyImport_ImportModule("enum");
  if (!pyObj_enum_module) {
    return nullptr;
  }
  PyObject* pyObj_enum_class = PyObject_GetAttrString(pyObj_enum_module, "Enum");
  PyObject* pyObj_enum_values = PyUnicode_FromString(pycbc::txns::TxOperations::ALL_OPERATIONS());
  PyObject* pyObj_enum_name = PyUnicode_FromString("TransactionOperations");
  PyObject* pyObj_args = PyTuple_Pack(2, pyObj_enum_name, pyObj_enum_values);
  Py_DECREF(pyObj_enum_name);
  Py_DECREF(pyObj_enum_values);

  PyObject* pyObj_kwargs = PyDict_New();
  PyObject_SetItem(
    pyObj_kwargs, PyUnicode_FromString("module"), PyModule_GetNameObject(pyObj_module));
  PyObject* transaction_operations = PyObject_Call(pyObj_enum_class, pyObj_args, pyObj_kwargs);
  Py_DECREF(pyObj_args);
  Py_DECREF(pyObj_kwargs);

  if (PyModule_AddObject(pyObj_module, "transaction_operations", transaction_operations)) {
    Py_XDECREF(transaction_operations);
    return nullptr;
  }
  Py_DECREF(pyObj_enum_class);
  Py_DECREF(pyObj_enum_module);

  if (register_pytype(pyObj_module, &transaction_get_result_type, "transaction_get_result") < 0) {
    return nullptr;
  }
  if (register_pytype(pyObj_module, &transaction_config_type, "transaction_config") < 0) {
    return nullptr;
  }
  if (register_pytype(pyObj_module, &transaction_query_options_type, "transaction_query_options") <
      0) {
    return nullptr;
  }
  if (register_pytype(pyObj_module, &transaction_options_type, "transaction_options") < 0) {
    return nullptr;
  }
  if (register_pytype(
        pyObj_module, &transaction_get_multi_result_type, "transaction_get_multi_result") < 0) {
    return nullptr;
  }

  return pyObj_module;
}

PyObject*
pycbc::txns::create_transactions([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  // we expect it to be called like:
  // create_transactions(conn, config)
  PyObject* pyObj_conn = nullptr;
  PyObject* pyObj_config = nullptr;
  const char* kw_list[] = { "conn", "config", nullptr };
  const char* kw_format = "OO";
  int ret = PyArg_ParseTupleAndKeywords(
    args, kwargs, kw_format, const_cast<char**>(kw_list), &pyObj_conn, &pyObj_config);

  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    return nullptr;
  }
  if (nullptr == pyObj_conn) {
    return raise_invalid_argument("expected a connection object", __FILE__, __LINE__);
  }

  // Validate connection type
  if (!PyObject_IsInstance(pyObj_conn, (PyObject*)&pycbc_connection_type)) {
    return raise_invalid_argument("conn must be a pycbc_connection object", __FILE__, __LINE__);
  }

  if (nullptr == pyObj_config) {
    return raise_invalid_argument("expected a TransactionConfig object", __FILE__, __LINE__);
  }

  // Extract pycbc_connection and get cluster reference
  auto* conn = reinterpret_cast<pycbc::pycbc_connection*>(pyObj_conn);
  if (conn->conn == nullptr) {
    return raise_invalid_argument("connection not initialized", __FILE__, __LINE__);
  }

  std::pair<std::error_code, std::shared_ptr<cbcoretxns::transactions>> res;
  Py_BEGIN_ALLOW_THREADS auto txn_config =
    reinterpret_cast<pycbc::txns::transaction_config*>(pyObj_config)->cfg;
  std::future<std::pair<std::error_code, std::shared_ptr<cbcoretxns::transactions>>> fut =
    cbcoretxns::transactions::create(conn->conn->cluster(), *txn_config);
  res = fut.get();
  Py_END_ALLOW_THREADS

    if (res.first.value())
  {
    return pycbc::build_exception(res.first, __FILE__, __LINE__, res.first.message().c_str());
  }

  pycbc::txns::transactions* txns = new pycbc::txns::transactions(res.second);
  PyObject* pyObj_txns = PyCapsule_New(txns, "txns_", dealloc_transactions);
  return pyObj_txns;
}

PyObject*
pycbc::txns::destroy_transactions([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_txns = nullptr;
  const char* kw_list[] = { "txns", nullptr };
  const char* kw_format = "O!";
  if (!PyArg_ParseTupleAndKeywords(
        args, kwargs, kw_format, const_cast<char**>(kw_list), &PyCapsule_Type, &pyObj_txns)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (nullptr == pyObj_txns) {
    PyErr_SetString(PyExc_ValueError, "expected a transactions object");
    Py_RETURN_NONE;
  }
  auto txns =
    reinterpret_cast<pycbc::txns::transactions*>(PyCapsule_GetPointer(pyObj_txns, "txns_"));
  if (nullptr == txns) {
    PyErr_SetString(PyExc_ValueError, "passed null transactions");
    Py_RETURN_NONE;
  }
  Py_BEGIN_ALLOW_THREADS txns->txns->close();
  Py_END_ALLOW_THREADS Py_RETURN_NONE;
}

PyObject*
init_transaction_exception_type(const char* klass)
{
  static PyObject* couchbase_exceptions = PyImport_ImportModule("couchbase.exceptions");
  assert(nullptr != couchbase_exceptions);
  return PyObject_GetAttrString(couchbase_exceptions, klass);
}

std::string
txn_external_exception_to_string(cbcoretxns::external_exception ext_exception)
{
  switch (ext_exception) {
    case cbcoretxns::external_exception::UNKNOWN:
      return "unknown";
    case cbcoretxns::external_exception::COUCHBASE_EXCEPTION:
      return "couchbase_exception";
    case cbcoretxns::external_exception::NOT_SET:
      return "not_set";
    case cbcoretxns::external_exception::ACTIVE_TRANSACTION_RECORD_ENTRY_NOT_FOUND:
      return "active_transaction_record_entry_not_found";
    case cbcoretxns::external_exception::ACTIVE_TRANSACTION_RECORD_FULL:
      return "active_transaction_record_full";
    case cbcoretxns::external_exception::COMMIT_NOT_PERMITTED:
      return "commit_not_permitted";
    case cbcoretxns::external_exception::ACTIVE_TRANSACTION_RECORD_NOT_FOUND:
      return "active_transaction_record_not_found";
    case cbcoretxns::external_exception::CONCURRENT_OPERATIONS_DETECTED_ON_SAME_DOCUMENT:
      return "concurrent_operations_detected_on_same_document";
    case cbcoretxns::external_exception::DOCUMENT_ALREADY_IN_TRANSACTION:
      return "document_already_in_transaction";
    case cbcoretxns::external_exception::DOCUMENT_EXISTS_EXCEPTION:
      return "document_exists_exception";
    case cbcoretxns::external_exception::DOCUMENT_NOT_FOUND_EXCEPTION:
      return "document_not_found_exception";
    case cbcoretxns::external_exception::FEATURE_NOT_AVAILABLE_EXCEPTION:
      return "feature_not_available_exception";
    case cbcoretxns::external_exception::FORWARD_COMPATIBILITY_FAILURE:
      return "forward_compatibility_failure";
    case cbcoretxns::external_exception::ILLEGAL_STATE_EXCEPTION:
      return "illegal_state_exception";
    case cbcoretxns::external_exception::PARSING_FAILURE:
      return "parsing_failure";
    case cbcoretxns::external_exception::PREVIOUS_OPERATION_FAILED:
      return "previous_operation_failed";
    case cbcoretxns::external_exception::REQUEST_CANCELED_EXCEPTION:
      return "request_canceled_exception";
    case cbcoretxns::external_exception::ROLLBACK_NOT_PERMITTED:
      return "rollback_not_permitted";
    case cbcoretxns::external_exception::SERVICE_NOT_AVAILABLE_EXCEPTION:
      return "service_not_available_exception";
    case cbcoretxns::external_exception::TRANSACTION_ABORTED_EXTERNALLY:
      return "transaction_aborted_externally";
    case cbcoretxns::external_exception::TRANSACTION_ALREADY_ABORTED:
      return "transaction_already_aborted";
    case cbcoretxns::external_exception::TRANSACTION_ALREADY_COMMITTED:
      return "transaction_already_committed";
    case cbcoretxns::external_exception::DOCUMENT_UNRETRIEVABLE_EXCEPTION:
      return "document_unretrievable_exception";
  }
  return "unknown";
}

PyObject*
create_python_exception(pycbc::txns::TxnExceptionType exc_type,
                        const char* message,
                        bool set_exception = false,
                        PyObject* pyObj_inner_exc = nullptr)
{
  static PyObject* pyObj_txn_failed = init_transaction_exception_type("TransactionFailed");
  static PyObject* pyObj_txn_expired = init_transaction_exception_type("TransactionExpired");
  static PyObject* pyObj_txn_ambig = init_transaction_exception_type("TransactionCommitAmbiguous");
  static PyObject* pyObj_txn_op_failed =
    init_transaction_exception_type("TransactionOperationFailed");
  static PyObject* pyObj_document_exists_ex =
    init_transaction_exception_type("DocumentExistsException");
  static PyObject* pyObj_document_not_found_ex =
    init_transaction_exception_type("DocumentNotFoundException");
  static PyObject* pyObj_query_parsing_failure =
    init_transaction_exception_type("ParsingFailedException");
  static PyObject* pyObj_couchbase_error = init_transaction_exception_type("CouchbaseException");
  static PyObject* pyObj_feature_not_available_error =
    init_transaction_exception_type("FeatureUnavailableException");
  static PyObject* pyObj_document_unretrievable_ex =
    init_transaction_exception_type("DocumentUnretrievableException");

  PyObject* pyObj_final_error = nullptr;
  PyObject* pyObj_exc_type = nullptr;
  PyObject* pyObj_error_ctx = PyDict_New();

  switch (exc_type) {
    case pycbc::txns::TxnExceptionType::TRANSACTION_FAILED: {
      pyObj_exc_type = pyObj_txn_failed;
      break;
    }
    case pycbc::txns::TxnExceptionType::TRANSACTION_COMMIT_AMBIGUOUS: {
      pyObj_exc_type = pyObj_txn_ambig;
      break;
    }
    case pycbc::txns::TxnExceptionType::TRANSACTION_EXPIRED: {
      pyObj_exc_type = pyObj_txn_expired;
      break;
    }
    case pycbc::txns::TxnExceptionType::TRANSACTION_OPERATION_FAILED: {
      pyObj_exc_type = pyObj_txn_op_failed;
      break;
    }
    case pycbc::txns::TxnExceptionType::FEATURE_NOT_AVAILABLE: {
      pyObj_exc_type = pyObj_feature_not_available_error;
      break;
    }
    case pycbc::txns::TxnExceptionType::QUERY_PARSING_FAILURE: {
      pyObj_exc_type = pyObj_query_parsing_failure;
      break;
    }
    case pycbc::txns::TxnExceptionType::DOCUMENT_EXISTS: {
      pyObj_exc_type = pyObj_document_exists_ex;
      break;
    }
    case pycbc::txns::TxnExceptionType::DOCUMENT_NOT_FOUND: {
      pyObj_exc_type = pyObj_document_not_found_ex;
      break;
    }
    case pycbc::txns::TxnExceptionType::DOCUMENT_UNRETRIEVABLE: {
      pyObj_exc_type = pyObj_document_unretrievable_ex;
      break;
    }
    case pycbc::txns::TxnExceptionType::COUCHBASE_ERROR:
    default:
      pyObj_exc_type = pyObj_couchbase_error;
  }
  PyObject* pyObj_tmp = PyUnicode_FromString(message);
  PyDict_SetItemString(pyObj_error_ctx, "message", pyObj_tmp);
  Py_DECREF(pyObj_tmp);
  if (pyObj_inner_exc != nullptr) {
    pyObj_tmp = PyDict_GetItemString(pyObj_inner_exc, "inner_cause");
    if (pyObj_tmp != nullptr) {
      PyDict_SetItemString(pyObj_error_ctx, "exc_info", pyObj_inner_exc);
      Py_DECREF(pyObj_inner_exc);
    }
    Py_DECREF(pyObj_tmp);
  }
  PyObject* pyObj_args = PyTuple_New(0);
  pyObj_final_error = PyObject_Call(pyObj_exc_type, pyObj_args, pyObj_error_ctx);
  Py_DECREF(pyObj_args);
  if (set_exception) {
    PyErr_SetObject(pyObj_exc_type, pyObj_final_error);
    return nullptr;
  }
  return pyObj_final_error;
}

PyObject*
convert_to_python_exc_type(std::exception_ptr err,
                           bool set_exception = false,
                           PyObject* pyObj_inner_exc = nullptr)
{
  auto exc_type = pycbc::txns::TxnExceptionType::COUCHBASE_ERROR;
  const char* message = nullptr;

  // Must be an error
  assert(!!err);

  try {
    std::rethrow_exception(err);
  } catch (const cbcoretxns::transaction_exception& e) {
    switch (e.type()) {
      case cbcoretxns::failure_type::FAIL:
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_FAILED;
        break;
      case cbcoretxns::failure_type::COMMIT_AMBIGUOUS:
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_COMMIT_AMBIGUOUS;
        break;
      case cbcoretxns::failure_type::EXPIRY:
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_EXPIRED;
        break;
    }
    message = e.what();
  } catch (const cbcoretxns::transaction_operation_failed& e) {
    if (e.cause() == cbcoretxns::external_exception::FEATURE_NOT_AVAILABLE_EXCEPTION) {
      exc_type = pycbc::txns::TxnExceptionType::FEATURE_NOT_AVAILABLE;
      message = "Possibly attempting a binary transaction operation with a server version < 7.6.2";
    } else if (e.cause() == cbcoretxns::external_exception::DOCUMENT_UNRETRIEVABLE_EXCEPTION) {
      exc_type = pycbc::txns::TxnExceptionType::DOCUMENT_UNRETRIEVABLE;
      message = e.what();
    } else {
      // follow logic that was used in C++ core transactions::wrap_run() to call
      // transaction_context::handle_error() which boils down to (not exactly, should suffice
      // for our purposes) calling transaction_operation_failed::get_final_exception()
      if (e.to_raise() == cbcoretxns::final_error::EXPIRED) {
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_EXPIRED;
      } else if (e.to_raise() == cbcoretxns::final_error::AMBIGUOUS) {
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_COMMIT_AMBIGUOUS;
      } else {
        exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_OPERATION_FAILED;
      }
      message = e.what();
    }
  } catch (const cbcoretxns::query_parsing_failure& e) {
    exc_type = pycbc::txns::TxnExceptionType::QUERY_PARSING_FAILURE;
    message = e.what();
  } catch (const cbcoretxns::document_exists& e) {
    exc_type = pycbc::txns::TxnExceptionType::DOCUMENT_EXISTS;
    message = e.what();
  } catch (const cbcoretxns::document_not_found& e) {
    exc_type = pycbc::txns::TxnExceptionType::DOCUMENT_NOT_FOUND;
    message = e.what();
  } catch (const cbcoretxns::document_unretrievable& e) {
    exc_type = pycbc::txns::TxnExceptionType::DOCUMENT_UNRETRIEVABLE;
    message = e.what();
  } catch (const cbcoretxns::op_exception& e) {
    message = e.what();
  } catch (const std::exception& e) {
    message = e.what();
  } catch (...) {
    message = "Unknown error";
  }
  return create_python_exception(exc_type, message, set_exception, pyObj_inner_exc);
}

void
handle_returning_void(PyObject* pyObj_callback,
                      PyObject* pyObj_errback,
                      std::shared_ptr<std::promise<PyObject*>> barrier,
                      std::exception_ptr err)
{
  auto state = PyGILState_Ensure();
  PyObject* pyObj_args = nullptr;
  PyObject* pyObj_func = nullptr;
  PyObject* pyObj_err = nullptr;
  if (err) {
    pyObj_err = convert_to_python_exc_type(err);
    if (nullptr == pyObj_errback) {
      barrier->set_value(pyObj_err);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_err);
      pyObj_func = pyObj_errback;
    }
  } else {
    Py_INCREF(Py_None);
    if (nullptr == pyObj_callback) {
      barrier->set_value(Py_None);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, Py_None);
      pyObj_func = pyObj_callback;
    }
  }
  if (nullptr != pyObj_func) {
    PyObject_CallObject(pyObj_func, pyObj_args);
    Py_DECREF(pyObj_errback);
    Py_DECREF(pyObj_callback);
    Py_DECREF(pyObj_args);
  }
  PyGILState_Release(state);
}

void
handle_returning_transaction_get_result(
  PyObject* pyObj_callback,
  PyObject* pyObj_errback,
  std::shared_ptr<std::promise<PyObject*>> barrier,
  std::exception_ptr err,
  std::optional<couchbase::core::transactions::transaction_get_result> res,
  bool is_replica_get = false)
{
  // TODO: flesh out transaction_get_result and exceptions...
  auto state = PyGILState_Ensure();
  PyObject* pyObj_args = nullptr;
  PyObject* pyObj_func = nullptr;
  PyObject* pyObj_err = nullptr;
  if (err) {
    pyObj_err = convert_to_python_exc_type(err);
    if (nullptr == pyObj_errback) {
      barrier->set_value(pyObj_err);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_err);
      pyObj_func = pyObj_errback;
    }
  } else {
    PyObject* pyObj_get_result = nullptr;

    // BUG(PYCBC-1476): We should revert to using direct get
    // operations once the underlying issue has been resolved.
    if (!res.has_value()) {
      pyObj_get_result = pycbc::build_exception(
        couchbase::errc::make_error_code((is_replica_get)
                                           ? couchbase::errc::key_value::document_irretrievable
                                           : couchbase::errc::key_value::document_not_found),
        __FILE__,
        __LINE__,
        "Txn get op: document not found.");
    } else {
      pyObj_get_result =
        PyObject_CallObject(reinterpret_cast<PyObject*>(&transaction_get_result_type), nullptr);
      auto result = reinterpret_cast<pycbc::txns::transaction_get_result*>(pyObj_get_result);
      result->res = std::make_unique<cbcoretxns::transaction_get_result>(std::move(res.value()));
    }

    if (nullptr == pyObj_callback) {
      barrier->set_value(pyObj_get_result);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_get_result);
      pyObj_func = pyObj_callback;
    }
  }
  if (nullptr != pyObj_func) {
    PyObject_CallObject(pyObj_func, pyObj_args);
    Py_DECREF(pyObj_errback);
    Py_DECREF(pyObj_callback);
    Py_DECREF(pyObj_args);
  }
  PyGILState_Release(state);
}

template<typename T>
void
handle_returning_transaction_get_multi_result(PyObject* pyObj_callback,
                                              PyObject* pyObj_errback,
                                              std::shared_ptr<std::promise<PyObject*>> barrier,
                                              std::exception_ptr err,
                                              T res,
                                              bool is_replica_get = false)
{
  auto state = PyGILState_Ensure();
  PyObject* pyObj_args = nullptr;
  PyObject* pyObj_func = nullptr;
  PyObject* pyObj_err = nullptr;
  if (err) {
    pyObj_err = convert_to_python_exc_type(err);
    if (nullptr == pyObj_errback) {
      barrier->set_value(pyObj_err);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_err);
      pyObj_func = pyObj_errback;
    }
  } else {
    PyObject* pyObj_get_multi_result =
      PyObject_CallObject(reinterpret_cast<PyObject*>(&transaction_get_multi_result_type), nullptr);
    auto result =
      reinterpret_cast<pycbc::txns::transaction_get_multi_result*>(pyObj_get_multi_result);
    for (const auto& item : res->content()) {
      if (!item.has_value()) {
        Py_INCREF(Py_None);
        PyList_Append(result->content, Py_None);
        continue;
      }
      PyObject* pyObj_value = nullptr;
      PyObject* pyObj_flags = nullptr;
      try {
        pyObj_value = pycbc::cbpp_to_py(item.value().data);
        pyObj_flags = PyLong_FromUnsignedLong(item.value().flags);
      } catch (const std::exception& e) {
        Py_INCREF(Py_None);
        PyList_Append(result->content, Py_None);
        CB_LOG_ERROR("PYCBC: Failed to convert item to PyObject. Error: {}", e.what());
        continue;
      }
      PyObject* pyObj_item = PyTuple_Pack(2, pyObj_value, pyObj_flags);
      PyList_Append(result->content, pyObj_item);
      Py_DECREF(pyObj_value);
      Py_DECREF(pyObj_flags);
      Py_DECREF(pyObj_item);
    }

    if (nullptr == pyObj_callback) {
      barrier->set_value(pyObj_get_multi_result);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_get_multi_result);
      pyObj_func = pyObj_callback;
    }
  }
  if (nullptr != pyObj_func) {
    PyObject_CallObject(pyObj_func, pyObj_args);
    Py_DECREF(pyObj_errback);
    Py_DECREF(pyObj_callback);
    Py_DECREF(pyObj_args);
  }
  PyGILState_Release(state);
}

void
handle_returning_query_result(PyObject* pyObj_callback,
                              PyObject* pyObj_errback,
                              std::shared_ptr<std::promise<PyObject*>> barrier,
                              std::exception_ptr err,
                              std::optional<couchbase::core::operations::query_response> res)
{
  auto state = PyGILState_Ensure();
  PyObject* pyObj_args = nullptr;
  PyObject* pyObj_func = nullptr;
  PyObject* pyObj_err = nullptr;
  if (err) {
    pyObj_err = convert_to_python_exc_type(err);
    if (nullptr == pyObj_errback) {
      barrier->set_value(pyObj_err);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_err);
      pyObj_func = pyObj_errback;
    }
  } else {
    PyObject* pyObj_json = PyBytes_FromString(res->ctx.http_body.c_str());
    if (nullptr == pyObj_callback) {
      barrier->set_value(pyObj_json);
    } else {
      pyObj_args = PyTuple_New(1);
      PyTuple_SetItem(pyObj_args, 0, pyObj_json);
      pyObj_func = pyObj_callback;
    }
  }
  if (nullptr != pyObj_func) {
    PyObject_CallObject(pyObj_func, pyObj_args);
    Py_DECREF(pyObj_errback);
    Py_DECREF(pyObj_callback);
    Py_DECREF(pyObj_args);
  }
  PyGILState_Release(state);
}

PyObject*
pycbc::txns::transaction_query_op([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_options = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  const char* statement = nullptr;

  const char* kw_list[] = { "ctx", "statement", "options", "callback", "errback", nullptr };
  const char* kw_format = "O!sO|OO";
  if (!PyArg_ParseTupleAndKeywords(args,
                                   kwargs,
                                   kw_format,
                                   const_cast<char**>(kw_list),
                                   &PyCapsule_Type,
                                   &pyObj_ctx,
                                   &statement,
                                   &pyObj_options,
                                   &pyObj_callback,
                                   &pyObj_errback)) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (nullptr == pyObj_ctx) {
    PyErr_SetString(PyExc_ValueError, "expected transaction_context");
    Py_RETURN_NONE;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));
  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction_context");
    Py_RETURN_NONE;
  }
  if (nullptr == statement) {
    PyErr_SetString(PyExc_ValueError, "expected query statement");
    Py_RETURN_NONE;
  }
  if (nullptr == pyObj_options) {
    PyErr_SetString(PyExc_ValueError, "expected options");
    Py_RETURN_NONE;
  }
  auto opt = reinterpret_cast<pycbc::txns::transaction_query_options*>(pyObj_options);
  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);
  auto barrier = std::make_shared<std::promise<PyObject*>>();
  auto fut = barrier->get_future();
  Py_BEGIN_ALLOW_THREADS ctx->ctx->query(
    statement,
    *opt->opts,
    [pyObj_callback, pyObj_errback, barrier](
      std::exception_ptr err, std::optional<couchbase::core::operations::query_response> resp) {
      handle_returning_query_result(pyObj_callback, pyObj_errback, barrier, err, resp);
    });
  Py_END_ALLOW_THREADS if (nullptr == pyObj_callback || nullptr == pyObj_errback)
  {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}

PyObject*
pycbc::txns::transaction_op([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  PyObject* pyObj_txn_get_result = nullptr;
  PyObject* pyObj_value = nullptr;
  const char* bucket = nullptr;
  const char* scope = nullptr;
  const char* collection = nullptr;
  const char* key = nullptr;
  couchbase::codec::encoded_value value{};
  TxOperations::TxOperationType op_type = TxOperations::UNKNOWN;
  const char* kw_list[] = { "ctx",      "bucket",  "scope", "collection_name", "key",  "op",
                            "callback", "errback", "value", "txn_get_result",  nullptr };
  const char* kw_format = "O!|ssssIOOOO";

  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_ctx,
                                        &bucket,
                                        &scope,
                                        &collection,
                                        &key,
                                        &op_type,
                                        &pyObj_callback,
                                        &pyObj_errback,
                                        &pyObj_value,
                                        &pyObj_txn_get_result);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }
  if (nullptr != pyObj_value) {
    PyObject* pyObj_data = PyTuple_GET_ITEM(pyObj_value, 0);
    PyObject* pyObj_flags = PyTuple_GET_ITEM(pyObj_value, 1);
    value.flags = static_cast<uint32_t>(PyLong_AsLong(pyObj_flags));
    try {
      value.data = pycbc::py_to_cbpp<std::vector<std::byte>>(pyObj_data);
    } catch (const std::exception& e) {
      return raise_invalid_argument(e.what(), __FILE__, __LINE__);
    }
  }
  if (nullptr == pyObj_ctx) {
    PyErr_SetString(PyExc_ValueError, "no transaction_context passed in");
    Py_RETURN_NONE;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));
  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction_context");
    Py_RETURN_NONE;
  }

  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);

  auto barrier = std::make_shared<std::promise<PyObject*>>();
  auto fut = barrier->get_future();
  switch (op_type) {
    case TxOperations::GET: {
      if (nullptr == bucket || nullptr == scope || nullptr == collection || nullptr == key) {
        PyErr_SetString(PyExc_ValueError, "couldn't create document id for get");
        Py_RETURN_NONE;
      }
      couchbase::core::document_id id{ bucket, scope, collection, key };
      Py_BEGIN_ALLOW_THREADS ctx->ctx->get_optional(
        id,
        [barrier, pyObj_callback, pyObj_errback](
          std::exception_ptr err, std::optional<cbcoretxns::transaction_get_result> res) {
          handle_returning_transaction_get_result(pyObj_callback, pyObj_errback, barrier, err, res);
        });
      Py_END_ALLOW_THREADS break;
    }
    case TxOperations::GET_REPLICA_FROM_PREFERRED_SERVER_GROUP: {
      if (nullptr == bucket || nullptr == scope || nullptr == collection || nullptr == key) {
        PyErr_SetString(PyExc_ValueError,
                        "couldn't create document id for get_replica_from_preferred_server_group");
        Py_RETURN_NONE;
      }
      couchbase::core::document_id id{ bucket, scope, collection, key };
      Py_BEGIN_ALLOW_THREADS ctx->ctx->get_replica_from_preferred_server_group(
        id,
        [barrier, pyObj_callback, pyObj_errback](
          std::exception_ptr err, std::optional<cbcoretxns::transaction_get_result> res) {
          handle_returning_transaction_get_result(
            pyObj_callback, pyObj_errback, barrier, err, res, true);
        });
      Py_END_ALLOW_THREADS break;
    }
    case TxOperations::INSERT: {
      if (nullptr == bucket || nullptr == scope || nullptr == collection || nullptr == key) {
        PyErr_SetString(PyExc_ValueError, "couldn't create document id for insert");
        Py_RETURN_NONE;
      }
      couchbase::core::document_id id{ bucket, scope, collection, key };
      if (nullptr == pyObj_value) {
        PyErr_SetString(PyExc_ValueError,
                        fmt::format("no value given for an insert of key {}", id.key()).c_str());
        Py_RETURN_NONE;
      }
      Py_BEGIN_ALLOW_THREADS ctx->ctx->insert(
        id,
        value,
        [barrier, pyObj_callback, pyObj_errback](
          std::exception_ptr err, std::optional<cbcoretxns::transaction_get_result> res) {
          handle_returning_transaction_get_result(pyObj_callback, pyObj_errback, barrier, err, res);
        });
      Py_END_ALLOW_THREADS break;
    }
    case TxOperations::REPLACE: {
      if (nullptr == pyObj_value) {
        PyErr_SetString(PyExc_ValueError, "replace expects a value");
        Py_RETURN_NONE;
      }
      if (nullptr == pyObj_txn_get_result ||
          0 == PyObject_TypeCheck(pyObj_txn_get_result, &transaction_get_result_type)) {
        PyErr_SetString(PyExc_ValueError, "replace expects to be passed a transaction_get_result");
        Py_RETURN_NONE;
      }
      auto tx_get_result =
        reinterpret_cast<pycbc::txns::transaction_get_result*>(pyObj_txn_get_result);
      Py_BEGIN_ALLOW_THREADS ctx->ctx->replace(
        *tx_get_result->res,
        value,
        [pyObj_callback, pyObj_errback, barrier](
          std::exception_ptr err, std::optional<cbcoretxns::transaction_get_result> res) {
          handle_returning_transaction_get_result(pyObj_callback, pyObj_errback, barrier, err, res);
        });
      Py_END_ALLOW_THREADS break;
    }
    case TxOperations::REMOVE: {
      if (nullptr == pyObj_txn_get_result ||
          0 == PyObject_TypeCheck(pyObj_txn_get_result, &transaction_get_result_type)) {
        PyErr_SetString(PyExc_ValueError, "remove expects to be passed a transaction_get_result");
        Py_RETURN_NONE;
      }
      auto tx_get_result =
        reinterpret_cast<pycbc::txns::transaction_get_result*>(pyObj_txn_get_result);
      Py_BEGIN_ALLOW_THREADS ctx->ctx->remove(
        *tx_get_result->res, [pyObj_callback, pyObj_errback, barrier](std::exception_ptr err) {
          handle_returning_void(pyObj_callback, pyObj_errback, barrier, err);
        });
      Py_END_ALLOW_THREADS break;
    }
    default:
      // return error!
      PyErr_SetString(PyExc_ValueError, "unknown txn operation");
  }
  if (nullptr == pyObj_callback || nullptr == pyObj_errback) {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}

cbcoretxns::transaction_get_multi_mode
get_transaction_get_multi_mode(std::string mode)
{
  if (!mode.empty()) {
    if (mode.compare("prioritise_latency") == 0) {
      return cbcoretxns::transaction_get_multi_mode::prioritise_latency;
    } else if (mode.compare("disable_read_skew_detection") == 0) {
      return cbcoretxns::transaction_get_multi_mode::disable_read_skew_detection;
    } else if (mode.compare("prioritise_read_skew_detection") == 0) {
      return cbcoretxns::transaction_get_multi_mode::prioritise_read_skew_detection;
    }
  }
  return cbcoretxns::transaction_get_multi_mode::prioritise_latency;
}

cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_mode
get_transaction_get_multi_replicas_from_preferred_server_group_mode(std::string mode)
{
  if (!mode.empty()) {
    if (mode.compare("prioritise_latency") == 0) {
      return cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_mode::
        prioritise_latency;
    } else if (mode.compare("disable_read_skew_detection") == 0) {
      return cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_mode::
        disable_read_skew_detection;
    } else if (mode.compare("prioritise_read_skew_detection") == 0) {
      return cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_mode::
        prioritise_read_skew_detection;
    }
  }
  return cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_mode::
    prioritise_latency;
}

PyObject*
pycbc::txns::transaction_get_multi_op([[maybe_unused]] PyObject* self,
                                      PyObject* args,
                                      PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  PyObject* pyObj_specs = nullptr;
  const char* mode = nullptr;
  TxOperations::TxOperationType op_type = TxOperations::UNKNOWN;
  const char* kw_list[] = { "ctx", "op", "specs", "mode", "callback", "errback", nullptr };
  const char* kw_format = "O!IO|sOO";

  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_ctx,
                                        &op_type,
                                        &pyObj_specs,
                                        &mode,
                                        &pyObj_callback,
                                        &pyObj_errback);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    Py_RETURN_NONE;
  }

  if (nullptr == pyObj_ctx) {
    PyErr_SetString(PyExc_ValueError, "no transaction_context passed in");
    Py_RETURN_NONE;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));
  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction_context");
    Py_RETURN_NONE;
  }

  if (!PyTuple_Check(pyObj_specs) && !PyList_Check(pyObj_specs)) {
    return raise_invalid_argument(
      "Cannot perform transaction get_multi operation.  Specs must be a tuple or list.",
      __FILE__,
      __LINE__);
  }

  size_t nspecs;
  if (PyTuple_Check(pyObj_specs)) {
    nspecs = static_cast<size_t>(PyTuple_GET_SIZE(pyObj_specs));
  } else {
    nspecs = static_cast<size_t>(PyList_GET_SIZE(pyObj_specs));
  }

  if (nspecs == 0) {
    return raise_invalid_argument(
      "Cannot perform transaction get_multi operation.  Need at least one spec.",
      __FILE__,
      __LINE__);
  }

  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);

  auto barrier = std::make_shared<std::promise<PyObject*>>();
  auto fut = barrier->get_future();

  size_t ii;
  auto ids = std::vector<couchbase::core::document_id>{};
  for (ii = 0; ii < nspecs; ++ii) {
    PyObject* pyObj_spec = nullptr;
    if (PyTuple_Check(pyObj_specs)) {
      pyObj_spec = PyTuple_GetItem(pyObj_specs, ii);
    } else {
      pyObj_spec = PyList_GetItem(pyObj_specs, ii);
    }
    if (!pyObj_spec) {
      Py_XDECREF(pyObj_callback);
      Py_XDECREF(pyObj_errback);
      return raise_invalid_argument("Unable to parse spec.", __FILE__, __LINE__);
    }
    char* bucket = nullptr;
    char* scope = nullptr;
    char* collection = nullptr;
    char* id = nullptr;
    if (!PyArg_ParseTuple(pyObj_spec, "ssss", &bucket, &scope, &collection, &id)) {
      Py_XDECREF(pyObj_callback);
      Py_XDECREF(pyObj_errback);
      return raise_invalid_argument("Unable to parse spec.", __FILE__, __LINE__);
    }
    ids.emplace_back(couchbase::core::document_id{ bucket, scope, collection, id });
  }

  if (op_type == TxOperations::GET_MULTI) {
    auto get_multi_mode = get_transaction_get_multi_mode(mode);
    Py_BEGIN_ALLOW_THREADS ctx->ctx->get_multi(
      ids,
      get_multi_mode,
      [barrier, pyObj_callback, pyObj_errback](
        std::exception_ptr err, std::optional<cbcoretxns::transaction_get_multi_result> res) {
        handle_returning_transaction_get_multi_result(
          pyObj_callback, pyObj_errback, barrier, err, res);
      });
    Py_END_ALLOW_THREADS
  } else if (op_type == TxOperations::GET_MULTI_REPLICAS_FROM_PREFERRED_SERVER_GROUP) {
    auto get_multi_mode = get_transaction_get_multi_replicas_from_preferred_server_group_mode(mode);
    Py_BEGIN_ALLOW_THREADS ctx->ctx->get_multi_replicas_from_preferred_server_group(
      ids,
      get_multi_mode,
      [barrier, pyObj_callback, pyObj_errback](
        std::exception_ptr err,
        std::optional<cbcoretxns::transaction_get_multi_replicas_from_preferred_server_group_result>
          res) {
        handle_returning_transaction_get_multi_result(
          pyObj_callback, pyObj_errback, barrier, err, res);
      });
    Py_END_ALLOW_THREADS
  } else {
    PyErr_SetString(PyExc_ValueError, "Unknown transaction operation");
    Py_RETURN_NONE;
  }

  if (nullptr == pyObj_callback || nullptr == pyObj_errback) {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}

PyObject*
transaction_result_to_dict(std::optional<cbtxns::transaction_result> res)
{
  PyObject* dict = PyDict_New();
  if (res) {
    PyObject* tmp = PyUnicode_FromString(res->transaction_id.c_str());
    PyDict_SetItemString(dict, "transaction_id", tmp);
    Py_DECREF(tmp);
    PyDict_SetItemString(dict, "unstaging_complete", res->unstaging_complete ? Py_True : Py_False);
  }
  return dict;
}

PyObject*
pycbc::txns::create_new_attempt_context([[maybe_unused]] PyObject* self,
                                        PyObject* args,
                                        PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  const char* kw_list[] = { "ctx", "callback", "errback", nullptr };
  const char* kw_format = "O!|OO";
  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_ctx,
                                        &pyObj_callback,
                                        &pyObj_errback);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    return nullptr;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));

  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction context");
    return nullptr;
  }

  std::shared_ptr<std::promise<PyObject*>> barrier = nullptr;
  std::future<PyObject*> fut;
  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);
  if (nullptr == pyObj_callback || nullptr == pyObj_errback) {
    barrier = std::make_shared<std::promise<PyObject*>>();
    fut = barrier->get_future();
  }
  Py_BEGIN_ALLOW_THREADS ctx->ctx->new_attempt_context(
    [barrier, pyObj_callback, pyObj_errback](std::exception_ptr err) {
      handle_returning_void(pyObj_callback, pyObj_errback, barrier, err);
    });
  Py_END_ALLOW_THREADS if (nullptr == pyObj_callback || nullptr == pyObj_errback)
  {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}

PyObject*
pycbc::txns::create_transaction_context([[maybe_unused]] PyObject* self,
                                        PyObject* args,
                                        PyObject* kwargs)
{
  PyObject* pyObj_txns = nullptr;
  PyObject* pyObj_transaction_options = nullptr;
  const char* kw_list[] = { "txns", "transaction_options", nullptr };
  const char* kw_format = "O!|O";
  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_txns,
                                        &pyObj_transaction_options);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    return nullptr;
  }
  auto txns =
    reinterpret_cast<pycbc::txns::transactions*>(PyCapsule_GetPointer(pyObj_txns, "txns_"));

  if (nullptr == txns) {
    PyErr_SetString(PyExc_ValueError, "passed null transactions");
    return nullptr;
  }
  if (nullptr != pyObj_transaction_options) {
    if (!PyObject_IsInstance(pyObj_transaction_options,
                             reinterpret_cast<PyObject*>(&transaction_options_type))) {
      PyErr_SetString(PyExc_ValueError, "expected a valid transaction_options object");
      return nullptr;
    }
  }

  auto tx_options =
    nullptr != pyObj_transaction_options && Py_None != pyObj_transaction_options
      ? *(reinterpret_cast<pycbc::txns::transaction_options*>(pyObj_transaction_options)->opts)
      : cbtxns::transaction_options();
  auto py_ctx = new pycbc::txns::transaction_context(
    cbcoretxns::transaction_context::create(*txns->txns, tx_options));
  PyObject* pyObj_ctx = PyCapsule_New(py_ctx, "ctx_", dealloc_transaction_context);
  return pyObj_ctx;
}

PyObject*
pycbc::txns::transaction_commit([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  const char* kw_list[] = { "ctx", "callback", "errback", nullptr };
  const char* kw_format = "O!|OO";
  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_ctx,
                                        &pyObj_callback,
                                        &pyObj_errback);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    return nullptr;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));

  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction context");
    return nullptr;
  }

  std::shared_ptr<std::promise<PyObject*>> barrier = nullptr;
  std::future<PyObject*> fut;
  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);
  if (nullptr == pyObj_callback || nullptr == pyObj_errback) {
    barrier = std::make_shared<std::promise<PyObject*>>();
    fut = barrier->get_future();
  }
  Py_BEGIN_ALLOW_THREADS ctx->ctx->finalize(
    [pyObj_callback, pyObj_errback, barrier](std::optional<cbcoretxns::transaction_exception> err,
                                             std::optional<cbtxns::transaction_result> res) {
      auto state = PyGILState_Ensure();
      PyObject* pyObj_args = nullptr;
      PyObject* pyObj_func = nullptr;
      PyObject* pyObj_err = nullptr;
      auto exc_type = pycbc::txns::TxnExceptionType::COUCHBASE_ERROR;
      if (err) {
        switch (err->type()) {
          case cbcoretxns::failure_type::FAIL:
            exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_FAILED;
            break;
          case cbcoretxns::failure_type::COMMIT_AMBIGUOUS:
            exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_COMMIT_AMBIGUOUS;
            break;
          case cbcoretxns::failure_type::EXPIRY:
            exc_type = pycbc::txns::TxnExceptionType::TRANSACTION_EXPIRED;
            break;
        }
        auto message = txn_external_exception_to_string(err->cause());
        pyObj_err = create_python_exception(exc_type, message.c_str());
        if (nullptr == pyObj_errback) {
          barrier->set_value(pyObj_err);
        } else {
          pyObj_args = PyTuple_New(1);
          PyTuple_SET_ITEM(pyObj_args, 0, pyObj_err);
          pyObj_func = pyObj_errback;
        }
      } else {
        PyObject* ret = transaction_result_to_dict(res);
        if (nullptr == pyObj_callback) {
          barrier->set_value(ret);
        } else {
          pyObj_args = PyTuple_New(1);
          PyTuple_SET_ITEM(pyObj_args, 0, ret);
          pyObj_func = pyObj_callback;
        }
      }
      if (nullptr != pyObj_func) {
        PyObject_CallObject(pyObj_func, pyObj_args);
        Py_DECREF(pyObj_errback);
        Py_DECREF(pyObj_callback);
        Py_DECREF(pyObj_args);
      }
      PyGILState_Release(state);
    });
  Py_END_ALLOW_THREADS if (nullptr == pyObj_callback || nullptr == pyObj_errback)
  {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}

PyObject*
pycbc::txns::transaction_rollback([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  PyObject* pyObj_ctx = nullptr;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  const char* kw_list[] = { "ctx", "callback", "errback", nullptr };
  const char* kw_format = "O!|OO";
  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_ctx,
                                        &pyObj_callback,
                                        &pyObj_errback);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "couldn't parse args");
    return nullptr;
  }
  auto ctx =
    reinterpret_cast<pycbc::txns::transaction_context*>(PyCapsule_GetPointer(pyObj_ctx, "ctx_"));

  if (nullptr == ctx) {
    PyErr_SetString(PyExc_ValueError, "passed null transaction context");
    return nullptr;
  }
  std::shared_ptr<std::promise<PyObject*>> barrier = nullptr;
  std::future<PyObject*> fut;
  Py_XINCREF(pyObj_callback);
  Py_XINCREF(pyObj_errback);
  if (nullptr == pyObj_callback || nullptr == pyObj_errback) {
    barrier = std::make_shared<std::promise<PyObject*>>();
    fut = barrier->get_future();
  }
  Py_BEGIN_ALLOW_THREADS
  {
    ctx->ctx->rollback([pyObj_callback, pyObj_errback, barrier](std::exception_ptr err) {
      handle_returning_void(pyObj_callback, pyObj_errback, barrier, err);
    });
  }
  Py_END_ALLOW_THREADS if (nullptr == pyObj_callback || nullptr == pyObj_errback)
  {
    PyObject* ret = nullptr;
    Py_BEGIN_ALLOW_THREADS ret = fut.get();
    Py_END_ALLOW_THREADS return ret;
  }
  Py_RETURN_NONE;
}
