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

#include "src/standardserviceroot.h"

#include "src/definitions.h"
#include "src/gui/formdiscoverfeeds.h"
#include "src/gui/formeditstandardaccount.h"
#include "src/gui/formstandardfeeddetails.h"
#include "src/gui/formstandardimportexport.h"
#include "src/parsers/atomparser.h"
#include "src/parsers/gemlogparser.h"
#include "src/parsers/icalparser.h"
#include "src/parsers/jsonparser.h"
#include "src/parsers/rdfparser.h"
#include "src/parsers/rssparser.h"
#include "src/parsers/sitemapparser.h"
#include "src/quiterssimport.h"
#include "src/rssguard4import.h"
#include "src/standardcategory.h"
#include "src/standardfeed.h"
#include "src/standardfeedsimportexportmodel.h"
#include "src/standardserviceentrypoint.h"

#include <librssguard/database/databasequeries.h>
#include <librssguard/definitions/definitions.h>
#include <librssguard/exceptions/applicationexception.h>
#include <librssguard/exceptions/feedfetchexception.h>
#include <librssguard/exceptions/scriptexception.h>
#include <librssguard/gui/dialogs/formprogressworker.h>
#include <librssguard/gui/messagebox.h>
#include <librssguard/miscellaneous/application.h>
#include <librssguard/miscellaneous/iconfactory.h>
#include <librssguard/miscellaneous/mutex.h>
#include <librssguard/miscellaneous/settings.h>
#include <librssguard/network-web/networkfactory.h>
#include <librssguard/services/abstract/gui/formcategorydetails.h>
#include <qtlinq/qtlinq.h>

#if defined(ENABLE_COMPRESSED_SITEMAP)
#include "src/3rd-party/qcompressor/qcompressor.h"
#endif

#include <QAction>
#include <QElapsedTimer>
#include <QSqlTableModel>
#include <QStack>
#include <QThread>

StandardServiceRoot::StandardServiceRoot(RootItem* parent)
  : ServiceRoot(parent), m_spacingSameHostsRequests(0), m_actionFetchMetadata(nullptr) {
  setIcon(StandardServiceEntryPoint().icon());
  setDescription(tr("This is the obligatory service account for standard RSS/RDF/ATOM feeds."));
}

StandardServiceRoot::~StandardServiceRoot() {}

QNetworkProxy StandardServiceRoot::networkProxyForItem(RootItem* item) const {
  if (item != nullptr && item->kind() == RootItem::Kind::Feed) {
    auto* std_feed = qobject_cast<StandardFeed*>(item);

    if (!std_feed->useAccountProxy()) {
      return std_feed->networkProxy();
    }
  }

  return ServiceRoot::networkProxyForItem(item);
}

void StandardServiceRoot::onDatabaseCleanup() {
  for (Feed* fd : getSubTreeFeeds()) {
    qobject_cast<StandardFeed*>(fd)->setLastEtag({});
  }
}

void StandardServiceRoot::onAfterFeedsPurged(const QList<Feed*>& feeds) {
  for (Feed* fd : feeds) {
    static_cast<StandardFeed*>(fd)->setLastEtag(QString());
  }

  ServiceRoot::onAfterFeedsPurged(feeds);
}

void StandardServiceRoot::start(bool freshly_activated) {
  DatabaseQueries::loadRootFromDatabase<StandardCategory, StandardFeed>(this);

  if (getSubTreeFeeds().isEmpty()) {
    // In other words, if there are no feeds or categories added.
    MsgBox::
      show({},
           QMessageBox::Icon::Warning,
           tr("First steps"),
           tr("This new profile does not include any feeds. What do you want to do?"),

           tr("Import from RSS Guard 4.x: Only standard RSS/ATOM feeds, folders, articles, labels and queries are "
              "imported. Article filters are imported too but with old syntax. You have to double check and fix them "
              "after the migration. Article filters assignments are NOT migrated. Make sure to "
              "assign them again after the migration. Only latest database file "
              "version from newest RSS Guard 4.x is supported.\n\n"
              "Import from QuiteRSS: All feeds, folders, articles and labels are imported. Only latest database file "
              "version from newest available QuiteRSS is supported.\n\n"
              "Load from OPML file: Standard OPML 2.0 file import.\n\n"
              "Load default feeds: Will load small set of various interesting feeds."),
           {},
           QMessageBox::StandardButton::Ignore,
           QMessageBox::StandardButton::Ignore,
           {},
           {MsgBox::CustomBoxAction{tr("Import from &RSS Guard 4.x"),
                                    [this]() {
                                      importFromRssGuard4();
                                    }},
            MsgBox::CustomBoxAction{tr("Import from &QuiteRSS"),
                                    [this]() {
                                      importFromQuiteRss();
                                    }},
            MsgBox::CustomBoxAction{tr("Load from &OPML file"),
                                    [this]() {
                                      importFeeds();
                                    }},
            MsgBox::CustomBoxAction{tr("Load &default feeds"), [this]() {
                                      loadDefaultFeeds();
                                    }}});

    requestItemExpand({this}, true);
  }
}

