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

#include "filtering/filteringsystem.h"

#include "database/databasefactory.h"
#include "database/sqlquery.h"
#include "definitions/globals.h"
#include "exceptions/filteringexception.h"
#include "miscellaneous/applicationpaths.h"
#include "miscellaneous/localization.h"
#include "miscellaneous/textfactory.h"
#include "qtlinq/qtlinq.h"
#include "services/abstract/labelsnode.h"
#include "services/abstract/serviceroot.h"

#include <QElapsedTimer>

FilteringSystem::FilteringSystem(FiteringUseCase mode,
                                 Feed* feed,
                                 ServiceRoot* account,
                                 ApplicationPaths* app,
                                 DatabaseFactory* db_factory,
                                 IconFactory* icon_factory,
                                 Localization* localization,
                                 GuiNotificationCoordinator* gui_notif,
                                 QObject* parent)
  : QObject(parent), m_mode(mode), m_feed(feed), m_account(account), m_applicationPaths(app),
    m_databaseFactory(db_factory), m_iconFactory(icon_factory), m_localization(localization),
    m_guiNotifications(gui_notif) {
  initializeEngine();

  m_availableLabels =
    (m_account != nullptr && m_account->labelsNode() != nullptr) ? m_account->labelsNode()->labels() : QList<Label*>();

  FilterMechanism* filter_mechanisms[] =
    {&m_filterMessage, &m_filterFeed, &m_filterAccount, &m_filterApp, &m_filterRun, &m_filterFs, &m_filterUtils};

  for (FilterMechanism* mechanism : filter_mechanisms) {
    mechanism->setSystem(this);
  }
}

void FilteringSystem::setMessage(Message* message) {
  m_filterMessage.setMessage(message);
}

void FilteringSystem::pushMessageStatesToServices(QList<Message>& read_msgs,
                                                  QList<Message>& important_msgs,
                                                  RootItem* item,
                                                  ServiceRoot* account) {
  if (!read_msgs.isEmpty()) {
    // Now we push new read states to the service.
    account->onBeforeSetMessagesRead(item, read_msgs, RootItem::ReadStatus::Read);
  }

  if (!important_msgs.isEmpty()) {
    // Now we push new read states to the service.
    auto chngs = qlinq::from(important_msgs)
                   .select([](const Message& msg) {
                     return RootItem::ImportanceChange(msg, RootItem::Importance::Important);
                   })
                   .toList();

    account->onBeforeSwitchMessageImportance(item, chngs);
  }
}

void FilteringSystem::compareAndWriteArticleStates(Message* msg_original,
                                                   Message* msg_filtered,
                                                   QList<Message>& read_msgs,
                                                   QList<Message>& important_msgs) {
  if (!msg_original->m_isRead && msg_filtered->m_isRead) {
    qDebugNN << LOGSEC_ARTICLEFILTER << "Message with custom ID:" << QUOTE_W_SPACE(msg_original->m_customId)
             << "was marked as read by message scripts.";

    read_msgs << *msg_filtered;
  }

  if (!msg_original->m_isImportant && msg_filtered->m_isImportant) {
    qDebugNN << LOGSEC_ARTICLEFILTER << "Message with custom ID:" << QUOTE_W_SPACE(msg_original->m_customId)
             << "was marked as important by message scripts.";

    important_msgs << *msg_filtered;
  }

  // Process changed labels.
  for (Label* lbl : std::as_const(msg_original->m_assignedLabels)) {
    if (!msg_filtered->m_assignedLabels.contains(lbl)) {
      // Label is not there anymore, it was deassigned.
      msg_filtered->m_deassignedLabelsByFilter << lbl;

      qDebugNN << LOGSEC_ARTICLEFILTER << "It was detected that label" << QUOTE_W_SPACE(lbl->customId())
               << "was DEASSIGNED from message" << QUOTE_W_SPACE(msg_filtered->m_customId) << "by message filter(s).";
    }
  }

  for (Label* lbl : std::as_const(msg_filtered->m_assignedLabels)) {
    if (!msg_original->m_assignedLabels.contains(lbl)) {
      // Label is in new message, but is not in old message, it
      // was newly assigned.
      msg_filtered->m_assignedLabelsByFilter << lbl;

      qDebugNN << LOGSEC_ARTICLEFILTER << "It was detected that label" << QUOTE_W_SPACE(lbl->customId())
               << "was ASSIGNED to message" << QUOTE_W_SPACE(msg_filtered->m_customId) << "by message filter(s).";
    }
  }
}

