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

#include "services/inoreader/inoreadernetworkfactory.h"

#include "3rd-party/boolinq/boolinq.h"
#include "database/databasequeries.h"
#include "definitions/definitions.h"
#include "exceptions/applicationexception.h"
#include "exceptions/networkexception.h"
#include "gui/dialogs/formmain.h"
#include "gui/tabwidget.h"
#include "miscellaneous/application.h"
#include "network-web/networkfactory.h"
#include "network-web/oauth2service.h"
#include "network-web/silentnetworkaccessmanager.h"
#include "network-web/webfactory.h"
#include "services/abstract/category.h"
#include "services/abstract/labelsnode.h"
#include "services/inoreader/definitions.h"
#include "services/inoreader/inoreaderserviceroot.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QUrl>

InoreaderNetworkFactory::InoreaderNetworkFactory(QObject* parent) : QObject(parent),
  m_service(nullptr), m_username(QString()), m_downloadOnlyUnreadMessages(false), m_batchSize(INOREADER_DEFAULT_BATCH_SIZE),
  m_oauth2(new OAuth2Service(INOREADER_OAUTH_AUTH_URL, INOREADER_OAUTH_TOKEN_URL,
                             {}, {}, INOREADER_OAUTH_SCOPE, this)) {
  initializeOauth();
}

void InoreaderNetworkFactory::setService(InoreaderServiceRoot* service) {
  m_service = service;
}

OAuth2Service* InoreaderNetworkFactory::oauth() const {
  return m_oauth2;
}

QString InoreaderNetworkFactory::username() const {
  return m_username;
}

int InoreaderNetworkFactory::batchSize() const {
  return m_batchSize;
}

void InoreaderNetworkFactory::setBatchSize(int batch_size) {
  m_batchSize = batch_size;
}

void InoreaderNetworkFactory::initializeOauth() {
#if defined(INOREADER_OFFICIAL_SUPPORT)
  m_oauth2->setClientSecretId(TextFactory::decrypt(INOREADER_CLIENT_ID, OAUTH_DECRYPTION_KEY));
  m_oauth2->setClientSecretSecret(TextFactory::decrypt(INOREADER_CLIENT_SECRET, OAUTH_DECRYPTION_KEY));
#endif

  m_oauth2->setRedirectUrl(QString(OAUTH_REDIRECT_URI) +
                           QL1C(':') +
                           QString::number(INOREADER_OAUTH_REDIRECT_URI_PORT));

  connect(m_oauth2, &OAuth2Service::tokensRetrieveError, this, &InoreaderNetworkFactory::onTokensError);
  connect(m_oauth2, &OAuth2Service::authFailed, this, &InoreaderNetworkFactory::onAuthFailed);
  connect(m_oauth2, &OAuth2Service::tokensRetrieved, this, [this](QString access_token, QString refresh_token, int expires_in) {
    Q_UNUSED(expires_in)
    Q_UNUSED(access_token)

    if (m_service != nullptr && m_service->accountId() > 0 && !refresh_token.isEmpty()) {
      QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());

      DatabaseQueries::storeNewOauthTokens(database, refresh_token, m_service->accountId());
    }
  });
}

bool InoreaderNetworkFactory::downloadOnlyUnreadMessages() const {
  return m_downloadOnlyUnreadMessages;
}

void InoreaderNetworkFactory::setDownloadOnlyUnreadMessages(bool download_only_unread) {
  m_downloadOnlyUnreadMessages = download_only_unread;
}

void InoreaderNetworkFactory::setUsername(const QString& username) {
  m_username = username;
}

RootItem* InoreaderNetworkFactory::feedsCategories(bool obtain_icons) {
  QString bearer = m_oauth2->bearer().toLocal8Bit();

  if (bearer.isEmpty()) {
    return nullptr;
  }

  QByteArray output_labels;
  auto result_labels = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_LABELS,
                                                               qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(),
                                                               {},
                                                               output_labels,
                                                               QNetworkAccessManager::Operation::GetOperation,
                                                               { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                                                 bearer.toLocal8Bit() } },
                                                               false,
                                                               {},
                                                               {},
                                                               m_service->networkProxy());

  if (result_labels.first != QNetworkReply::NetworkError::NoError) {
    return nullptr;
  }

  QByteArray output_feeds;
  auto result_feeds = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_FEEDS,
                                                              qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(),
                                                              {},
                                                              output_feeds,
                                                              QNetworkAccessManager::Operation::GetOperation,
                                                              { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                                                bearer.toLocal8Bit() } },
                                                              false,
                                                              {},
                                                              {},
                                                              m_service->networkProxy());

  if (result_feeds.first != QNetworkReply::NetworkError::NoError) {
    return nullptr;
  }

  return decodeFeedCategoriesData(output_labels, output_feeds, obtain_icons);
}

