/*
 *   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 "n1ql.hxx"
#include "exceptions.hxx"
#include "utils.hxx"

std::string
scan_consistency_type_to_string(couchbase::query_scan_consistency consistency)
{
  switch (consistency) {
    case couchbase::query_scan_consistency::not_bounded:
      return "not_bounded";
    case couchbase::query_scan_consistency::request_plus:
      return "request_plus";
  }
  // should not be able to reach here, since this is an enum class
  return "unknown";
}

std::string
profile_mode_to_str(couchbase::query_profile profile_mode)
{
  switch (profile_mode) {
    case couchbase::query_profile::off:
      return "off";
    case couchbase::query_profile::phases:
      return "phases";
    case couchbase::query_profile::timings:
      return "timings";
  }
  return "unknown profile_mode";
}

PyObject*
get_result_metrics(couchbase::core::operations::query_response::query_metrics metrics)
{
  PyObject* pyObj_metrics = PyDict_New();
  std::chrono::duration<unsigned long long, std::nano> int_nsec = metrics.elapsed_time;
  PyObject* pyObj_tmp = PyLong_FromUnsignedLongLong(int_nsec.count());
  if (-1 == PyDict_SetItemString(pyObj_metrics, "elapsed_time", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  int_nsec = metrics.execution_time;
  pyObj_tmp = PyLong_FromUnsignedLongLong(int_nsec.count());
  if (-1 == PyDict_SetItemString(pyObj_metrics, "execution_time", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.result_count);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "result_count", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.result_size);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "result_size", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.sort_count);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "sort_count", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.mutation_count);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "mutation_count", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.error_count);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "error_count", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyLong_FromUnsignedLongLong(metrics.warning_count);
  if (-1 == PyDict_SetItemString(pyObj_metrics, "warning_count", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  return pyObj_metrics;
}

PyObject*
get_result_metadata(couchbase::core::operations::query_response::query_meta_data metadata,
                    bool include_metrics)
{
  PyObject* pyObj_metadata = PyDict_New();
  PyObject* pyObj_tmp = PyUnicode_FromString(metadata.request_id.c_str());
  if (-1 == PyDict_SetItemString(pyObj_metadata, "request_id", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(metadata.client_context_id.c_str());
  if (-1 == PyDict_SetItemString(pyObj_metadata, "client_context_id", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  pyObj_tmp = PyUnicode_FromString(metadata.status.c_str());
  if (-1 == PyDict_SetItemString(pyObj_metadata, "status", pyObj_tmp)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_tmp);

  if (metadata.signature.has_value()) {
    pyObj_tmp = PyUnicode_FromString(metadata.signature.value().c_str());
    if (-1 == PyDict_SetItemString(pyObj_metadata, "signature", pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_XDECREF(pyObj_tmp);
  }

  if (metadata.profile.has_value()) {
    pyObj_tmp = PyUnicode_FromString(metadata.profile.value().c_str());
    if (-1 == PyDict_SetItemString(pyObj_metadata, "profile", pyObj_tmp)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_XDECREF(pyObj_tmp);
  }

  if (metadata.warnings.has_value()) {
    PyObject* pyObj_warnings = PyList_New(static_cast<Py_ssize_t>(0));
    for (auto const& warning : metadata.warnings.value()) {
      PyObject* pyObj_warning = PyDict_New();

      pyObj_tmp = PyLong_FromLong(warning.code);
      if (-1 == PyDict_SetItemString(pyObj_warning, "code", pyObj_tmp)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_tmp);

      pyObj_tmp = PyUnicode_FromString(warning.message.c_str());
      if (-1 == PyDict_SetItemString(pyObj_warning, "message", pyObj_tmp)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_tmp);

      if (-1 == PyList_Append(pyObj_warnings, pyObj_warning)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_warning);
    }

    if (-1 == PyDict_SetItemString(pyObj_metadata, "warnings", pyObj_warnings)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_XDECREF(pyObj_warnings);
  }

  if (metadata.errors.has_value()) {
    PyObject* pyObj_errors = PyList_New(static_cast<Py_ssize_t>(0));
    for (auto const& error : metadata.errors.value()) {
      PyObject* pyObj_error = PyDict_New();

      pyObj_tmp = PyLong_FromLong(error.code);
      if (-1 == PyDict_SetItemString(pyObj_error, "code", pyObj_tmp)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_tmp);

      pyObj_tmp = PyUnicode_FromString(error.message.c_str());
      if (-1 == PyDict_SetItemString(pyObj_error, "message", pyObj_tmp)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_tmp);

      if (-1 == PyList_Append(pyObj_errors, pyObj_error)) {
        PyErr_Print();
        PyErr_Clear();
      }
      Py_XDECREF(pyObj_error);
    }

    if (-1 == PyDict_SetItemString(pyObj_metadata, "errors", pyObj_errors)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_XDECREF(pyObj_errors);
  }

  if (include_metrics && metadata.metrics.has_value()) {
    PyObject* pyObject_metrics = get_result_metrics(metadata.metrics.value());

    if (-1 == PyDict_SetItemString(pyObj_metadata, "metrics", pyObject_metrics)) {
      PyErr_Print();
      PyErr_Clear();
    }
    Py_XDECREF(pyObject_metrics);
  }

  return pyObj_metadata;
}

result*
create_result_from_query_response(couchbase::core::operations::query_response resp,
                                  bool include_metrics)
{
  PyObject* pyObj_result = create_result_obj();
  result* res = reinterpret_cast<result*>(pyObj_result);

  PyObject* pyObj_payload = PyDict_New();

  PyObject* pyObject_metadata = get_result_metadata(resp.meta, include_metrics);
  if (-1 == PyDict_SetItemString(pyObj_payload, "metadata", pyObject_metadata)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObject_metadata);

  if (-1 == PyDict_SetItemString(res->dict, RESULT_VALUE, pyObj_payload)) {
    PyErr_Print();
    PyErr_Clear();
  }
  Py_XDECREF(pyObj_payload);

  return res;
}

void
create_query_result(couchbase::core::operations::query_response resp,
                    bool include_metrics,
                    std::shared_ptr<rows_queue<PyObject*>> rows,
                    PyObject* pyObj_callback,
                    PyObject* pyObj_errback)
{

  auto set_exception = false;
  PyObject* pyObj_exc = nullptr;
  PyObject* pyObj_args = NULL;
  PyObject* pyObj_func = NULL;
  PyObject* pyObj_callback_res = nullptr;

  PyGILState_STATE state = PyGILState_Ensure();
  if (resp.ctx.ec.value()) {
    pyObj_exc =
      build_exception_from_context(resp.ctx, __FILE__, __LINE__, "Error doing N1QL operation.");
    // lets clear any errors
    PyErr_Clear();
    rows->put(pyObj_exc);
  } else {
    for (auto const& row : resp.rows) {
      PyObject* pyObj_row = PyBytes_FromStringAndSize(row.c_str(), row.length());
      rows->put(pyObj_row);
    }

    auto res = create_result_from_query_response(resp, include_metrics);
    if (res == nullptr || PyErr_Occurred() != nullptr) {
      set_exception = true;
    } else {
      // None indicates done (i.e. raise StopIteration)
      Py_INCREF(Py_None);
      rows->put(Py_None);
      rows->put(reinterpret_cast<PyObject*>(res));
    }
  }

  if (set_exception) {
    pyObj_exc = pycbc_build_exception(
      PycbcError::UnableToBuildResult, __FILE__, __LINE__, "N1QL operation error.");
    rows->put(pyObj_exc);
  }

  // This is for txcouchbase -- let it knows we're done w/ the query request
  if (pyObj_callback != nullptr) {
    pyObj_func = pyObj_callback;
    pyObj_args = PyTuple_New(1);
    PyTuple_SET_ITEM(pyObj_args, 0, PyBool_FromLong(static_cast<long>(1)));
  }

  if (pyObj_func != nullptr) {
    pyObj_callback_res = PyObject_CallObject(pyObj_func, pyObj_args);
    if (pyObj_callback_res) {
      Py_DECREF(pyObj_callback_res);
    } else {
      pycbc_set_python_exception(
        PycbcError::InternalSDKError, __FILE__, __LINE__, "N1QL complete callback failed.");
    }
    Py_DECREF(pyObj_args);
    Py_XDECREF(pyObj_callback);
    Py_XDECREF(pyObj_errback);
  }

  PyGILState_Release(state);
}

streamed_result*
handle_n1ql_query([[maybe_unused]] PyObject* self, PyObject* args, PyObject* kwargs)
{
  // need these for all operations
  PyObject* pyObj_conn = nullptr;
  PyObject* pyObj_query_args = nullptr;
  std::uint64_t streaming_timeout_us = 0;
  PyObject* pyObj_callback = nullptr;
  PyObject* pyObj_errback = nullptr;
  PyObject* pyObj_row_callback = nullptr;

  static const char* kw_list[] = { "conn",     "query_args", "streaming_timeout",
                                   "callback", "errback",    "row_callback",
                                   nullptr };

  const char* kw_format = "O!|OKOOO";
  int ret = PyArg_ParseTupleAndKeywords(args,
                                        kwargs,
                                        kw_format,
                                        const_cast<char**>(kw_list),
                                        &PyCapsule_Type,
                                        &pyObj_conn,
                                        &pyObj_query_args,
                                        &streaming_timeout_us,
                                        &pyObj_callback,
                                        &pyObj_errback,
                                        &pyObj_row_callback);
  if (!ret) {
    PyErr_SetString(PyExc_ValueError, "Unable to parse arguments");
    return nullptr;
  }

  connection* conn = nullptr;
  conn = reinterpret_cast<connection*>(PyCapsule_GetPointer(pyObj_conn, "conn_"));
  if (nullptr == conn) {
    PyErr_SetString(PyExc_ValueError, "passed null connection");
    return nullptr;
  }
  PyErr_Clear();

  auto req = build_query_request(pyObj_query_args);
  if (PyErr_Occurred()) {
    return nullptr;
  }

  // PyObjects that need to be around for the cxx client lambda
  // have their increment/decrement handled w/in the callback_context struct
  // struct callback_context callback_ctx = { pyObj_callback, pyObj_errback };
  Py_XINCREF(pyObj_errback);
  Py_XINCREF(pyObj_callback);

  // timeout is always set either to default, or timeout provided in options
  auto streaming_timeout = couchbase::core::timeout_defaults::query_timeout;
  if (streaming_timeout_us > 0) {
    streaming_timeout = std::chrono::milliseconds(streaming_timeout_us / 1000ULL);
  }
  streamed_result* streamed_res = create_streamed_result_obj(streaming_timeout);

  // TODO:  let the couchbase++ streaming stabilize a bit more...
  // req.row_callback = [rows = streamed_res->rows](std::string&& row) {
  //     PyGILState_STATE state = PyGILState_Ensure();
  //     PyObject* pyObj_row = PyBytes_FromStringAndSize(row.c_str(), row.length());
  //     rows->put(pyObj_row);
  //     PyGILState_Release(state);
  //     return couchbase::core::utils::json::stream_control::next_row;
  // };

  {
    Py_BEGIN_ALLOW_THREADS conn->cluster_.execute(
      req,
      [rows = streamed_res->rows, include_metrics = req.metrics, pyObj_callback, pyObj_errback](
        couchbase::core::operations::query_response resp) {
        create_query_result(resp, include_metrics, rows, pyObj_callback, pyObj_errback);
      });
    Py_END_ALLOW_THREADS
  }
  return streamed_res;
}
