// For license of this file, see
// <project-root-folder>/LICENSE.md.

#include "network-web/apiserver.h"

#include "3rd-party/boolinq/boolinq.h"
#include "core/feedsmodel.h"
#include "database/databasefactory.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "gui/dialogs/formmain.h"
#include "gui/feedmessageviewer.h"
#include "gui/messagesview.h"
#include "miscellaneous/application.h"

#include <QJsonArray>
#include <QMetaEnum>

ApiServer::ApiServer(QObject* parent) : HttpServer(parent) {}

void ApiServer::answerClient(QTcpSocket* socket, const HttpRequest& request) {
  QByteArray incoming_data = socket->readAll();
  QByteArray reply_message;

  if (request.m_method == HttpRequest::Method::Options) {
    reply_message = processCorsPreflight();
  }
  else if (request.m_url.path().contains("rssguard")) {
    reply_message = processHtmlPage();
  }
  else {
    QJsonParseError json_err;
    QByteArray json_data;
    QJsonDocument incoming_doc = QJsonDocument::fromJson(incoming_data, &json_err);

    if (json_err.error != QJsonParseError::ParseError::NoError) {
      json_data =
        ApiResponse(ApiResponse::Result::Error, ApiRequest::Method::Unknown, QJsonValue(json_err.errorString()))
          .toJson()
          .toJson();
    }
    else {
      ApiRequest req(incoming_doc);

      try {
        ApiResponse resp(processRequest(req));

        json_data = resp.toJson().toJson();
      }
      catch (const ApplicationException& ex) {
        ApiResponse err_resp(ApiResponse::Result::Error, req.m_method, ex.message());

        json_data = err_resp.toJson().toJson();
      }
    }

    reply_message = generateHttpAnswer(200,
                                       {{QSL("Access-Control-Allow-"
                                             "Origin"),
                                         QSL("*")},
                                        {QSL("Access-Control-Allow-"
                                             "Headers"),
                                         QSL("*")},
                                        {QSL("Content-Type"),
                                         QSL("application/json; "
                                             "charset=\"utf-8\"")}},
                                       json_data);

#if !defined(NDEBUG)
    IOFactory::writeFile("a.out", json_data);
#endif
  }

  socket->write(reply_message);
  socket->disconnectFromHost();
}

QByteArray ApiServer::processCorsPreflight() const {
  return generateHttpAnswer(204,
                            {{QSL("Access-Control-Allow-"
                                  "Origin"),
                              QSL("*")},
                             {QSL("Access-Control-Allow-"
                                  "Headers"),
                              QSL("*")},
                             {QSL("Access-Control-Allow-"
                                  "Methods"),
                              QSL("POST, GET, OPTIONS, "
                                  "DELETE")}});
}

QByteArray ApiServer::processHtmlPage() const {
  QByteArray page;
  QString runtime_page_path = QCoreApplication::applicationDirPath() + QDir::separator() + WEB_UI_FILE;

  if (QFile::exists(runtime_page_path)) {
    page = IOFactory::readFile(runtime_page_path);
  }
  else {
    page = IOFactory::readFile(WEB_UI_FOLDER + QL1C('/') + WEB_UI_FILE);
  }

  QByteArray data = generateHttpAnswer(200,
                                       {{QSL("Access-Control-Allow-"
                                             "Origin"),
                                         QSL("*")},
                                        {QSL("Access-Control-Allow-"
                                             "Headers"),
                                         QSL("*")},
                                        {QSL("Access-Control-Allow-"
                                             "Methods"),
                                         QSL("POST, GET, OPTIONS, "
                                             "DELETE")},
                                        {QSL("Content-Type"),
                                         QSL("text/html; "
                                             "charset=\"utf-8\"")}},
                                       page);

  return data;
}

ApiResponse ApiServer::processRequest(const ApiRequest& req) const {
  switch (req.m_method) {
    case ApiRequest::Method::AppVersion:
      return processAppVersion();

    case ApiRequest::Method::ArticlesFromFeed:
      return processArticlesFromFeed(req.m_parameters);

    case ApiRequest::Method::MarkArticles:
      return processMarkArticles(req.m_parameters);

    case ApiRequest::Method::Unknown:
    default:
      return processUnknown();
  }
}

ApiResponse ApiServer::processAppVersion() const {
  return ApiResponse(ApiResponse::Result::Success, ApiRequest::Method::AppVersion, QSL(APP_VERSION));
}