void StandardServiceRoot::stop() {
  qDebugNN << LOGSEC_STANDARD << "Stopping StandardServiceRoot instance.";
}

QString StandardServiceRoot::code() const {
  return StandardServiceEntryPoint().code();
}

bool StandardServiceRoot::canBeEdited() const {
  return true;
}

FormAccountDetails* StandardServiceRoot::accountSetupDialog() const {
  return new FormEditStandardAccount(qApp->mainFormWidget());
}

void StandardServiceRoot::updateItemTitle(RootItem* item, const QString& new_title) {
  if (item->kind() == RootItem::Kind::Category) {
    item->setTitle(new_title);

    qApp->database()->worker()->write([&](const QSqlDatabase& db) {
      DatabaseQueries::createOverwriteCategory(db, item->toCategory(), accountId(), item->parent()->id());
    });
  }
  else if (item->kind() == RootItem::Kind::Feed) {
    item->setTitle(new_title);

    qApp->database()->worker()->write([&](const QSqlDatabase& db) {
      DatabaseQueries::createOverwriteFeed(db, item->toFeed(), accountId(), item->parent()->id());
    });
  }
}

void StandardServiceRoot::editItems(const QList<RootItem*>& items) {
  auto feeds = qlinq::from(items).ofType<Feed*>();

  if (!feeds.isEmpty()) {
    QScopedPointer<FormStandardFeedDetails> form_pointer(new FormStandardFeedDetails(this,
                                                                                     nullptr,
                                                                                     {},
                                                                                     qApp->mainFormWidget()));

    form_pointer->addEditFeed<StandardFeed>(feeds.toList());
    return;
  }

  if (items.first()->kind() == RootItem::Kind::ServiceRoot) {
    QScopedPointer<FormEditStandardAccount> p(qobject_cast<FormEditStandardAccount*>(accountSetupDialog()));

    p->addEditAccount(this);
    return;
  }

  ServiceRoot::editItems(items);
}

bool StandardServiceRoot::supportsFeedAdding() const {
  return true;
}

bool StandardServiceRoot::supportsCategoryAdding() const {
  return true;
}

void StandardServiceRoot::addNewFeed(RootItem* selected_item, const QString& url) {
  if (!qApp->feedUpdateLock()->tryLock()) {
    // Lock was not obtained because
    // it is used probably by feed updater or application
    // is quitting.
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot add item"),
                          tr("Cannot add feed because another critical operation is ongoing."),
                          QSystemTrayIcon::MessageIcon::Warning});

    return;
  }

  QScopedPointer<FormDiscoverFeeds> form_discover(new FormDiscoverFeeds(this,
                                                                        selected_item,
                                                                        url,
                                                                        qApp->mainFormWidget()));

  if (form_discover->exec() == ADVANCED_FEED_ADD_DIALOG_CODE) {
    QScopedPointer<FormStandardFeedDetails> form_pointer(new FormStandardFeedDetails(this,
                                                                                     selected_item,
                                                                                     url,
                                                                                     qApp->mainFormWidget()));

    form_pointer->addEditFeed<StandardFeed>();
  }

  qApp->feedUpdateLock()->unlock();
}

Qt::ItemFlags StandardServiceRoot::additionalFlags(int column) const {
  return ServiceRoot::additionalFlags(column) | Qt::ItemFlag::ItemIsDragEnabled | Qt::ItemFlag::ItemIsDropEnabled;
}