QVariantHash InoreaderNetworkFactory::userInfo(const QNetworkProxy& custom_proxy) {
  QString bearer = m_oauth2->bearer().toLocal8Bit();

  if (bearer.isEmpty()) {
    throw ApplicationException(tr("not logged in"));
  }

  int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
  QByteArray output;
  auto res = NetworkFactory::performNetworkOperation(INOREADER_API_USER_INFO,
                                                     timeout,
                                                     {},
                                                     output,
                                                     QNetworkAccessManager::Operation::GetOperation,
                                                     { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                                       bearer.toLocal8Bit() } },
                                                     false,
                                                     {},
                                                     {},
                                                     custom_proxy);

  if (res.first != QNetworkReply::NetworkError::NoError) {
    throw NetworkException(res.first);
  }

  return QJsonDocument::fromJson(output).object().toVariantHash();
}

QList<RootItem*> InoreaderNetworkFactory::getLabels() {
  QList<RootItem*> lbls;
  QString bearer = m_oauth2->bearer().toLocal8Bit();

  if (bearer.isEmpty()) {
    return lbls;
  }

  QByteArray output;
  auto result = NetworkFactory::performNetworkOperation(INOREADER_API_LIST_LABELS,
                                                        qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(),
                                                        {},
                                                        output,
                                                        QNetworkAccessManager::Operation::GetOperation,
                                                        { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                                          bearer.toLocal8Bit() } },
                                                        false,
                                                        {},
                                                        {},
                                                        m_service->networkProxy());
  QJsonDocument json_lbls = QJsonDocument::fromJson(output);
  auto json_tags = json_lbls.object()["tags"].toArray();

  for (const QJsonValue& lbl_val : qAsConst(json_tags)) {
    QJsonObject lbl_obj = lbl_val.toObject();

    if (lbl_obj["type"] == QL1S("tag")) {
      QString name_id = lbl_obj["id"].toString();
      QString plain_name = QRegularExpression(".+\\/([^\\/]+)").match(name_id).captured(1);
      auto* new_lbl = new Label(plain_name, TextFactory::generateColorFromText(name_id));

      new_lbl->setCustomId(name_id);
      lbls.append(new_lbl);
    }
  }

  return lbls;
}

QList<Message> InoreaderNetworkFactory::messages(ServiceRoot* root, const QString& stream_id, Feed::Status& error) {
  QString target_url = INOREADER_API_FEED_CONTENTS;
  QString bearer = m_oauth2->bearer().toLocal8Bit();

  if (bearer.isEmpty()) {
    qCriticalNN << LOGSEC_INOREADER
                << "Cannot download messages for"
                << QUOTE_NO_SPACE(stream_id)
                << ", bearer is empty.";
    error = Feed::Status::AuthError;
    return QList<Message>();
  }

  target_url += QSL("/") + QUrl::toPercentEncoding(stream_id) + QString("?n=%1").arg(batchSize() <= 0
                                                                                     ? INOREADER_MAX_BATCH_SIZE
                                                                                     : batchSize());

  if (downloadOnlyUnreadMessages()) {
    target_url += QSL("&xt=%1").arg(INOREADER_FULL_STATE_READ);
  }

  QByteArray output_msgs;
  auto result = NetworkFactory::performNetworkOperation(target_url,
                                                        qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(),
                                                        {},
                                                        output_msgs,
                                                        QNetworkAccessManager::Operation::GetOperation,
                                                        { { QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                                          bearer.toLocal8Bit() } },
                                                        false,
                                                        {},
                                                        {},
                                                        m_service->networkProxy());

  if (result.first != QNetworkReply::NetworkError::NoError) {
    qCriticalNN << LOGSEC_INOREADER
                << "Cannot download messages for "
                << QUOTE_NO_SPACE(stream_id)
                << ", network error:"
                << QUOTE_W_SPACE_DOT(result.first);
    error = Feed::Status::NetworkError;
    return QList<Message>();
  }
  else {
    error = Feed::Status::Normal;
    return decodeMessages(root, output_msgs, stream_id);
  }
}