FilterMessage::FilteringAction FilteringSystem::filterMessage(const MessageFilter& filter) {
  QJSValue filter_func = prepareFilter(filter);
  QJSValue filter_output = filter_func.call();

  if (filter_output.isError()) {
    throw FilteringException(filter_output);
  }

  const int action = filter_output.toInt();

  if (!filter_output.isNumber() ||
      (action != int(FilterMessage::FilteringAction::Accept) && action != int(FilterMessage::FilteringAction::Ignore) &&
       action != int(FilterMessage::FilteringAction::Purge))) {
    QJSValue error =
      m_engine.newErrorObject(QJSValue::TypeError,
                              tr("article filter '%1' must return valid FilteringAction value.").arg(filter.name()));

    throw FilteringException(error);
  }

  return FilterMessage::FilteringAction(action);
}

const QList<FilteringSystem::DuplicateCandidate>& FilteringSystem::duplicateCandidates(FilterMessage::DuplicityCheck
                                                                                         criteria) {
  const bool all_feeds_same_account = Globals::hasFlag(criteria, FilterMessage::DuplicityCheck::AllFeedsSameAccount);
  bool& loaded = all_feeds_same_account ? m_accountDuplicateCandidatesLoaded : m_feedDuplicateCandidatesLoaded;
  QList<DuplicateCandidate>& candidates =
    all_feeds_same_account ? m_accountDuplicateCandidates : m_feedDuplicateCandidates;

  if (loaded) {
    return candidates;
  }

#ifndef QT_NO_DEBUG
  QElapsedTimer load_timer;

  load_timer.start();
#endif

  candidates.clear();

  m_databaseFactory->worker()->read([&](const QSqlDatabase& db) {
    SqlQuery query(db);

    if (all_feeds_same_account) {
      query.prepare(QSL("SELECT id, title, url, author, date_created, custom_id "
                        "FROM Messages "
                        "WHERE account_id = :account_id AND is_deleted = 0 AND is_pdeleted = 0 "
                        "ORDER BY id DESC;"));
      query.bindValue(QSL(":account_id"), m_filterAccount.id());
    }
    else {
      query.prepare(QSL("SELECT id, title, url, author, date_created, custom_id "
                        "FROM Messages "
                        "WHERE feed = :feed AND is_deleted = 0 AND is_pdeleted = 0 "
                        "ORDER BY id DESC;"));
      query.bindValue(QSL(":feed"), m_filterMessage.feedId());
    }

    query.exec();

    while (query.next()) {
      DuplicateCandidate candidate;

      candidate.m_id = query.value(0).toInt();
      candidate.m_title = query.value(1).toString();
      candidate.m_url = query.value(2).toString();
      candidate.m_author = query.value(3).toString();
      candidate.m_created = TextFactory::parseDateTime(query.value(4).value<qint64>());
      candidate.m_customId = query.value(5).toString();

      candidates.append(candidate);
    }
  });

  loaded = true;

#ifndef QT_NO_DEBUG
  qDebugNN << LOGSEC_ARTICLEFILTER << "Loaded" << NONQUOTE_W_SPACE(candidates.size())
           << "duplicate-check candidates for" << (all_feeds_same_account ? "account" : "feed") << "in"
           << NONQUOTE_W_SPACE(load_timer.elapsed()) << "milliseconds.";
#endif

  return candidates;
}