void StandardServiceRoot::spaceHost(const QString& host, const QString& url) {
  if (m_spacingSameHostsRequests <= 0 || host.simplified().isEmpty()) {
    // Spacing not enabled or host information unavailable.
    return;
  }

  m_spacingMutex.lock();

  QDateTime host_last_fetched = m_spacingHosts.value(host);
  QDateTime now = QDateTime::currentDateTimeUtc();
  int secs_to_wait = 0;

  if (host_last_fetched.isValid()) {
    // No last fetch time saved yet.
    QDateTime last = host_last_fetched.addSecs(m_spacingSameHostsRequests);

    if (last < now) {
      // This host was last fetched sometimes in the past and not within the critical spacing window.
      // We can therefore fetch now.
      secs_to_wait = 0;
    }
    else {
      secs_to_wait = now.secsTo(last);
    }
  }

  resetHostSpacing(host, now.addSecs(secs_to_wait));

  m_spacingMutex.unlock();

  if (secs_to_wait > 0) {
    qDebugNN << LOGSEC_STANDARD << "Freezing feed with URL" << QUOTE_W_SPACE(url) << "for"
             << NONQUOTE_W_SPACE(secs_to_wait)
             << "seconds, because its host was used for fetching another feed during the spacing period.";
    QThread::sleep(ulong(secs_to_wait));
    qDebugNN << LOGSEC_STANDARD << "Freezing feed with URL" << QUOTE_W_SPACE(url) << "is done.";
  }
}

void StandardServiceRoot::resetHostSpacing(const QString& host, const QDateTime& next_dt) {
  m_spacingHosts.insert(host, next_dt);
  qDebugNN << LOGSEC_STANDARD << "Setting spacing for" << QUOTE_W_SPACE(host) << "to" << QUOTE_W_SPACE_DOT(next_dt);
}

