//
/* Copyright: before 2012: missing, see KMail copyrights
 * SPDX-FileCopyrightText: 2012 Andras Mantia <amantia@kde.org> *
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 */
#include "filtermanager.h"

#include "mailfilteragent_debug.h"
#include <Akonadi/AgentManager>
#include <Akonadi/ChangeRecorder>
#include <Akonadi/CollectionFetchJob>
#include <Akonadi/CollectionFetchScope>
#include <Akonadi/EntityMimeTypeFilterModel>
#include <Akonadi/EntityTreeModel>
#include <Akonadi/ItemDeleteJob>
#include <Akonadi/ItemFetchJob>
#include <Akonadi/ItemFetchScope>
#include <Akonadi/ItemModifyJob>
#include <Akonadi/ItemMoveJob>
#include <Akonadi/MessageParts>
#include <KLocalizedString>
#include <KMime/Message>
#include <KNotification>
#include <MailCommon/FilterImporterExporter>
#include <MailCommon/FilterLog>
#include <MailCommon/MailFilter>
#include <MailCommon/MailKernel>

// other headers
#include <KSharedConfig>
#include <QLocale>
#include <algorithm>
#include <cerrno>

using namespace MailCommon;

class FilterManager::Private
{
public:
    explicit Private(FilterManager *qq)
        : q(qq)
    {
    }

    void itemsFetchJobForFilterDone(KJob *job);
    void itemFetchJobForFilterDone(KJob *job);
    void moveJobResult(KJob *);
    void modifyJobResult(KJob *);
    void deleteJobResult(KJob *);
    void slotItemsFetchedForFilter(const Akonadi::Item::List &items);
    void showNotification(const QString &errorMsg, const QString &jobErrorString);

    [[nodiscard]] bool isMatching(const Akonadi::Item &item, const MailCommon::MailFilter *filter);
    void beginFiltering(const Akonadi::Item &item) const;
    void endFiltering(const Akonadi::Item &item) const;
    [[nodiscard]] bool atLeastOneFilterAppliesTo(const QString &accountId) const;
    [[nodiscard]] bool atLeastOneIncomingFilterAppliesTo(const QString &accountId) const;
    FilterManager *const q;
    QList<MailCommon::MailFilter *> mFilters;
    QMap<QString, SearchRule::RequiredPart> mRequiredParts;
    SearchRule::RequiredPart mRequiredPartsBasedOnAll = SearchRule::Envelope;
    int mTotalProgressCount = 0;
    int mCurrentProgressCount = 0;
    bool mInboundFiltersExist = false;
    bool mAllFoldersFiltersExist = false;
};

void FilterManager::Private::slotItemsFetchedForFilter(const Akonadi::Item::List &items)
{
    FilterManager::FilterSet filterSet = FilterManager::Inbound;
    if (q->sender()->property("filterSet").isValid()) {
        filterSet = static_cast<FilterManager::FilterSet>(q->sender()->property("filterSet").toInt());
    }

    QList<MailFilter *> listMailFilters;
    const QVariant listFiltersProp = q->sender()->property("listFilters");
    if (listFiltersProp.isValid()) {
        const QStringList listFilters = listFiltersProp.toStringList();
        for (const QString &filterId : listFilters) {
            for (MailCommon::MailFilter *filter : std::as_const(mFilters)) {
                if (filter->identifier() == filterId) {
                    listMailFilters << filter;
                    break;
                }
            }
        }
    }

    if (listMailFilters.isEmpty()) {
        listMailFilters = mFilters;
    }

    bool needsFullPayload = q->sender()->property("needsFullPayload").toBool();

    for (const Akonadi::Item &item : std::as_const(items)) {
        ++mCurrentProgressCount;

        if ((mTotalProgressCount > 0) && (mCurrentProgressCount != mTotalProgressCount)) {
            const QString statusMsg = i18n("Filtering message %1 of %2", mCurrentProgressCount, mTotalProgressCount);
            Q_EMIT q->progressMessage(statusMsg);
            Q_EMIT q->percent(mCurrentProgressCount * 100 / mTotalProgressCount);
        } else {
            Q_EMIT q->percent(0);
        }

        const bool filterResult = q->process(listMailFilters, item, needsFullPayload, filterSet);

        if (mCurrentProgressCount == mTotalProgressCount) {
            mTotalProgressCount = 0;
            mCurrentProgressCount = 0;
        }

        if (!filterResult) {
            Q_EMIT q->filteringFailed(item);
            // something went horribly wrong (out of space?)
            // CommonKernel->emergencyExit( i18n( "Unable to process messages: " ) + QString::fromLocal8Bit( strerror( errno ) ) );
        }
    }
}

