/*
 *   Copyright 2016-2022. 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 "exceptions.hxx"

#include "result.hxx"

static PyObject*
exception_base__category__(exception_base* self, [[maybe_unused]] PyObject* args)
{
  return PyUnicode_FromString(self->ec.category().name());
}

static PyObject*
exception_base__err__(exception_base* self, [[maybe_unused]] PyObject* args)
{
  return PyLong_FromLong(self->ec.value());
}
static PyObject*
exception_base__strerror__(exception_base* self, [[maybe_unused]] PyObject* args)
{
  if (self->ec) {
    return PyUnicode_FromString(self->ec.message().c_str());
  }
  Py_RETURN_NONE;
}

static PyObject*
exception_base__context__(exception_base* self, [[maybe_unused]] PyObject* args)
{
  if (self->error_context) {
    PyObject* pyObj_error_context = PyDict_Copy(self->error_context);
    return pyObj_error_context;
  }
  Py_RETURN_NONE;
}

static PyObject*
exception_base__info__(exception_base* self, [[maybe_unused]] PyObject* args)
{
  if (self->exc_info) {
    PyObject* pyObj_exc_info = PyDict_Copy(self->exc_info);
    return pyObj_exc_info;
  }
  Py_RETURN_NONE;
}

static void
exception_base_dealloc(exception_base* self)
{
  if (self->error_context) {
    if (PyDict_Check(self->error_context)) {
      PyDict_Clear(self->error_context);
    }
    Py_DECREF(self->error_context);
  }
  if (self->exc_info) {
    if (PyDict_Check(self->exc_info)) {
      PyDict_Clear(self->exc_info);
    }
    Py_DECREF(self->exc_info);
  }
  Py_TYPE(self)->tp_free((PyObject*)self);
  CB_LOG_DEBUG("{}: exception_base_dealloc completed", "PYCBC");
}

static PyObject*
exception_base__new__(PyTypeObject* type, PyObject* args, PyObject* kwargs)
{
  auto self = reinterpret_cast<exception_base*>(type->tp_alloc(type, 0));
  self->ec = std::error_code();
  return reinterpret_cast<PyObject*>(self);
}

static PyMethodDef exception_base_methods[] = {
  { "strerror",
    (PyCFunction)exception_base__strerror__,
    METH_NOARGS,
    PyDoc_STR("String description of error") },
  { "err", (PyCFunction)exception_base__err__, METH_NOARGS, PyDoc_STR("Integer error code") },
  { "err_category",
    (PyCFunction)exception_base__category__,
    METH_NOARGS,
    PyDoc_STR("error category, expressed as a string") },
  { "error_context",
    (PyCFunction)exception_base__context__,
    METH_NOARGS,
    PyDoc_STR("error context dict") },
  { "error_info", (PyCFunction)exception_base__info__, METH_NOARGS, PyDoc_STR("error info dict") },
  { nullptr, nullptr, 0, nullptr }
};

/*
Initializing the PyTypeObject w/ the C99-style designated initializers fails to compile when using
Python 3.12 on alpine3.18 (error below). The container I tested was using gcc 12.2 (don't know how
other versions might behave).

Error:
/tmp/couchbase-python-client/src/exceptions.cxx:130:1: error: designator order for field
'_typeobject::tp_basicsize' does not match declaration order in 'PyTypeObject' {aka '_typeobject'}
130 | };

This is what the docs show, so odd there is an issue...
https://docs.python.org/3.12/extending/newtypes_tutorial.html#the-basics
*/
// static PyTypeObject exception_base_type = {
//     .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
//     .tp_name = "pycbc_core.exception",
//     .tp_doc = PyDoc_STR("Base class for exceptions coming from pycbc_core"),
//     .tp_basicsize = sizeof(exception_base),
//     .tp_itemsize = 0,
//     .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
//     .tp_new = exception_base__new__,
//     .tp_dealloc = (destructor)exception_base_dealloc,
//     .tp_methods = exception_base_methods,
// };