QList<Message> StandardServiceRoot::obtainNewMessages(Feed* feed,
                                                      const QHash<ServiceRoot::BagOfMessages, QStringList>&
                                                        stated_messages,
                                                      const QHash<QString, QStringList>& tagged_messages) {
  Q_UNUSED(stated_messages)
  Q_UNUSED(tagged_messages)

  StandardFeed* f = static_cast<StandardFeed*>(feed);
  QString host = QUrl(f->source()).host();
  QByteArray feed_contents;
  int download_timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
  QList<QPair<QByteArray, QByteArray>> headers = {
    {HTTP_HEADERS_ACCEPT, StandardFeed::idealHttpAcceptForFeedType(f->type()).toLocal8Bit()}};

  if (f->sourceType() == StandardFeed::SourceType::Url) {
    spaceHost(host, f->source());

    qDebugNN << LOGSEC_STANDARD << "Downloading URL" << QUOTE_W_SPACE(feed->source()) << "to obtain feed data.";

    headers << StandardFeed::httpHeadersToList(f->httpHeaders());
    headers << NetworkFactory::generateBasicAuthHeader(f->protection(), f->username(), f->password());

    if (!f->lastEtag().isEmpty()) {
      headers.append({QSL("If-None-Match").toLocal8Bit(), f->lastEtag().toLocal8Bit()});

      qDebugNN << "Using ETag value:" << QUOTE_W_SPACE_DOT(f->lastEtag());
    }

    auto network_result =
      NetworkFactory::performNetworkOperation(f->source(),
                                              download_timeout,
                                              {},
                                              feed_contents,
                                              QNetworkAccessManager::Operation::GetOperation,
                                              headers,
                                              false,
                                              {},
                                              {},
                                              f->useAccountProxy() ? networkProxy() : f->networkProxy(),
                                              f->http2Status());

    // Update last datetime this host was used.
    // resetHostSpacing(host);

    if (network_result.m_networkError != QNetworkReply::NetworkError::NoError) {
      qWarningNN << LOGSEC_STANDARD << "Error" << QUOTE_W_SPACE(network_result.m_networkError)
                 << "during fetching of new messages for feed" << QUOTE_W_SPACE_DOT(feed->source());
      throw FeedFetchException(Feed::Status::NetworkError,
                               NetworkFactory::networkErrorText(network_result.m_networkError),
                               QVariant::fromValue(network_result));
    }
    else {
      f->setLastEtag(network_result.m_headers.value(QSL("etag")));

      if (network_result.m_httpCode == HTTP_CODE_NOT_MODIFIED && feed_contents.trimmed().isEmpty()) {
        // We very likely used "eTag" before and server reports that
        // content was not modified since.
        qWarningNN << LOGSEC_STANDARD << QUOTE_W_SPACE(feed->source())
                   << "reported HTTP/304, meaning that the remote file did not change since last time we checked it.";
        return {};
      }
    }
  }
  else if (f->sourceType() == StandardFeed::SourceType::LocalFile) {
    feed_contents = IOFactory::readFile(feed->source());
  }
  else {
    qDebugNN << LOGSEC_STANDARD << "Running custom script" << QUOTE_W_SPACE(feed->source()) << "to obtain feed data.";

    // Use script to generate feed file.
    try {
      feed_contents = StandardFeed::generateFeedFileWithScript(feed->source(), download_timeout);
    }
    catch (const ScriptException& ex) {
      qCriticalNN << LOGSEC_STANDARD
                  << "Custom script for generating feed file failed:" << QUOTE_W_SPACE_DOT(ex.message());

      throw FeedFetchException(Feed::Status::OtherError, ex.message());
    }
  }

  // Sitemap parser supports gzip-encoded data too.
  // We need to decode it here before encoding
  // stuff kicks in.
  if (SitemapParser::isGzip(feed_contents)) {
#if defined(ENABLE_COMPRESSED_SITEMAP)
    qWarningNN << LOGSEC_STANDARD << "Decompressing gzipped feed data.";

    QByteArray uncompressed_feed_contents;

    if (!QCompressor::gzipDecompress(feed_contents, uncompressed_feed_contents)) {
      throw ApplicationException("gzip decompression failed");
    }

    feed_contents = uncompressed_feed_contents;
#else
    qWarningNN << LOGSEC_STANDARD << "This feed is gzipped.";
#endif
  }

  if (!f->postProcessScript().simplified().isEmpty()) {
    qDebugNN << LOGSEC_STANDARD << "We will process feed data with post-process script"
             << QUOTE_W_SPACE_DOT(f->postProcessScript());

    try {
      feed_contents =
        StandardFeed::postProcessFeedFileWithScript(f->postProcessScript(), feed_contents, download_timeout);
    }
    catch (const ScriptException& ex) {
      qCriticalNN << LOGSEC_STANDARD
                  << "Post-processing script for feed file failed:" << QUOTE_W_SPACE_DOT(ex.message());

      throw FeedFetchException(Feed::Status::OtherError, ex.message());
    }
  }

  QString formatted_feed_contents = TextFactory::fromEncoding(feed_contents, f->encoding());

  // Feed data are downloaded and encoded.
  // Parse data and obtain messages.
  QList<Message> messages;
  FeedParser* parser;
  QElapsedTimer tmr;

  tmr.start();

  switch (f->type()) {
    case StandardFeed::Type::Rss0X:
    case StandardFeed::Type::Rss2X:
      parser = new RssParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::Rdf:
      parser = new RdfParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::Atom10:
      parser = new AtomParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::Json:
      parser = new JsonParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::iCalendar:
      parser = new IcalParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::Gemlog:
      parser = new GemlogParser(formatted_feed_contents);
      break;

    case StandardFeed::Type::Sitemap:
      parser = new SitemapParser(formatted_feed_contents);
      break;

    default:
      break;
  }

  parser->setArticleDateMode(f->publishedInsteadOfUpdatedTime());
  parser->setFetchComments(f->fetchCommentsEnabled());
  parser->setResourceHandler([&](const QUrl& url) {
    QByteArray resource;
    NetworkResult resource_result =
      NetworkFactory::performNetworkOperation(url.toString(),
                                              download_timeout,
                                              {},
                                              resource,
                                              QNetworkAccessManager::Operation::GetOperation,
                                              headers,
                                              false,
                                              {},
                                              {},
                                              f->useAccountProxy() ? networkProxy() : f->networkProxy(),
                                              f->http2Status());

    if (resource_result.m_networkError != QNetworkReply::NetworkError::NoError) {
      qWarningNN << LOGSEC_STANDARD << "Failed to fetch resource embedded into feed" << QUOTE_W_SPACE_DOT(url);
    }

    return resource;
  });

  if (!f->dateTimeFormat().isEmpty()) {
    parser->setDateTimeFormat(f->dateTimeFormat());
  }

  parser->setDontUseRawXmlSaving(f->dontUseRawXmlSaving());
  messages = parser->messages();

  qDebugNN << LOGSEC_STANDARD << "XML parsing for feed" << QUOTE_W_SPACE(f->title()) << "took"
           << NONQUOTE_W_SPACE(tmr.elapsed()) << "ms.";

  if (f->fetchFullArticles()) {
    for (Message& msg : messages) {
      QUrl url = msg.m_url;

      if (!url.isValid() || url.isEmpty()) {
        qWarningNN << LOGSEC_STANDARD << "Cannot call article extractor with empty or invalid URL.";
        continue;
      }

      QString full_contents = FeedReader::getFullArticle(f, url, f->fetchFullArticlesInPlainText());

      if (full_contents.simplified().isEmpty()) {
        qWarningNN << LOGSEC_STANDARD << "Empty contents returned from article extractor for URL"
                   << QUOTE_W_SPACE_DOT(msg.m_url);
        continue;
      }

      msg.m_contents = full_contents;
    }
  }

  if (!parser->dateTimeFormat().isEmpty()) {
    f->setDateTimeFormat(parser->dateTimeFormat());
  }

  delete parser;
  return messages;
}

