/*
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 */

#include "mailinglist.h"
using namespace Qt::Literals::StringLiterals;

#include "messagecore_debug.h"
#include <KConfig>
#include <KConfigGroup>
#include <QUrl>

#include <QSharedData>
#include <QStringList>

using namespace MessageCore;

using MagicDetectorFunc = QString (*)(const std::shared_ptr<KMime::Message> &, QByteArray &, QString &);

/* Sender: (owner-([^@]+)|([^@+]-owner)@ */
static QString check_sender(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header = message->sender()->asUnicodeString();

    if (header.isEmpty()) {
        return {};
    }

    if (header.left(6) == QLatin1StringView("owner-")) {
        headerName = "Sender";
        headerValue = header;
        header = header.mid(6, header.indexOf(u'@') - 6);
    } else {
        const int index = header.indexOf(QLatin1StringView("-owner@ "));
        if (index == -1) {
            return {};
        }

        header.truncate(index);
        headerName = "Sender";
        headerValue = header;
    }

    return header;
}

/* X-BeenThere: ([^@]+) */
static QString check_x_beenthere(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("X-BeenThere")) {
        header = hrd->asUnicodeString();
    }
    if (header.isNull() || header.indexOf(u'@') == -1) {
        return {};
    }

    headerName = "X-BeenThere";
    headerValue = header;
    header.truncate(header.indexOf(u'@'));

    return header;
}

/* Delivered-To:: <([^@]+) */
static QString check_delivered_to(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("Delivered-To")) {
        header = hrd->asUnicodeString();
    }
    if (header.isNull() || header.left(13) != QLatin1StringView("mailing list") || header.indexOf(u'@') == -1) {
        return {};
    }

    headerName = "Delivered-To";
    headerValue = header;

    return header.mid(13, header.indexOf(u'@') - 13);
}

/* X-Mailing-List: <?([^@]+) */
static QString check_x_mailing_list(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("X-Mailing-List")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    if (header.indexOf(u'@') < 1) {
        return {};
    }

    headerName = "X-Mailing-List";
    headerValue = header;
    if (header[0] == u'<') {
        header = header.mid(1, header.indexOf(u'@') - 1);
    } else {
        header.truncate(header.indexOf(u'@'));
    }

    return header;
}

/* List-Id: [^<]* <([^.]+) */
static QString check_list_id(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("List-Id")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    const int leftAnglePos = header.indexOf(u'<');
    if (leftAnglePos < 0) {
        return {};
    }

    const int firstDotPos = header.indexOf(u'.', leftAnglePos);
    if (firstDotPos < 0) {
        return {};
    }

    headerName = "List-Id";
    headerValue = header.mid(leftAnglePos);
    header = header.mid(leftAnglePos + 1, firstDotPos - leftAnglePos - 1);

    return header;
}

/* List-Post: <mailto:[^< ]*>) */
static QString check_list_post(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("List-Post")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    int leftAnglePos = header.indexOf(QLatin1StringView("<mailto:"));
    if (leftAnglePos < 0) {
        return {};
    }

    headerName = "List-Post";
    headerValue = header;
    header = header.mid(leftAnglePos + 8, header.length());
    header.truncate(header.indexOf(u'@'));

    return header;
}

/* Mailing-List: list ([^@]+) */
static QString check_mailing_list(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("Mailing-List")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    if (header.left(5) != QLatin1StringView("list ") || header.indexOf(u'@') < 5) {
        return {};
    }

    headerName = "Mailing-List";
    headerValue = header;
    header = header.mid(5, header.indexOf(u'@') - 5);

    return header;
}

/* X-Loop: ([^@]+) */
static QString check_x_loop(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("X-Loop")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    const int indexOfHeader(header.indexOf(u'@'));
    if (indexOfHeader < 2) {
        return {};
    }

    headerName = "X-Loop";
    headerValue = header;
    header.truncate(indexOfHeader);

    return header;
}

/* X-ML-Name: (.+) */
static QString check_x_ml_name(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    QString header;
    if (auto hrd = message->headerByType("X-ML-Name")) {
        header = hrd->asUnicodeString();
    }
    if (header.isEmpty()) {
        return {};
    }

    headerName = "X-ML-Name";
    headerValue = header;
    header.truncate(header.indexOf(u'@'));

    return header;
}

static const MagicDetectorFunc magic_detectors[] = {check_list_id,
                                                    check_list_post,
                                                    check_sender,
                                                    check_x_mailing_list,
                                                    check_mailing_list,
                                                    check_delivered_to,
                                                    check_x_beenthere,
                                                    check_x_loop,
                                                    check_x_ml_name};