static PyTypeObject
init_exception_base_type()
{
  PyTypeObject obj = {};
  obj.ob_base = PyVarObject_HEAD_INIT(NULL, 0) obj.tp_name = "pycbc_core.exception";
  obj.tp_doc = PyDoc_STR("Base class for exceptions coming from pycbc_core");
  obj.tp_basicsize = sizeof(exception_base);
  obj.tp_itemsize = 0;
  obj.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
  obj.tp_new = exception_base__new__;
  obj.tp_dealloc = (destructor)exception_base_dealloc;
  obj.tp_methods = exception_base_methods;
  return obj;
}

static PyTypeObject exception_base_type = init_exception_base_type();

exception_base*
create_exception_base_obj()
{
  PyObject* exc = PyObject_CallObject(reinterpret_cast<PyObject*>(&exception_base_type), nullptr);
  return reinterpret_cast<exception_base*>(exc);
}

std::string
retry_reason_to_string(couchbase::retry_reason reason)
{
  switch (reason) {
    case couchbase::retry_reason::socket_not_available:
      return "socket_not_available";
    case couchbase::retry_reason::service_not_available:
      return "service_not_available";
    case couchbase::retry_reason::node_not_available:
      return "node_not_available";
    case couchbase::retry_reason::key_value_not_my_vbucket:
      return "key_value_not_my_vbucket";
    case couchbase::retry_reason::key_value_collection_outdated:
      return "key_value_collection_outdated";
    case couchbase::retry_reason::key_value_error_map_retry_indicated:
      return "key_value_error_map_retry_indicated";
    case couchbase::retry_reason::key_value_locked:
      return "key_value_locked";
    case couchbase::retry_reason::key_value_temporary_failure:
      return "key_value_temporary_failure";
    case couchbase::retry_reason::key_value_sync_write_in_progress:
      return "key_value_sync_write_in_progress";
    case couchbase::retry_reason::key_value_sync_write_re_commit_in_progress:
      return "key_value_sync_write_re_commit_in_progress";
    case couchbase::retry_reason::service_response_code_indicated:
      return "service_response_code_indicated";
    case couchbase::retry_reason::circuit_breaker_open:
      return "circuit_breaker_open";
    case couchbase::retry_reason::query_prepared_statement_failure:
      return "query_prepared_statement_failure";
    case couchbase::retry_reason::query_index_not_found:
      return "query_index_not_found";
    case couchbase::retry_reason::analytics_temporary_failure:
      return "analytics_temporary_failure";
    case couchbase::retry_reason::search_too_many_requests:
      return "search_too_many_requests";
    case couchbase::retry_reason::views_temporary_failure:
      return "views_temporary_failure";
    case couchbase::retry_reason::views_no_active_partition:
      return "views_no_active_partition";
    case couchbase::retry_reason::do_not_retry:
      return "do_not_retry";
    case couchbase::retry_reason::socket_closed_while_in_flight:
      return "socket_closed_while_in_flight";
    case couchbase::retry_reason::unknown:
      return "unknown";
    default:
      return "unknown";
  }
}