void FilteringSystem::removeDuplicateCandidate(int message_id) {
  const auto remove_candidate = [message_id](QList<DuplicateCandidate>& candidates) {
    for (int i = candidates.size() - 1; i >= 0; --i) {
      if (candidates.at(i).m_id == message_id) {
        candidates.removeAt(i);
      }
    }
  };

  if (m_accountDuplicateCandidatesLoaded) {
    remove_candidate(m_accountDuplicateCandidates);
  }

  if (m_feedDuplicateCandidatesLoaded) {
    remove_candidate(m_feedDuplicateCandidates);
  }
}

QJSValue FilteringSystem::prepareFilter(const MessageFilter& filter) {
  const auto* filter_key = &filter;
  const auto prepared_filter = m_preparedFilters.constFind(filter_key);

  if (prepared_filter != m_preparedFilters.constEnd()) {
    return prepared_filter.value();
  }

  // Keep each filter script in its own scope so helper functions/variables from
  // one filter do not overwrite helpers from another filter in the shared engine.
  const QString filter_script = QSL("(function() {\n%1\n; return filterMessage;\n})()")
                                  .arg(m_applicationPaths->replaceUserDataFolderPlaceholder(filter.script(), true));
  QJSValue filter_func = m_engine.evaluate(filter_script, filter.name());

  if (filter_func.isError()) {
    throw FilteringException(filter_func);
  }

  if (!filter_func.isCallable()) {
    const QString error_message =
      tr("Article filter '%1' does not define a callable filterMessage() function.").arg(filter.name());
    QJSValue error = m_engine.newErrorObject(QJSValue::TypeError, error_message);

    throw FilteringException(error);
  }

  m_preparedFilters.insert(filter_key, filter_func);
  return filter_func;
}

ApplicationPaths* FilteringSystem::applicationPaths() const {
  return m_applicationPaths;
}

DatabaseFactory* FilteringSystem::database() const {
  return m_databaseFactory;
}

IconFactory* FilteringSystem::icons() const {
  return m_iconFactory;
}

Localization* FilteringSystem::localization() const {
  return m_localization;
}

GuiNotificationCoordinator* FilteringSystem::guiNotifications() const {
  return m_guiNotifications;
}

QJSEngine& FilteringSystem::engine() {
  return m_engine;
}

FilterMessage& FilteringSystem::message() {
  return m_filterMessage;
}

void FilteringSystem::initializeEngine() {
  m_engine.installExtensions(QJSEngine::Extension::ConsoleExtension | QJSEngine::Extension::GarbageCollectionExtension);
  m_engine.setUiLanguage(m_localization->loadedLanguage());

  // msg
  m_engine.globalObject().setProperty(QSL("Msg"), m_engine.newQMetaObject(&FilterMessage::staticMetaObject));
  m_engine.globalObject().setProperty(QSL("msg"), m_engine.newQObject(&m_filterMessage));

  // feed
  m_engine.globalObject().setProperty(QSL("feed"), m_engine.newQObject(&m_filterFeed));

  // acc
  m_engine.globalObject().setProperty(QSL("acc"), m_engine.newQObject(&m_filterAccount));

  // utils
  m_engine.globalObject().setProperty(QSL("utils"), m_engine.newQObject(&m_filterUtils));

  // app
  m_engine.globalObject().setProperty(QSL("App"), m_engine.newQMetaObject(&FilterApp::staticMetaObject));
  m_engine.globalObject().setProperty(QSL("app"), m_engine.newQObject(&m_filterApp));

  // run
  m_engine.globalObject().setProperty(QSL("run"), m_engine.newQObject(&m_filterRun));

  // fs
  m_engine.globalObject().setProperty(QSL("fs"), m_engine.newQObject(&m_filterFs));
}

FilterApp& FilteringSystem::filterApp() {
  return m_filterApp;
}

FilterAccount& FilteringSystem::filterAccount() {
  return m_filterAccount;
}

FilterRun& FilteringSystem::filterRun() {
  return m_filterRun;
}

FilteringSystem::FiteringUseCase FilteringSystem::mode() const {
  return m_mode;
}

QList<Label*>& FilteringSystem::availableLabels() {
  return m_availableLabels;
}

ServiceRoot* FilteringSystem::account() const {
  return m_account;
}

Feed* FilteringSystem::feed() const {
  return m_feed;
}