QList<QAction*> StandardServiceRoot::contextMenuFeedsList(const QList<RootItem*>& selected_items) {
  auto base_menu = ServiceRoot::contextMenuFeedsList(selected_items);
  auto items_linq = qlinq::from(selected_items);
  QList<QAction*> my_menu;

  if (items_linq.all([](RootItem* it) {
        return it->kind() == RootItem::Kind::ServiceRoot || it->kind() == RootItem::Kind::Feed ||
               it->kind() == RootItem::Kind::Category;
      })) {
    // All selected items are feeds-containing.
    auto all_feeds = items_linq
                       .selectMany([](RootItem* it) {
                         return it->getSubTreeFeeds(true);
                       })
                       .toList();

    if (m_actionFetchMetadata == nullptr) {
      m_actionFetchMetadata =
        new QAction(qApp->icons()->fromTheme(QSL("download"), QSL("emblem-downloads")), tr("Fetch metadata"), this);
    }

    my_menu.append(m_actionFetchMetadata);
    m_actionFetchMetadata->disconnect();
    connect(m_actionFetchMetadata, &QAction::triggered, this, [this, all_feeds, selected_items]() {
      fetchMetadataForAllFeeds(all_feeds);
      itemChanged(selected_items);
    });
  }

  if (!my_menu.isEmpty()) {
    auto* sep = new QAction(this);
    sep->setSeparator(true);

    base_menu.append(sep);
    base_menu.append(my_menu);
  }

  return base_menu;
}

QVariantHash StandardServiceRoot::customDatabaseData() const {
  QVariantHash data = ServiceRoot::customDatabaseData();

  data[QSL("title")] = title();
  data[QSL("icon")] = IconFactory::toByteArray(icon());
  data[QSL("requests_spacing")] = spacingSameHostsRequests();

  return data;
}

void StandardServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
  ServiceRoot::setCustomDatabaseData(data);

  setTitle(data.value(QSL("title"), defaultTitle()).toString());

  QByteArray icon_data = data.value(QSL("icon")).toByteArray();

  if (!icon_data.isEmpty()) {
    setIcon(IconFactory::fromByteArray(icon_data));
  }

  setSpacingSameHostsRequests(data.value(QSL("requests_spacing")).toInt());
}

QString StandardServiceRoot::defaultTitle() {
  return qApp->system()->loggedInUser() + QSL(" (RSS/ATOM/JSON)");
}