void FilterManager::Private::itemsFetchJobForFilterDone(KJob *job)
{
    if (job->error()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Error while fetching items. " << job->error() << job->errorString();
    }
}

void FilterManager::Private::itemFetchJobForFilterDone(KJob *job)
{
    if (job->error()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Error while fetching item. " << job->error() << job->errorString();
        return;
    }

    const Akonadi::ItemFetchJob *fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);

    const Akonadi::Item::List items = fetchJob->items();
    if (items.isEmpty()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Error while fetching item: item not found";
        return;
    }

    const QString resourceId = fetchJob->property("resourceId").toString();
    const bool needsFullPayload = q->requiredPart(resourceId) != SearchRule::Envelope;

    if (job->property("filterId").isValid()) {
        const QString filterId = job->property("filterId").toString();

        // find correct filter object
        MailCommon::MailFilter *wantedFilter = nullptr;
        for (MailCommon::MailFilter *filter : std::as_const(mFilters)) {
            if (filter->identifier() == filterId) {
                wantedFilter = filter;
                break;
            }
        }

        if (!wantedFilter) {
            qCCritical(MAILFILTERAGENT_LOG) << "Cannot find filter object with id" << filterId;
            return;
        }

        if (!q->process(items.first(), needsFullPayload, wantedFilter)) {
            Q_EMIT q->filteringFailed(items.first());
        }
    } else {
        const FilterManager::FilterSet set = static_cast<FilterManager::FilterSet>(job->property("filterSet").toInt());

        if (!q->process(items.first(), needsFullPayload, set, !resourceId.isEmpty(), resourceId)) {
            Q_EMIT q->filteringFailed(items.first());
        }
    }
}

void FilterManager::Private::moveJobResult(KJob *job)
{
    if (job->error()) {
        const Akonadi::ItemMoveJob *movejob = qobject_cast<Akonadi::ItemMoveJob *>(job);
        if (movejob) {
            qCCritical(MAILFILTERAGENT_LOG) << "Error while moving items. " << job->error() << job->errorString()
                                            << " to destinationCollection.id() :" << movejob->destinationCollection().id();
        } else {
            qCCritical(MAILFILTERAGENT_LOG) << "Error while moving items. " << job->error() << job->errorString();
        }
        // Laurent: not real info and when we have 200 errors it's very long to click all the time on ok.
        showNotification(i18n("Error applying mail filter move"), job->errorString());
    }
}

void FilterManager::Private::deleteJobResult(KJob *job)
{
    if (job->error()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Error while delete items. " << job->error() << job->errorString();
        showNotification(i18n("Error applying mail filter delete"), job->errorString());
    }
}

void FilterManager::Private::modifyJobResult(KJob *job)
{
    if (job->error()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Error while modifying items. " << job->error() << job->errorString();
        showNotification(i18n("Error applying mail filter modifications"), job->errorString());
    }
}

void FilterManager::Private::showNotification(const QString &errorMsg, const QString &jobErrorString)
{
    auto notify = new KNotification(QStringLiteral("mailfilterjoberror"));
    notify->setComponentName(QStringLiteral("akonadi_mailfilter_agent"));
    notify->setIconName(QStringLiteral("view-filter"));
    notify->setText(errorMsg + QLatin1Char('\n') + jobErrorString);
    notify->sendEvent();
}

bool FilterManager::Private::isMatching(const Akonadi::Item &item, const MailCommon::MailFilter *filter)
{
    bool result = false;
    if (FilterLog::instance()->isLogging()) {
        QString logText(i18n("<b>Evaluating filter rules:</b> "));
        logText.append(filter->pattern()->asString());
        FilterLog::instance()->add(logText, FilterLog::PatternDescription);
    }

    if (filter->pattern()->matches(item)) {
        if (FilterLog::instance()->isLogging()) {
            FilterLog::instance()->add(i18n("<b>Filter rules have matched.</b>"), FilterLog::PatternResult);
        }

        result = true;
    }

    return result;
}

void FilterManager::Private::beginFiltering(const Akonadi::Item &item) const
{
    if (FilterLog::instance()->isLogging()) {
        FilterLog::instance()->addSeparator();
        if (item.hasPayload<KMime::Message::Ptr>()) {
            auto msg = item.payload<KMime::Message::Ptr>();
            QString subject;
            if (auto msgSubject = msg->subject(false)) {
                subject = msgSubject->asUnicodeString();
            }
            QString from;
            if (auto msgFrom = msg->from(false)) {
                from = msgFrom->asUnicodeString();
            }
            QDateTime dateTime;
            if (auto msgDate = msg->date(false)) {
                dateTime = msgDate->dateTime();
            }
            const QString date = QLocale().toString(dateTime, QLocale::LongFormat);
            const QString logText(i18n("<b>Begin filtering on message \"%1\" from \"%2\" at \"%3\" :</b>", subject, from, date));
            FilterLog::instance()->add(logText, FilterLog::PatternDescription);
        }
    }
}