static QStringList headerToAddress(const QString &header)
{
    QStringList addresses;
    if (header.isEmpty()) {
        return addresses;
    }

    int start = 0;
    while ((start = header.indexOf(u'<', start)) != -1) {
        int end = 0;
        if ((end = header.indexOf(u'>', ++start)) == -1) {
            qCWarning(MESSAGECORE_LOG) << "Serious mailing list header parsing error!";
            return addresses;
        }

        addresses.append(header.mid(start, end - start));
    }

    return addresses;
}

class Q_DECL_HIDDEN MessageCore::MailingList::MailingListPrivate : public QSharedData
{
public:
    MailingListPrivate()
        : mFeatures(None)

    {
    }

    MailingListPrivate(const MailingListPrivate &other)
        : QSharedData(other)
    {
        mFeatures = other.mFeatures;
        mHandler = other.mHandler;
        mPostUrls = other.mPostUrls;
        mSubscribeUrls = other.mSubscribeUrls;
        mUnsubscribeUrls = other.mUnsubscribeUrls;
        mHelpUrls = other.mHelpUrls;
        mArchiveUrls = other.mArchiveUrls;
        mOwnerUrls = other.mOwnerUrls;
        mArchivedAtUrls = other.mArchivedAtUrls;
        mId = other.mId;
    }

    Features mFeatures;
    Handler mHandler{KMail};
    QList<QUrl> mPostUrls;
    QList<QUrl> mSubscribeUrls;
    QList<QUrl> mUnsubscribeUrls;
    QList<QUrl> mHelpUrls;
    QList<QUrl> mArchiveUrls;
    QList<QUrl> mOwnerUrls;
    QList<QUrl> mArchivedAtUrls;
    QString mId;
};

MailingList MailingList::detect(const std::shared_ptr<KMime::Message> &message)
{
    MailingList mailingList;

    if (auto hrd = message->headerByType("List-Post")) {
        mailingList.setPostUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Help")) {
        mailingList.setHelpUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Subscribe")) {
        mailingList.setSubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Unsubscribe")) {
        mailingList.setUnsubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Archive")) {
        mailingList.setArchiveUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Owner")) {
        mailingList.setOwnerUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("Archived-At")) {
        mailingList.setArchivedAtUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
    }

    if (auto hrd = message->headerByType("List-Id")) {
        mailingList.setId(hrd->asUnicodeString());
    }

    return mailingList;
}

QString MailingList::name(const std::shared_ptr<KMime::Message> &message, QByteArray &headerName, QString &headerValue)
{
    headerName = QByteArray();
    headerValue.clear();

    if (!message) {
        return {};
    }

    for (const MagicDetectorFunc &detector : magic_detectors) {
        const QString mailingList = detector(message, headerName, headerValue);
        if (!mailingList.isNull()) {
            return mailingList;
        }
    }

    return {};
}

MailingList::MailingList()
    : d(new MailingListPrivate)
{
}

MailingList::MailingList(const MailingList &other)

    = default;

MailingList &MailingList::operator=(const MailingList &other)
{
    if (this != &other) {
        d = other.d;
    }

    return *this;
}

bool MailingList::operator==(const MailingList &other) const
{
    return other.features() == d->mFeatures && other.handler() == d->mHandler && other.postUrls() == d->mPostUrls && other.subscribeUrls() == d->mSubscribeUrls
        && other.unsubscribeUrls() == d->mUnsubscribeUrls && other.helpUrls() == d->mHelpUrls && other.archiveUrls() == d->mArchiveUrls
        && other.ownerUrls() == d->mOwnerUrls && other.archivedAtUrls() == d->mArchivedAtUrls && other.id() == d->mId;
}

MailingList::~MailingList() = default;

MailingList::Features MailingList::features() const
{
    return d->mFeatures;
}

void MailingList::setHandler(MailingList::Handler handler)
{
    d->mHandler = handler;
}

MailingList::Handler MailingList::handler() const
{
    return d->mHandler;
}

void MailingList::setPostUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Post;

    if (urls.empty()) {
        d->mFeatures ^= Post;
    }

    d->mPostUrls = urls;
}

QList<QUrl> MailingList::postUrls() const
{
    return d->mPostUrls;
}

void MailingList::setSubscribeUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Subscribe;

    if (urls.empty()) {
        d->mFeatures ^= Subscribe;
    }

    d->mSubscribeUrls = urls;
}

QList<QUrl> MailingList::subscribeUrls() const
{
    return d->mSubscribeUrls;
}