bool StandardServiceRoot::mergeImportExportModel(FeedsImportExportModel* model,
                                                 RootItem* target_root_node,
                                                 QString& output_message) {
  QStack<RootItem*> original_parents;

  original_parents.push(target_root_node);
  QStack<RootItem*> new_parents;

  new_parents.push(model->sourceModel()->rootItem());
  bool some_feed_category_error = false;

  // Iterate all new items we would like to merge into current model.
  while (!new_parents.isEmpty()) {
    RootItem* target_parent = original_parents.pop();
    RootItem* source_parent = new_parents.pop();
    auto sour_chi = source_parent->childItems();

    for (RootItem* source_item : std::as_const(sour_chi)) {
      if (!model->sourceModel()->isItemChecked(source_item)) {
        // We can skip this item, because it is not checked and should not be imported.
        // NOTE: All descendants are thus skipped too.
        continue;
      }

      if (source_item->kind() == RootItem::Kind::Category) {
        auto* source_category = qobject_cast<StandardCategory*>(source_item);
        auto* new_category = new StandardCategory(*source_category);
        QString new_category_title = new_category->title();

        // Add category to model.
        new_category->clearChildren();

        try {
          qApp->database()->worker()->write([&](const QSqlDatabase& db) {
            DatabaseQueries::createOverwriteCategory(db,
                                                     new_category,
                                                     target_root_node->account()->accountId(),
                                                     target_parent->id());
          });

          requestItemReassignment(new_category, target_parent);

          original_parents.push(new_category);
          new_parents.push(source_category);
        }
        catch (ApplicationException& ex) {
          // Add category failed, but this can mean that the same category (with same title)
          // already exists. If such a category exists in current parent, then find it and
          // add descendants to it.
          RootItem* existing_category = nullptr;
          auto tar_chi = target_parent->childItems();

          for (RootItem* child : std::as_const(tar_chi)) {
            if (child->kind() == RootItem::Kind::Category && child->title() == new_category_title) {
              existing_category = child;
            }
          }

          if (existing_category != nullptr) {
            original_parents.push(existing_category);
            new_parents.push(source_category);
          }
          else {
            some_feed_category_error = true;

            qCriticalNN << LOGSEC_STANDARD << "Cannot import category:" << QUOTE_W_SPACE_DOT(ex.message());
          }
        }
      }
      else if (source_item->kind() == RootItem::Kind::Feed) {
        auto* source_feed = qobject_cast<StandardFeed*>(source_item);
        const auto* feed_with_same_url = target_root_node->getItemFromSubTree([source_feed](const RootItem* it) {
          return it->kind() == RootItem::Kind::Feed &&
                 it->toFeed()->source().toLower() == source_feed->source().toLower();
        });

        if (feed_with_same_url != nullptr) {
          continue;
        }

        auto* new_feed = new StandardFeed(*source_feed);

        try {
          qApp->database()->worker()->write([&](const QSqlDatabase& db) {
            DatabaseQueries::createOverwriteFeed(db,
                                                 new_feed,
                                                 target_root_node->account()->accountId(),
                                                 target_parent->id());
          });

          requestItemReassignment(new_feed, target_parent);
        }
        catch (const ApplicationException& ex) {
          qCriticalNN << LOGSEC_STANDARD << "Cannot import feed:" << QUOTE_W_SPACE_DOT(ex.message());
          some_feed_category_error = true;
        }
      }
    }
  }

  if (some_feed_category_error) {
    output_message = tr("Some feeds/folders were not imported due to error, check debug log for more details.");
  }
  else {
    output_message = tr("Import was completely successful.");
  }

  return !some_feed_category_error;
}

int StandardServiceRoot::spacingSameHostsRequests() const {
  return m_spacingSameHostsRequests;
}

void StandardServiceRoot::setSpacingSameHostsRequests(int spacing) {
  m_spacingSameHostsRequests = qMin(spacing, MAX_SPACING_SECONDS);
}

void StandardServiceRoot::addNewCategory(RootItem* selected_item) {
  if (!qApp->feedUpdateLock()->tryLock()) {
    // Lock was not obtained because
    // it is used probably by feed updater or application
    // is quitting.
    qApp->showGuiMessage(Notification::Event::GeneralEvent,
                         {tr("Cannot add folder"),
                          tr("Cannot add folder because another critical operation is ongoing."),
                          QSystemTrayIcon::MessageIcon::Warning});

    // Thus, cannot delete and quit the method.
    return;
  }

  QScopedPointer<FormCategoryDetails> form_pointer(new FormCategoryDetails(this,
                                                                           selected_item,
                                                                           qApp->mainFormWidget()));

  form_pointer->addEditCategory<StandardCategory>();
  qApp->feedUpdateLock()->unlock();
}