void FilterManager::Private::endFiltering(const Akonadi::Item & /*item*/) const
{
}

bool FilterManager::Private::atLeastOneFilterAppliesTo(const QString &accountId) const
{
    for (const MailCommon::MailFilter *filter : std::as_const(mFilters)) {
        if (filter->applyOnAccount(accountId)) {
            return true;
        }
    }

    return false;
}

bool FilterManager::Private::atLeastOneIncomingFilterAppliesTo(const QString &accountId) const
{
    for (const MailCommon::MailFilter *filter : std::as_const(mFilters)) {
        if (filter->applyOnInbound() && filter->applyOnAccount(accountId)) {
            return true;
        }
    }

    return false;
}

FilterManager::FilterManager(QObject *parent)
    : QObject(parent)
    , d(new Private(this))
{
}

FilterManager::~FilterManager()
{
    clear();
}

void FilterManager::clear()
{
    qDeleteAll(d->mFilters);
    d->mFilters.clear();
}

void FilterManager::readConfig()
{
    KSharedConfig::Ptr config = KSharedConfig::openConfig(); // use akonadi_mailfilter_agentrc
    config->reparseConfiguration();
    clear();

    QStringList emptyFilters;
    d->mFilters = FilterImporterExporter::readFiltersFromConfig(config, emptyFilters);
    d->mRequiredParts.clear();

    d->mRequiredPartsBasedOnAll = SearchRule::Envelope;
    if (!d->mFilters.isEmpty()) {
        const Akonadi::AgentInstance::List agents = Akonadi::AgentManager::self()->instances();
        for (const Akonadi::AgentInstance &agent : agents) {
            const QString id = agent.identifier();

            auto it = std::max_element(d->mFilters.constBegin(), d->mFilters.constEnd(), [id](MailCommon::MailFilter *lhs, MailCommon::MailFilter *rhs) {
                return lhs->requiredPart(id) < rhs->requiredPart(id);
            });
            const SearchRule::RequiredPart rp = (*it)->requiredPart(id);
            d->mRequiredParts.insert(id, rp);
            d->mRequiredPartsBasedOnAll = qMax(d->mRequiredPartsBasedOnAll, rp);
        }
    }
    // check if at least one filter is to be applied on inbound mail

    for (auto i = d->mFilters.cbegin(), e = d->mFilters.cend(); i != e && (!d->mInboundFiltersExist || !d->mAllFoldersFiltersExist); ++i) {
        if ((*i)->applyOnInbound()) {
            d->mInboundFiltersExist = true;
        }
        if ((*i)->applyOnAllFoldersInbound()) {
            d->mAllFoldersFiltersExist = true;
        }
    }

    Q_EMIT filterListUpdated();
}

void FilterManager::mailCollectionRemoved(const Akonadi::Collection &collection)
{
    QList<MailCommon::MailFilter *>::const_iterator end(d->mFilters.constEnd());
    for (QList<MailCommon::MailFilter *>::const_iterator it = d->mFilters.constBegin(); it != end; ++it) {
        (*it)->folderRemoved(collection, Akonadi::Collection());
    }
}

void FilterManager::agentRemoved(const QString &identifier)
{
    QList<MailCommon::MailFilter *>::const_iterator end(d->mFilters.constEnd());
    for (QList<MailCommon::MailFilter *>::const_iterator it = d->mFilters.constBegin(); it != end; ++it) {
        (*it)->agentRemoved(identifier);
    }
}

void FilterManager::filter(const Akonadi::Item &item, FilterManager::FilterSet set, const QString &resourceId)
{
    auto job = new Akonadi::ItemFetchJob(item, this);
    job->setProperty("filterSet", static_cast<int>(set));
    job->setProperty("resourceId", resourceId);
    SearchRule::RequiredPart requestedPart = requiredPart(resourceId);
    if (requestedPart == SearchRule::CompleteMessage) {
        job->fetchScope().fetchFullPayload(true);
    } else if (requestedPart == SearchRule::Header) {
        job->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Header, true);
    } else {
        job->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Envelope, true);
    }
    job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);

    connect(job, &Akonadi::ItemFetchJob::result, this, [this](KJob *job) {
        d->itemFetchJobForFilterDone(job);
    });
}