PyObject*
build_kv_error_map_info(couchbase::core::key_value_error_map_info error_info)
{
  PyObject* err_info = PyDict_New();
  PyObject* pyObj_tmp = PyLong_FromLong(static_cast<uint16_t>(error_info.code()));
  if (-1 == PyDict_SetItemString(err_info, "code", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(error_info.name().c_str());
  if (-1 == PyDict_SetItemString(err_info, "name", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(error_info.description().c_str());
  if (-1 == PyDict_SetItemString(err_info, "description", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  PyObject* attr_set = PySet_New(nullptr);
  for (auto attr : error_info.attributes()) {
    pyObj_tmp = PyLong_FromLong(static_cast<uint16_t>(attr));
    if (-1 == PySet_Add(attr_set, pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(pyObj_tmp);
  }
  Py_ssize_t set_size = PySet_Size(attr_set);
  if (set_size > 0) {
    if (-1 == PyDict_SetItemString(err_info, "attributes", attr_set)) {
      PyErr_Print();
      PyErr_Clear();
    }
  }
  Py_XDECREF(attr_set);

  return err_info;
}

void
build_kv_error_context(const couchbase::core::key_value_error_context& ctx,
                       PyObject* pyObj_error_ctx)
{
  PyObject* pyObj_tmp = nullptr;
  pyObj_tmp = PyUnicode_FromString(ctx.id().c_str());
  if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_DOCUMENT_ID, pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(ctx.bucket().c_str());
  if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_DOCUMENT_BUCKET, pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(ctx.scope().c_str());
  if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_DOCUMENT_SCOPE, pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(ctx.collection().c_str());
  if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_DOCUMENT_COLLECTION, pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromLong(ctx.opaque());
  if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_OPAQUE, pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_DECREF(pyObj_tmp);

  if (ctx.status_code().has_value()) {
    pyObj_tmp = PyLong_FromLong(static_cast<uint16_t>(ctx.status_code().value()));
    if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_STATUS_CODE, pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(pyObj_tmp);
  }

  if (ctx.error_map_info().has_value()) {
    PyObject* err_info = build_kv_error_map_info(ctx.error_map_info().value());
    if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_ERROR_MAP_INFO, err_info)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(err_info);
  }

  if (ctx.extended_error_info().has_value()) {
    PyObject* enhanced_err_info = PyDict_New();
    pyObj_tmp = PyUnicode_FromString(ctx.extended_error_info().value().reference().c_str());
    if (-1 == PyDict_SetItemString(enhanced_err_info, "reference", pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(pyObj_tmp);

    pyObj_tmp = PyUnicode_FromString(ctx.extended_error_info().value().context().c_str());
    if (-1 == PyDict_SetItemString(enhanced_err_info, "context", pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(pyObj_tmp);

    if (-1 == PyDict_SetItemString(pyObj_error_ctx, KV_EXTENDED_ERROR_INFO, enhanced_err_info)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_DECREF(enhanced_err_info);
  }
}

struct PycbcErrorCategory : std::error_category {
  const char* name() const noexcept override;
  std::string message(int ec) const override;
};

const char*
PycbcErrorCategory::name() const noexcept
{
  return "pycbc";
}

std::string
PycbcErrorCategory::message(int ec) const
{
  switch (static_cast<PycbcError>(ec)) {
    case PycbcError::InvalidArgument:
      return "Invalid argument";
    case PycbcError::BucketNotFound:
      return "Bucket not found";
    case PycbcError::HTTPError:
      return "HTTP Error";
    case PycbcError::UnsuccessfulOperation:
      return "Unsuccessful operation";
    case PycbcError::UnableToBuildResult:
      return "Unable to build operation's result";
    case PycbcError::CallbackUnsuccessful:
      return "Async callback failed";
    case PycbcError::InternalSDKError:
      return "Internal SDK error occurred";
    default:
      return "(Unrecognized error)";
  }
}

const PycbcErrorCategory defaultPycbcErrorCategory{};

std::error_code
make_error_code(PycbcError ec)
{
  return { static_cast<int>(ec), defaultPycbcErrorCategory };
}

PyObject*
get_pycbc_exception_class(PyObject* pyObj_exc_module, std::error_code ec)
{
  switch (static_cast<PycbcError>(ec.value())) {
    case PycbcError::InvalidArgument:
      return PyObject_GetAttrString(pyObj_exc_module, "InvalidArgumentException");
    case PycbcError::BucketNotFound:
      return PyObject_GetAttrString(pyObj_exc_module, "BucketNotFoundException");
    case PycbcError::HTTPError:
      return PyObject_GetAttrString(pyObj_exc_module, "HTTPException");
    case PycbcError::UnsuccessfulOperation:
      return PyObject_GetAttrString(pyObj_exc_module, "UnsuccessfulOperationException");
    case PycbcError::FeatureUnavailable:
      return PyObject_GetAttrString(pyObj_exc_module, "FeatureUnavailableException");
    case PycbcError::UnableToBuildResult:
    case PycbcError::CallbackUnsuccessful:
    case PycbcError::InternalSDKError:
    default:
      return PyObject_GetAttrString(pyObj_exc_module, "InternalSDKException");
  }

  return PyObject_GetAttrString(pyObj_exc_module, "InternalSDKException");
}

void
pycbc_set_python_exception(std::error_code ec, const char* file, int line, const char* msg)
{
  PyObject *pyObj_type = nullptr, *pyObj_value = nullptr, *pyObj_traceback = nullptr;
  PyObject* pyObj_exc_class = nullptr;
  PyObject* pyObj_exc_instance = nullptr;

  PyErr_Fetch(&pyObj_type, &pyObj_value, &pyObj_traceback);
  PyErr_Clear();

  PyObject* pyObj_exc_params = PyDict_New();

  if (pyObj_type != nullptr) {
    PyErr_NormalizeException(&pyObj_type, &pyObj_value, &pyObj_traceback);
    if (-1 == PyDict_SetItemString(pyObj_exc_params, "inner_cause", pyObj_value)) {
      PyErr_Print();
      Py_DECREF(pyObj_type);
      Py_XDECREF(pyObj_value);
      Py_XDECREF(pyObj_traceback);
      Py_DECREF(pyObj_exc_params);
      return;
    }
    Py_XDECREF(pyObj_type);
    Py_XDECREF(pyObj_value);
  }

  PyObject* pyObj_cinfo = Py_BuildValue("(s,i)", file, line);
  if (-1 == PyDict_SetItemString(pyObj_exc_params, "cinfo", pyObj_cinfo)) {
    PyErr_Print();
    Py_XDECREF(pyObj_cinfo);
    Py_DECREF(pyObj_exc_params);
    return;
  }
  Py_DECREF(pyObj_cinfo);

  PyObject* pyObj_exc_module = PyImport_ImportModule("couchbase.exceptions");
  if (pyObj_exc_module == nullptr) {
    PyErr_Print();
    Py_DECREF(pyObj_exc_params);
    return;
  }

  pyObj_exc_class = get_pycbc_exception_class(pyObj_exc_module, ec);
  if (pyObj_exc_class == nullptr) {
    PyErr_Print();
    Py_XDECREF(pyObj_exc_params);
    Py_DECREF(pyObj_exc_module);
    return;
  }
  Py_DECREF(pyObj_exc_module);

  PyObject* pyObj_args = PyTuple_New(0);
  PyObject* pyObj_kwargs = PyDict_New();
  PyObject* pyObj_tmp = PyUnicode_FromString(msg);
  if (-1 == PyDict_SetItemString(pyObj_kwargs, "message", pyObj_tmp)) {
    PyErr_Print();
    Py_XDECREF(pyObj_args);
    Py_XDECREF(pyObj_kwargs);
    Py_XDECREF(pyObj_tmp);
    Py_DECREF(pyObj_exc_params);
    Py_DECREF(pyObj_exc_class);
    return;
  }
  Py_DECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromLong(ec.value());
  if (-1 == PyDict_SetItemString(pyObj_kwargs, "error_code", pyObj_tmp)) {
    PyErr_Print();
    Py_XDECREF(pyObj_args);
    Py_XDECREF(pyObj_kwargs);
    Py_XDECREF(pyObj_tmp);
    Py_DECREF(pyObj_exc_params);
    Py_DECREF(pyObj_exc_class);
    return;
  }
  Py_DECREF(pyObj_tmp);

  if (-1 == PyDict_SetItemString(pyObj_kwargs, "exc_info", pyObj_exc_params)) {
    PyErr_Print();
    Py_DECREF(pyObj_args);
    Py_DECREF(pyObj_kwargs);
    Py_DECREF(pyObj_exc_params);
    Py_DECREF(pyObj_exc_class);
    return;
  }
  Py_DECREF(pyObj_exc_params);

  pyObj_exc_instance = PyObject_Call(pyObj_exc_class, pyObj_args, pyObj_kwargs);
  Py_DECREF(pyObj_args);
  Py_DECREF(pyObj_kwargs);
  Py_DECREF(pyObj_exc_class);

  if (pyObj_exc_instance == nullptr) {
    Py_XDECREF(pyObj_traceback);
    return;
  }

  Py_INCREF(Py_TYPE(pyObj_exc_instance));
  PyErr_Restore((PyObject*)Py_TYPE(pyObj_exc_instance), pyObj_exc_instance, pyObj_traceback);
}

PyObject*
pycbc_build_exception(std::error_code ec, const char* file, int line, std::string msg)
{
  PyObject *pyObj_type = nullptr, *pyObj_value = nullptr, *pyObj_traceback = nullptr;

  PyErr_Fetch(&pyObj_type, &pyObj_value, &pyObj_traceback);
  PyErr_Clear();

  PyObject* pyObj_exc_info = PyDict_New();

  if (pyObj_type != nullptr) {
    PyErr_NormalizeException(&pyObj_type, &pyObj_value, &pyObj_traceback);
    if (-1 == PyDict_SetItemString(pyObj_exc_info, "inner_cause", pyObj_value)) {
      PyErr_Print();
      Py_DECREF(pyObj_type);
      Py_XDECREF(pyObj_value);

      Py_XDECREF(pyObj_exc_info);
      return nullptr;
    }
    Py_DECREF(pyObj_type);
    Py_XDECREF(pyObj_value);
    // Py_XDECREF(pyObj_traceback);
  }

  PyObject* pyObj_cinfo = Py_BuildValue("(s,i)", file, line);
  if (-1 == PyDict_SetItemString(pyObj_exc_info, "cinfo", pyObj_cinfo)) {
    PyErr_Print();
    Py_XDECREF(pyObj_cinfo);
    Py_XDECREF(pyObj_exc_info);
    return nullptr;
  }
  Py_DECREF(pyObj_cinfo);

  if (!msg.empty()) {
    PyObject* pyObj_msg = PyUnicode_FromString(msg.c_str());
    if (-1 == PyDict_SetItemString(pyObj_exc_info, "error_msg", pyObj_msg)) {
      PyErr_Print();
      Py_DECREF(pyObj_exc_info);
      Py_XDECREF(pyObj_msg);
      return nullptr;
    }
    Py_DECREF(pyObj_msg);
  }

  exception_base* exc = create_exception_base_obj();
  exc->ec = ec;

  exc->exc_info = pyObj_exc_info;
  Py_INCREF(exc->exc_info);

  return reinterpret_cast<PyObject*>(exc);
}

void
pycbc_add_exception_info(PyObject* pyObj_exc_base, const char* key, PyObject* pyObj_value)
{
  exception_base* exc = reinterpret_cast<exception_base*>(pyObj_exc_base);

  if (exc->exc_info) {
    if (-1 == PyDict_SetItemString(exc->exc_info, key, pyObj_value)) {
      PyErr_Print();
      return;
    }
    Py_DECREF(pyObj_value);
  } else {
    PyObject* pyObj_exc_info = PyDict_New();
    if (-1 == PyDict_SetItemString(pyObj_exc_info, key, pyObj_value)) {
      PyErr_Print();
      Py_XDECREF(pyObj_exc_info);
      return;
    }
    Py_DECREF(pyObj_value);
    exc->exc_info = pyObj_exc_info;
    Py_INCREF(exc->exc_info);
  }
}

PyObject*
add_exception_objects(PyObject* pyObj_module)
{
  if (PyType_Ready(&exception_base_type) < 0) {
    return nullptr;
  }
  Py_INCREF(&exception_base_type);
  if (PyModule_AddObject(
        pyObj_module, "exception", reinterpret_cast<PyObject*>(&exception_base_type)) < 0) {
    Py_DECREF(&exception_base_type);
    return nullptr;
  }
  return pyObj_module;
}