QNetworkReply::NetworkError InoreaderNetworkFactory::editLabels(const QString& state, bool assign,
                                                                const QStringList& msg_custom_ids) {
  QString target_url = INOREADER_API_EDIT_TAG;

  if (assign) {
    target_url += QString("?a=") + state + "&";
  }
  else {
    target_url += QString("?r=") + state + "&";
  }

  QString bearer = m_oauth2->bearer().toLocal8Bit();

  if (bearer.isEmpty()) {
    return QNetworkReply::NetworkError::AuthenticationRequiredError;
  }

  QList<QPair<QByteArray, QByteArray>> headers;

  headers.append(QPair<QByteArray, QByteArray>(QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(),
                                               m_oauth2->bearer().toLocal8Bit()));

  QStringList trimmed_ids;

  for (const QString& id : msg_custom_ids) {
    trimmed_ids.append(QString("i=") + id);
  }

  QStringList working_subset; working_subset.reserve(std::min(INOREADER_API_EDIT_TAG_BATCH, trimmed_ids.size()));
  int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();

  // Now, we perform messages update in batches (max XX messages per batch).
  while (!trimmed_ids.isEmpty()) {
    // We take XX IDs.
    for (int i = 0; i < INOREADER_API_EDIT_TAG_BATCH && !trimmed_ids.isEmpty(); i++) {
      working_subset.append(trimmed_ids.takeFirst());
    }

    QString batch_final_url = target_url + working_subset.join(QL1C('&'));

    // We send this batch.
    QByteArray output;
    auto result = NetworkFactory::performNetworkOperation(batch_final_url,
                                                          timeout,
                                                          {},
                                                          output,
                                                          QNetworkAccessManager::Operation::GetOperation,
                                                          headers,
                                                          false,
                                                          {},
                                                          {},
                                                          m_service->networkProxy());

    if (result.first != QNetworkReply::NetworkError::NoError) {
      return result.first;
    }

    // Cleanup for next batch.
    working_subset.clear();
  }

  return QNetworkReply::NetworkError::NoError;
}

QNetworkReply::NetworkError InoreaderNetworkFactory::markMessagesRead(RootItem::ReadStatus status, const QStringList& msg_custom_ids) {
  return editLabels(INOREADER_FULL_STATE_READ, status == RootItem::ReadStatus::Read, msg_custom_ids);
}

QNetworkReply::NetworkError InoreaderNetworkFactory::markMessagesStarred(RootItem::Importance importance, const QStringList& msg_custom_ids) {
  return editLabels(INOREADER_FULL_STATE_IMPORTANT, importance == RootItem::Importance::Important, msg_custom_ids);
}

void InoreaderNetworkFactory::onTokensError(const QString& error, const QString& error_description) {
  Q_UNUSED(error)

  qApp->showGuiMessage(Notification::Event::GeneralEvent,
                       tr("Inoreader: authentication error"),
                       tr("Click this to login again. Error is: '%1'").arg(error_description),
                       QSystemTrayIcon::MessageIcon::Critical,
                       {}, {},
                       [this]() {
    m_oauth2->setAccessToken(QString());
    m_oauth2->setRefreshToken(QString());
    m_oauth2->login();
  });
}

void InoreaderNetworkFactory::onAuthFailed() {
  qApp->showGuiMessage(Notification::Event::GeneralEvent,
                       tr("Inoreader: authorization denied"),
                       tr("Click this to login again."),
                       QSystemTrayIcon::MessageIcon::Critical,
                       {}, {},
                       [this]() {
    m_oauth2->login();
  });
}