void FilterManager::filter(const Akonadi::Item &item, const QString &filterId, const QString &resourceId)
{
    auto job = new Akonadi::ItemFetchJob(item, this);
    job->setProperty("filterId", filterId);

    SearchRule::RequiredPart requestedPart = requiredPart(resourceId);
    if (requestedPart == SearchRule::CompleteMessage) {
        job->fetchScope().fetchFullPayload(true);
    } else if (requestedPart == SearchRule::Header) {
        job->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Header, true);
    } else {
        job->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Envelope, true);
    }

    job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);

    connect(job, &Akonadi::ItemFetchJob::result, this, [this](KJob *job) {
        d->itemFetchJobForFilterDone(job);
    });
}

bool FilterManager::process(const Akonadi::Item &item, bool needsFullPayload, const MailFilter *filter)
{
    if (!filter) {
        return false;
    }

    if (!filter->isEnabled()) {
        return true;
    }

    if (!item.hasPayload<KMime::Message::Ptr>()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Filter is null or item doesn't have correct payload.";
        return false;
    }

    if (d->isMatching(item, filter)) {
        // do the actual filtering stuff
        d->beginFiltering(item);

        ItemContext context(item, needsFullPayload);

        bool stopIt = false;
        bool applyOnOutbound = false;
        if (filter->execActions(context, stopIt, applyOnOutbound) == MailCommon::MailFilter::CriticalError) {
            return false;
        }

        d->endFiltering(item);

        if (!processContextItem(context)) {
            return false;
        }
    }

    return true;
}

bool FilterManager::processContextItem(ItemContext context)
{
    const auto msg = context.item().payload<KMime::Message::Ptr>();
    msg->assemble();

    auto col = Akonadi::EntityTreeModel::updatedCollection(MailCommon::Kernel::self()->kernelIf()->collectionModel(), context.item().parentCollection());
    const bool itemCanDelete = (col.rights() & Akonadi::Collection::CanDeleteItem);
    if (context.deleteItem()) {
        if (itemCanDelete) {
            auto deleteJob = new Akonadi::ItemDeleteJob(context.item(), this);
            connect(deleteJob, &Akonadi::ItemDeleteJob::result, this, [this](KJob *job) {
                d->deleteJobResult(job);
            });
        } else {
            return false;
        }
    } else {
        if (context.moveTargetCollection().isValid() && context.item().storageCollectionId() != context.moveTargetCollection().id()) {
            if (itemCanDelete) {
                auto moveJob = new Akonadi::ItemMoveJob(context.item(), context.moveTargetCollection(), this);
                connect(moveJob, &Akonadi::ItemMoveJob::result, this, [this](KJob *job) {
                    d->moveJobResult(job);
                });
            } else {
                return false;
            }
        }
        if (context.needsPayloadStore() || context.needsFlagStore()) {
            Akonadi::Item item = context.item();
            // the item might be in a new collection with a different remote id, so don't try to force on it
            // the previous remote id. Example: move to another collection on another resource => new remoteId, but our context.item()
            // remoteid still holds the old one. Without clearing it, we try to enforce that on the new location, which is
            // anything but good (and the server replies with "NO Only resources can modify remote identifiers"
            item.setRemoteId(QString());
            auto modifyJob = new Akonadi::ItemModifyJob(item, this);
            modifyJob->disableRevisionCheck(); // no conflict handling for mails as no other process could change the mail body and we don't care about flag
                                               // conflicts
            // The below is a safety check to ignore modifying payloads if it was not requested,
            // as in that case we might change the payload to an invalid one
            modifyJob->setIgnorePayload(!context.needsFullPayload());
            connect(modifyJob, &Akonadi::ItemModifyJob::result, this, [this](KJob *job) {
                d->modifyJobResult(job);
            });
        }
    }

    return true;
}