void MailingList::setUnsubscribeUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Unsubscribe;

    if (urls.empty()) {
        d->mFeatures ^= Unsubscribe;
    }

    d->mUnsubscribeUrls = urls;
}

QList<QUrl> MailingList::unsubscribeUrls() const
{
    return d->mUnsubscribeUrls;
}

void MailingList::setHelpUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Help;

    if (urls.empty()) {
        d->mFeatures ^= Help;
    }

    d->mHelpUrls = urls;
}

QList<QUrl> MailingList::helpUrls() const
{
    return d->mHelpUrls;
}

void MailingList::setArchiveUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Archive;

    if (urls.empty()) {
        d->mFeatures ^= Archive;
    }

    d->mArchiveUrls = urls;
}

QList<QUrl> MailingList::archiveUrls() const
{
    return d->mArchiveUrls;
}

void MailingList::setOwnerUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= Owner;

    if (urls.empty()) {
        d->mFeatures ^= Owner;
    }

    d->mOwnerUrls = urls;
}

QList<QUrl> MailingList::ownerUrls() const
{
    return d->mOwnerUrls;
}

void MailingList::setArchivedAtUrls(const QList<QUrl> &urls)
{
    d->mFeatures |= ArchivedAt;

    if (urls.isEmpty()) {
        d->mFeatures ^= ArchivedAt;
    }

    d->mArchivedAtUrls = urls;
}

QList<QUrl> MailingList::archivedAtUrls() const
{
    return d->mArchivedAtUrls;
}

void MailingList::setId(const QString &id)
{
    d->mFeatures |= Id;

    if (id.isEmpty()) {
        d->mFeatures ^= Id;
    }

    d->mId = id;
}

QString MailingList::id() const
{
    return d->mId;
}

void MailingList::writeConfig(KConfigGroup &group) const
{
    if (d->mFeatures != Feature::None) {
        group.writeEntry("MailingListFeatures", static_cast<int>(d->mFeatures));
    } else {
        group.deleteEntry("MailingListFeatures");
    }
    if (d->mHandler != Handler::KMail) {
        group.writeEntry("MailingListHandler", static_cast<int>(d->mHandler));
    } else {
        group.deleteEntry("MailingListHandler");
    }
    if (!d->mId.isEmpty()) {
        group.writeEntry("MailingListId", d->mId);
    } else {
        group.deleteEntry("MailingListId");
    }
    QStringList lst = QUrl::toStringList(d->mPostUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListPostingAddress", lst);
    } else {
        group.deleteEntry("MailingListPostingAddress");
    }

    lst = QUrl::toStringList(d->mSubscribeUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListSubscribeAddress", lst);
    } else {
        group.deleteEntry("MailingListSubscribeAddress");
    }

    lst = QUrl::toStringList(d->mUnsubscribeUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListUnsubscribeAddress", lst);
    } else {
        group.deleteEntry("MailingListUnsubscribeAddress");
    }

    lst = QUrl::toStringList(d->mArchiveUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListArchiveAddress", lst);
    } else {
        group.deleteEntry("MailingListArchiveAddress");
    }

    lst = QUrl::toStringList(d->mOwnerUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListOwnerAddress", lst);
    } else {
        group.deleteEntry("MailingListOwnerAddress");
    }

    lst = QUrl::toStringList(d->mHelpUrls);
    if (!lst.isEmpty()) {
        group.writeEntry("MailingListHelpAddress", lst);
    } else {
        group.deleteEntry("MailingListHelpAddress");
    }

    /* Note: mArchivedAtUrl deliberately not saved here as it refers to a single
     * instance of a message rather than an element of a general mailing list.
     * http://reviewboard.kde.org/r/1768/#review2783
     */
}

void MailingList::readConfig(const KConfigGroup &group)
{
    d->mFeatures = static_cast<MailingList::Features>(group.readEntry("MailingListFeatures", 0));
    d->mHandler = static_cast<MailingList::Handler>(group.readEntry("MailingListHandler", static_cast<int>(MailingList::KMail)));
    d->mId = group.readEntry("MailingListId");
    d->mPostUrls = QUrl::fromStringList(group.readEntry("MailingListPostingAddress", QStringList()));
    d->mSubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListSubscribeAddress", QStringList()));
    d->mUnsubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListUnsubscribeAddress", QStringList()));
    d->mArchiveUrls = QUrl::fromStringList(group.readEntry("MailingListArchiveAddress", QStringList()));
    d->mOwnerUrls = QUrl::fromStringList(group.readEntry("MailingListOwnerAddress", QStringList()));
    d->mHelpUrls = QUrl::fromStringList(group.readEntry("MailingListHelpAddress", QStringList()));
}