void StandardServiceRoot::loadDefaultFeeds() {
  QString target_opml_file = APP_INITIAL_FEEDS_PATH + QDir::separator() + FEED_INITIAL_OPML_PATTERN;
  QString current_locale = qApp->localization()->loadedLanguage();
  QString file_to_load;

  if (QFile::exists(target_opml_file.arg(current_locale))) {
    file_to_load = target_opml_file.arg(current_locale);
  }
  else if (QFile::exists(target_opml_file.arg(QSL(DEFAULT_LOCALE)))) {
    file_to_load = target_opml_file.arg(QSL(DEFAULT_LOCALE));
  }

  FeedsImportExportModel model(this);
  QString output_msg;

  try {
    model.importAsOPML20(IOFactory::readFile(file_to_load), false, false, false);
    model.checkAllItems();

    if (mergeImportExportModel(&model, this, output_msg)) {
      requestItemExpand(getSubTree<RootItem>(), true);
    }
  }
  catch (ApplicationException& ex) {
    MsgBox::show({}, QMessageBox::Critical, tr("Error when loading initial feeds"), ex.message());
  }
}

void StandardServiceRoot::importFeeds() {
  QScopedPointer<FormStandardImportExport> form(new FormStandardImportExport(this, qApp->mainFormWidget()));

  form.data()->setMode(FeedsImportExportModel::Mode::Import);
  form.data()->exec();
}

void StandardServiceRoot::importFromQuiteRss() {
  try {
    QuiteRssImport(this, this).import();
  }
  catch (const ApplicationException& ex) {
    MsgBox::show({}, QMessageBox::Icon::Critical, tr("Error during file import"), ex.message());
  }
}

void StandardServiceRoot::importFromRssGuard4() {
  try {
    RssGuard4Import(this, this).import();
  }
  catch (const ApplicationException& ex) {
    MsgBox::show({}, QMessageBox::Icon::Critical, tr("Error during file import"), ex.message());
  }
}

void StandardServiceRoot::exportFeeds() {
  QScopedPointer<FormStandardImportExport> form(new FormStandardImportExport(this, qApp->mainFormWidget()));

  form.data()->setMode(FeedsImportExportModel::Mode::Export);
  form.data()->exec();
}

void StandardServiceRoot::fetchMetadataForAllFeeds(const QList<Feed*>& feeds) {
  FormProgressWorker worker(qApp->mainFormWidget());

  worker.doWork<Feed*>(
    tr("Fetching metadata for %n feeds", nullptr, feeds.size()),
    true,
    feeds,
    [](Feed* fd) {
      if (fd != nullptr) {
        qobject_cast<StandardFeed*>(fd)->fetchMetadataForItself();
      }
    },
    [](int progress) {
      return tr("Fetched %n feeds...", nullptr, progress);
    });

  itemChanged(qlinq::from(feeds).ofType<RootItem*>().toList());
}

QList<QAction*> StandardServiceRoot::serviceMenu() {
  if (m_serviceMenu.isEmpty()) {
    ServiceRoot::serviceMenu();

    auto* action_export_feeds = new QAction(qApp->icons()->fromTheme(QSL("document-export")), tr("Export feeds"), this);
    auto* action_import_feeds = new QAction(qApp->icons()->fromTheme(QSL("document-import")), tr("Import feeds"), this);
    auto* action_import_quiterss =
      new QAction(qApp->icons()->fromTheme(QSL("document-import")), tr("Import from QuiteRSS"), this);
    auto* action_import_rssguard4 =
      new QAction(qApp->icons()->fromTheme(QSL("document-import")), tr("Import from RSS Guard 4.x"), this);

    connect(action_export_feeds, &QAction::triggered, this, &StandardServiceRoot::exportFeeds);
    connect(action_import_feeds, &QAction::triggered, this, &StandardServiceRoot::importFeeds);
    connect(action_import_quiterss, &QAction::triggered, this, &StandardServiceRoot::importFromQuiteRss);
    connect(action_import_rssguard4, &QAction::triggered, this, &StandardServiceRoot::importFromRssGuard4);

    m_serviceMenu.append(action_export_feeds);
    m_serviceMenu.append(action_import_feeds);
    m_serviceMenu.append(action_import_quiterss);
    m_serviceMenu.append(action_import_rssguard4);
  }

  return m_serviceMenu;
}