bool FilterManager::process(const QList<MailFilter *> &mailFilters,
                            const Akonadi::Item &item,
                            bool needsFullPayload,
                            FilterManager::FilterSet set,
                            bool account,
                            const QString &accountId)
{
    if (set == NoSet) {
        qCDebug(MAILFILTERAGENT_LOG) << "FilterManager: process() called with not filter set selected";
        return false;
    }

    if (!item.hasPayload<KMime::Message::Ptr>()) {
        qCCritical(MAILFILTERAGENT_LOG) << "Filter is null or item doesn't have correct payload.";
        return false;
    }

    bool stopIt = false;

    d->beginFiltering(item);

    ItemContext context(item, needsFullPayload);
    QList<MailCommon::MailFilter *>::const_iterator end(mailFilters.constEnd());

    const bool applyOnOutbound = ((set & Outbound) || (set & BeforeOutbound));

    for (QList<MailCommon::MailFilter *>::const_iterator it = mailFilters.constBegin(); !stopIt && it != end; ++it) {
        if ((*it)->isEnabled()) {
            const bool inboundOk = ((set & Inbound) && (*it)->applyOnInbound());
            const bool outboundOk = ((set & Outbound) && (*it)->applyOnOutbound());
            const bool beforeOutboundOk = ((set & BeforeOutbound) && (*it)->applyBeforeOutbound());
            const bool explicitOk = ((set & Explicit) && (*it)->applyOnExplicit());
            const bool allFoldersOk = ((set & AllFolders) && (*it)->applyOnAllFoldersInbound());
            const bool accountOk = (!account || (*it)->applyOnAccount(accountId));

            if ((inboundOk && accountOk) || (allFoldersOk && accountOk) || outboundOk || beforeOutboundOk || explicitOk) {
                // filter is applicable

                if (d->isMatching(context.item(), *it)) {
                    // execute actions:
                    if ((*it)->execActions(context, stopIt, applyOnOutbound) == MailCommon::MailFilter::CriticalError) {
                        return false;
                    }
                }
            }
        }
    }

    d->endFiltering(item);
    if (!processContextItem(context)) {
        return false;
    }

    return true;
}

bool FilterManager::process(const Akonadi::Item &item, bool needsFullPayload, FilterSet set, bool account, const QString &accountId)
{
    return process(d->mFilters, item, needsFullPayload, set, account, accountId);
}

QString FilterManager::createUniqueName(const QString &name) const
{
    QString uniqueName = name;

    int counter = 0;
    bool found = true;

    while (found) {
        found = false;
        for (const MailCommon::MailFilter *filter : std::as_const(d->mFilters)) {
            if (!filter->name().compare(uniqueName)) {
                found = true;
                ++counter;
                uniqueName = name;
                uniqueName += QLatin1StringView(" (") + QString::number(counter) + QLatin1StringView(")");
                break;
            }
        }
    }

    return uniqueName;
}

MailCommon::SearchRule::RequiredPart FilterManager::requiredPart(const QString &id) const
{
    if (id.isEmpty()) {
        return d->mRequiredPartsBasedOnAll;
    }
    auto it = d->mRequiredParts.constFind(id);
    return it != d->mRequiredParts.cend() ? it.value() : SearchRule::Envelope;
}

void FilterManager::dump() const
{
    for (const MailCommon::MailFilter *filter : std::as_const(d->mFilters)) {
        qCDebug(MAILFILTERAGENT_LOG) << filter->asString();
    }
}

void FilterManager::applySpecificFilters(const Akonadi::Item::List &selectedMessages,
                                         SearchRule::RequiredPart requiredPart,
                                         const QStringList &listFilters,
                                         FilterSet filterSet)
{
    Q_EMIT progressMessage(i18n("Filtering messages"));
    d->mTotalProgressCount = selectedMessages.size();
    d->mCurrentProgressCount = 0;

    auto itemFetchJob = new Akonadi::ItemFetchJob(selectedMessages, this);
    if (requiredPart == SearchRule::CompleteMessage) {
        itemFetchJob->fetchScope().fetchFullPayload(true);
    } else if (requiredPart == SearchRule::Header) {
        itemFetchJob->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Header, true);
    } else {
        itemFetchJob->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Envelope, true);
    }

    itemFetchJob->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
    if (!listFilters.isEmpty()) {
        itemFetchJob->setProperty("listFilters", QVariant::fromValue(listFilters));
    }
    itemFetchJob->setProperty("filterSet", QVariant::fromValue(static_cast<int>(filterSet)));
    itemFetchJob->setProperty("needsFullPayload", requiredPart != SearchRule::Envelope);

    connect(itemFetchJob, &Akonadi::ItemFetchJob::itemsReceived, this, [this](const Akonadi::Item::List &lst) {
        d->slotItemsFetchedForFilter(lst);
    });
    connect(itemFetchJob, &Akonadi::ItemFetchJob::result, this, [this](KJob *job) {
        d->itemsFetchJobForFilterDone(job);
    });
}

void FilterManager::applyFilters(const Akonadi::Item::List &selectedMessages, FilterSet filterSet)
{
    applySpecificFilters(selectedMessages, requiredPart(QString()), QStringList(), filterSet);
}

bool FilterManager::hasAllFoldersFilter() const
{
    return d->mAllFoldersFiltersExist;
}

#include "moc_filtermanager.cpp"