QList<Message> InoreaderNetworkFactory::decodeMessages(ServiceRoot* root, const QString& messages_json_data, const QString& stream_id) {
  QList<Message> messages;
  QJsonArray json = QJsonDocument::fromJson(messages_json_data.toUtf8()).object()["items"].toArray();
  auto active_labels = root->labelsNode() != nullptr ? root->labelsNode()->labels() : QList<Label*>();

  messages.reserve(json.count());

  for (const QJsonValue& obj : json) {
    auto message_obj = obj.toObject();
    Message message;

    message.m_title = qApp->web()->unescapeHtml(message_obj["title"].toString());
    message.m_author = qApp->web()->unescapeHtml(message_obj["author"].toString());
    message.m_created = QDateTime::fromSecsSinceEpoch(message_obj["published"].toInt(), Qt::UTC);
    message.m_createdFromFeed = true;
    message.m_customId = message_obj["id"].toString();

    auto alternates = message_obj["alternate"].toArray();
    auto enclosures = message_obj["enclosure"].toArray();
    auto categories = message_obj["categories"].toArray();

    for (const QJsonValue& alt : alternates) {
      auto alt_obj = alt.toObject();
      QString mime = alt_obj["type"].toString();
      QString href = alt_obj["href"].toString();

      if (mime == QL1S("text/html")) {
        message.m_url = href;
      }
      else {
        message.m_enclosures.append(Enclosure(href, mime));
      }
    }

    for (const QJsonValue& enc : enclosures) {
      auto enc_obj = enc.toObject();
      QString mime = enc_obj["type"].toString();
      QString href = enc_obj["href"].toString();

      message.m_enclosures.append(Enclosure(href, mime));
    }

    for (const QJsonValue& cat : categories) {
      QString category = cat.toString();

      if (category.contains(INOREADER_STATE_READ)) {
        message.m_isRead = !category.contains(INOREADER_STATE_READING_LIST);
      }
      else if (category.contains(INOREADER_STATE_IMPORTANT)) {
        message.m_isImportant = category.contains(INOREADER_STATE_IMPORTANT);
      }
      else if (category.contains(QSL("label"))) {
        Label* label = boolinq::from(active_labels.begin(), active_labels.end()).firstOrDefault([category](Label* lbl) {
          return lbl->customId() == category;
        });

        if (label != nullptr) {
          // We found live Label object for our assigned label.
          message.m_assignedLabels.append(label);
        }
      }
    }

    message.m_contents = message_obj["summary"].toObject()["content"].toString();
    message.m_rawContents = QJsonDocument(message_obj).toJson(QJsonDocument::JsonFormat::Compact);
    message.m_feedId = stream_id;

    messages.append(message);
  }

  return messages;
}

RootItem* InoreaderNetworkFactory::decodeFeedCategoriesData(const QString& categories, const QString& feeds, bool obtain_icons) {
  auto* parent = new RootItem();
  QJsonArray json = QJsonDocument::fromJson(categories.toUtf8()).object()["tags"].toArray();
  QMap<QString, RootItem*> cats;

  cats.insert(QString(), parent);

  for (const QJsonValue& obj : json) {
    auto label = obj.toObject();

    if (label["type"].toString() == QL1S("folder")) {
      QString label_id = label["id"].toString();

      // We have label (not "state").
      auto* category = new Category();

      category->setDescription(label["htmlUrl"].toString());
      category->setTitle(label_id.mid(label_id.lastIndexOf(QL1C('/')) + 1));
      category->setCustomId(label_id);

      cats.insert(category->customId(), category);
      parent->appendChild(category);
    }
  }

  json = QJsonDocument::fromJson(feeds.toUtf8()).object()["subscriptions"].toArray();

  for (const QJsonValue& obj : qAsConst(json)) {
    auto subscription = obj.toObject();
    QString id = subscription["id"].toString();
    QString title = subscription["title"].toString();
    QString url = subscription["htmlUrl"].toString();
    QString parent_label;
    QJsonArray assigned_categories = subscription["categories"].toArray();

    for (const QJsonValue& cat : assigned_categories) {
      QString potential_id = cat.toObject()["id"].toString();

      if (potential_id.contains(QSL("/label/"))) {
        parent_label = potential_id;
        break;
      }
    }

    // We have label (not "state").
    auto* feed = new Feed();

    feed->setDescription(url);
    feed->setSource(url);
    feed->setTitle(title);
    feed->setCustomId(id);

    if (obtain_icons) {
      QString icon_url = subscription["iconUrl"].toString();

      if (!icon_url.isEmpty()) {
        QByteArray icon_data;

        if (NetworkFactory::performNetworkOperation(icon_url, DOWNLOAD_TIMEOUT,
                                                    QByteArray(), icon_data,
                                                    QNetworkAccessManager::GetOperation).first == QNetworkReply::NoError) {
          // Icon downloaded, set it up.
          QPixmap icon_pixmap;

          icon_pixmap.loadFromData(icon_data);
          feed->setIcon(QIcon(icon_pixmap));
        }
      }
    }

    if (cats.contains(parent_label)) {
      cats[parent_label]->appendChild(feed);
    }
  }

  return parent;
}

void InoreaderNetworkFactory::setOauth(OAuth2Service* oauth) {
  m_oauth2 = oauth;
}