ApiResponse ApiServer::processMarkArticles(const QJsonValue& req) const {
  QJsonObject data = req.toObject();

  bool mark_read = data.value(QSL("mark_read")).toBool();
  bool mark_unread = data.value(QSL("mark_unread")).toBool();
  bool mark_starred = data.value(QSL("mark_starred")).toBool();
  bool mark_unstarred = data.value(QSL("mark_unstarred")).toBool();

  QMap<int, QStringList> articles_per_accounts;

  for (const QJsonValue& article_val : data.value(QSL("articles")).toArray()) {
    QJsonObject article_obj = article_val.toObject();

    articles_per_accounts[article_obj.value(QSL("account")).toInt()]
      .append(article_obj.value(QSL("article_custom_id")).toString());
  }

  QMapIterator<int, QStringList> articles_per_accounts_iter(articles_per_accounts);
  QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());

  RootItem::ReadStatus target_read = mark_read ? RootItem::ReadStatus::Read : RootItem::ReadStatus::Unread;

  if (!mark_read && !mark_unread) {
    target_read = RootItem::ReadStatus::Unknown;
  }

  RootItem::Importance target_important =
    mark_starred ? RootItem::Importance::Important : RootItem::Importance::NotImportant;

  if (!mark_starred && !mark_unstarred) {
    target_important = RootItem::Importance::Unknown;
  }

  QList<ServiceRoot*> accts = qApp->feedReader()->feedsModel()->serviceRoots();
  auto linq_accts = boolinq::from(accts);

  while (articles_per_accounts_iter.hasNext()) {
    auto nxt = articles_per_accounts_iter.next();
    int account_id = nxt.key();
    QStringList custom_ids = nxt.value();
    ServiceRoot* acc = linq_accts.firstOrDefault([=](ServiceRoot* acc) {
      return acc->accountId() == account_id;
    });

    if (acc == nullptr) {
      throw ApplicationException(tr("account with ID %1 not found").arg(account_id));
    }

    DatabaseQueries::markMessagesReadUnreadImportant(database, account_id, custom_ids, target_read, target_important);
  }

  // All updates are done, recalculate.
  qApp->feedReader()->feedsModel()->reloadCountsOfWholeModel();
  qApp->mainForm()->tabWidget()->feedMessageViewer()->messagesView()->reloadSelections();

  ApiResponse resp(ApiResponse::Result::Success, ApiRequest::Method::MarkArticles);
  return resp;
}

ApiResponse ApiServer::processArticlesFromFeed(const QJsonValue& req) const {
  QJsonObject data = req.toObject();

  QString feed_id = data.value(QSL("feed")).toString();
  qint64 start_after_article_date = qint64(data.value(QSL("start_after_article_date")).toDouble());
  int account_id = data.value(QSL("account")).toInt();
  bool newest_first = data.value(QSL("newest_first")).toBool();
  bool unread_only = data.value(QSL("unread_only")).toBool();
  bool starred_only = data.value(QSL("starred_only")).toBool();
  int row_offset = data.value(QSL("row_offset")).toInt();
  int row_limit = data.value(QSL("row_limit")).toInt(100000);

  // NOTE: Fixup arguments.
  if (feed_id == QSL("0")) {
    feed_id = QString();
  }

  QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
  QList<Message> msgs = DatabaseQueries::getArticlesSlice(database,
                                                          feed_id,
                                                          account_id,
                                                          newest_first,
                                                          unread_only,
                                                          starred_only,
                                                          start_after_article_date,
                                                          row_offset,
                                                          row_limit);
  QJsonArray msgs_json_array;

  for (const Message& msg : msgs) {
    msgs_json_array.append(msg.toJson());
  }

  ApiResponse resp(ApiResponse::Result::Success, ApiRequest::Method::ArticlesFromFeed, msgs_json_array);

  return resp;
}

ApiResponse ApiServer::processUnknown() const {
  return ApiResponse(ApiResponse::Result::Error,
                     ApiRequest::Method::Unknown,
                     QSL("unknown "
                         "method"));
}

ApiResponse::ApiResponse(Result result, ApiRequest::Method method, const QJsonValue& response)
  : m_result(result), m_method(method), m_response(response) {}

QJsonDocument ApiResponse::toJson() const {
  QJsonObject obj;

  static QMetaEnum enumer_method = QMetaEnum::fromType<ApiRequest::Method>();
  static QMetaEnum enumer_result = QMetaEnum::fromType<ApiResponse::Result>();

  obj.insert(QSL("method"), enumer_method.valueToKey(int(m_method)));
  obj.insert(QSL("result"), enumer_result.valueToKey(int(m_result)));

  if (!m_response.isNull() && !m_response.isUndefined()) {
    obj.insert(QSL("data"), m_response);
  }

  return QJsonDocument(obj);
}

ApiRequest::ApiRequest(const QJsonDocument& data)
  : m_method(), m_parameters(data.object().value(QSL("dat"
                                                     "a"))) {
  static QMetaEnum enumer = QMetaEnum::fromType<ApiRequest::Method>();

  QByteArray method_name = data.object().value(QSL("method")).toString().toLocal8Bit();

  m_method = Method(enumer.keysToValue(method_name.constData()));
}
