/*
    SPDX-FileCopyrightText: 2009 Grégory Oestreicher <greg@kamago.net>
      Based on an original work for the IMAP resource which is :
    SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
    SPDX-FileCopyrightText: 2008 Omat Holding B.V. <info@omat.nl>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "settings.h"

#include "davresource_debug.h"
#include "settingsadaptor.h"
#include "utils.h"

#include <KAuthorized>
#include <KLocalizedString>

#include <KDAV/ProtocolInfo>

#include <QByteArray>
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDataStream>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QUrl>

#include <qt6keychain/keychain.h>
using namespace QKeychain;
using namespace Qt::Literals;

namespace
{
QString createDefaultPasswordKey(const KSharedConfigPtr &config)
{
    const auto configName = config->name();
    // We remove the "rc" suffix (contrary to IMAP) because of existing resources
    // those were storing their password with the resource identifier which is the
    // same than the config name (when AKONADI_INSTANCE isn't used) but without "rc"
    Q_ASSERT(configName.endsWith(QStringLiteral("rc")));
    return configName.chopped(2);
}
}

class SettingsHelper
{
public:
    SettingsHelper() = default;

    ~SettingsHelper()
    {
        delete q;
    }

    Settings *q = nullptr;
};

Settings::UrlConfiguration::UrlConfiguration() = default;

Settings::UrlConfiguration::UrlConfiguration(const QString &serialized)
{
    const QStringList splitString = serialized.split(u'|');

    if (splitString.size() == 3) {
        mUrl = splitString.at(2);
        mProtocol = KDAV::ProtocolInfo::protocolByName(splitString.at(1));
        mUser = splitString.at(0);
    }
}

QString Settings::UrlConfiguration::serialize()
{
    QString serialized = mUser;
    serialized.append(u'|').append(KDAV::ProtocolInfo::protocolName(KDAV::Protocol(mProtocol)));
    serialized.append(u'|').append(mUrl);
    return serialized;
}

Settings::Settings(const KSharedConfigPtr &config, Options options)
    : SettingsBase(config)
    , mDefaultPasswordKey(createDefaultPasswordKey(config))
{
    if (options & Option::ExportToDBus) {
        new SettingsAdaptor(this);
        QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"),
                                                     this,
                                                     QDBusConnection::ExportAdaptors | QDBusConnection::ExportScriptableContents);
    }
}

Settings::~Settings()
{
    QMapIterator<QString, UrlConfiguration *> it(mUrls);
    while (it.hasNext()) {
        it.next();
        delete it.value();
    }
}

void Settings::cleanup()
{
    const QString entry = mDefaultPasswordKey + u',' + QStringLiteral("$default$");
    auto deleteJob = new DeletePasswordJob(QStringLiteral("Passwords"));
    deleteJob->setKey(entry);
    deleteJob->start();
    QFile cacheFile(mCollectionsUrlsMappingCache);
    cacheFile.remove();
}

void Settings::setDefaultPassword(const QString &password)
{
    savePassword(mDefaultPasswordKey, QStringLiteral("$default$"), password);
}

QString Settings::defaultPassword()
{
    return loadPasswordFromWallet(mDefaultPasswordKey, QStringLiteral("$default$"));
}

void Settings::setIconName(const QString &icon)
{
    mIconName = icon;
}

QString Settings::iconName() const
{
    return mIconName;
}

void Settings::setAccountId(const QString &accountId)
{
    mAccountId = accountId;
}

KDAV::DavUrl::List Settings::configuredDavUrls()
{
    if (mUrls.isEmpty()) {
        buildUrlsList();
    }
    KDAV::DavUrl::List davUrls;
    davUrls.reserve(mUrls.count());
    QMap<QString, UrlConfiguration *>::const_iterator it = mUrls.cbegin();
    const QMap<QString, UrlConfiguration *>::const_iterator itEnd = mUrls.cend();
    for (; it != itEnd; ++it) {
        const QStringList split = it.key().split(u',');
        davUrls << configuredDavUrl(KDAV::ProtocolInfo::protocolByName(split.at(1)), split.at(0));
    }

    return davUrls;
}

KDAV::DavUrl Settings::configuredDavUrl(KDAV::Protocol proto, const QString &searchUrl, const QString &finalUrl)
{
    if (mUrls.isEmpty()) {
        buildUrlsList();
    }

    QUrl fullUrl;

    if (!finalUrl.isEmpty()) {
        fullUrl = QUrl::fromUserInput(finalUrl);
        if (finalUrl.startsWith(u'/')) {
            QUrl searchQUrl(searchUrl);
            fullUrl.setHost(searchQUrl.host());
            fullUrl.setScheme(searchQUrl.scheme());
            fullUrl.setPort(searchQUrl.port());
        }
    } else {
        fullUrl = QUrl::fromUserInput(searchUrl);
    }

    const QString user = username(proto, searchUrl);
    fullUrl.setUserName(user);
    fullUrl.setPassword(password(proto, searchUrl));

    return KDAV::DavUrl(fullUrl, proto);
}

KDAV::DavUrl Settings::davUrlFromCollectionUrl(const QString &collectionUrl, const QString &finalUrl)
{
    if (mCollectionsUrlsMapping.isEmpty()) {
        loadMappings();
    }

    KDAV::DavUrl davUrl;
    const QString targetUrl = finalUrl.isEmpty() ? collectionUrl : finalUrl;

    if (mCollectionsUrlsMapping.contains(collectionUrl)) {
        const QStringList split = mCollectionsUrlsMapping.value(collectionUrl).split(u',');
        if (split.size() == 2) {
            davUrl = configuredDavUrl(KDAV::ProtocolInfo::protocolByName(split.at(1)), split.at(0), targetUrl);
        }
    }

    return davUrl;
}

void Settings::addCollectionUrlMapping(KDAV::Protocol proto, const QString &collectionUrl, const QString &configuredUrl)
{
    if (mCollectionsUrlsMapping.isEmpty()) {
        loadMappings();
    }

    const QString value = configuredUrl + u',' + KDAV::ProtocolInfo::protocolName(proto);
    mCollectionsUrlsMapping.insert(collectionUrl, value);

    // Update the cache now
    // QMap<QString, QString> tmp( mCollectionsUrlsMapping );
    QFileInfo cacheFileInfo = QFileInfo(mCollectionsUrlsMappingCache);
    if (!cacheFileInfo.dir().exists()) {
        QDir::root().mkpath(cacheFileInfo.dir().absolutePath());
    }

    QFile cacheFile(mCollectionsUrlsMappingCache);
    if (cacheFile.open(QIODevice::WriteOnly)) {
        QDataStream cache(&cacheFile);
        cache.setVersion(QDataStream::Qt_4_7);
        cache << mCollectionsUrlsMapping;
        cacheFile.close();
    }
}

QStringList Settings::mappedCollections(KDAV::Protocol proto, const QString &configuredUrl)
{
    if (mCollectionsUrlsMapping.isEmpty()) {
        loadMappings();
    }

    const QString value = configuredUrl + u',' + KDAV::ProtocolInfo::protocolName(proto);
    return mCollectionsUrlsMapping.keys(value);
}

void Settings::reloadConfig()
{
    load();
    buildUrlsList();
    updateRemoteUrls();
    loadMappings();
}

void Settings::newUrlConfiguration(Settings::UrlConfiguration *urlConfig, UrlConfigurationPasswordPolicy passwordPolicy)
{
    const QString key = urlConfig->mUrl + u',' + KDAV::ProtocolInfo::protocolName(KDAV::Protocol(urlConfig->mProtocol));

    if (mUrls.contains(key)) {
        removeUrlConfiguration(KDAV::Protocol(urlConfig->mProtocol), urlConfig->mUrl);
    }

    mUrls[key] = urlConfig;
    if (urlConfig->mUser != QLatin1StringView("$default$") && passwordPolicy == StorePassword) {
        savePassword(key, urlConfig->mUser, urlConfig->mPassword);
    }
    updateRemoteUrls();
}

void Settings::removeUrlConfiguration(KDAV::Protocol proto, const QString &url)
{
    const QString key = url + u',' + KDAV::ProtocolInfo::protocolName(proto);

    if (!mUrls.contains(key)) {
        return;
    }

    delete mUrls[key];
    mUrls.remove(key);
    updateRemoteUrls();
}

Settings::UrlConfiguration *Settings::urlConfiguration(KDAV::Protocol proto, const QString &url)
{
    const QString key = url + u',' + KDAV::ProtocolInfo::protocolName(proto);

    UrlConfiguration *ret = nullptr;
    if (mUrls.contains(key)) {
        ret = mUrls[key];
    }

    return ret;
}

// KDAV::Protocol Settings::protocol( const QString &url ) const
// {
//   if ( mUrls.contains( url ) )
//     return KDAV::Protocol( mUrls[ url ]->mProtocol );
//   else
//     returnKDAV::CalDav;
// }

QString Settings::username(KDAV::Protocol proto, const QString &url) const
{
    const QString key = url + u',' + KDAV::ProtocolInfo::protocolName(proto);

    if (mUrls.contains(key)) {
        if (mUrls[key]->mUser == QLatin1StringView("$default$")) {
            return defaultUsername();
        } else {
            return mUrls[key]->mUser;
        }
    } else {
        return {};
    }
}

QString Settings::password(KDAV::Protocol proto, const QString &url)
{
    const QString key = url + u',' + KDAV::ProtocolInfo::protocolName(proto);

    if (mUrls.contains(key)) {
        if (mUrls[key]->mUser == QLatin1StringView("$default$")) {
            return defaultPassword();
        } else {
            return mUrls[key]->mPassword;
        }
    } else {
        return {};
    }
}

QDateTime Settings::getSyncRangeStart() const
{
    QDateTime start = QDateTime::currentDateTimeUtc();
    start.setTime(QTime());
    const int delta = -syncRangeStartNumber().toUInt();

    if (syncRangeStartType() == u'D') {
        start = start.addDays(delta);
    } else if (syncRangeStartType() == u'M') {
        start = start.addMonths(delta);
    } else if (syncRangeStartType() == u'Y') {
        start = start.addYears(delta);
    } else {
        start = QDateTime();
    }

    return start;
}

void Settings::buildUrlsList()
{
    const auto remoteUrlsLst = remoteUrls();
    for (const QString &serializedUrl : remoteUrlsLst) {
        auto urlConfig = new UrlConfiguration(serializedUrl);

        const QString key = urlConfig->mUrl + u',' + KDAV::ProtocolInfo::protocolName(KDAV::Protocol(urlConfig->mProtocol));

        QString pass;

        if (!mAccountId.isEmpty()) {
            qCDebug(DAVRESOURCE_LOG) << "Reading password from DBus account" << mAccountId;
            pass = loadPasswordFromOnlineAccount(urlConfig->mProtocol);
        } else {
            qCDebug(DAVRESOURCE_LOG) << "Reading password from wallet";
            pass = loadPasswordFromWallet(key, urlConfig->mUser);
        }

        if (!pass.isNull()) {
            urlConfig->mPassword = pass;
            mUrls[key] = urlConfig;
        } else {
            delete urlConfig;
        }
    }
}

void Settings::loadMappings()
{
    const QString collectionsMappingCacheBase = QStringLiteral("akonadi-davgroupware/%1_c2u.dat").arg(QCoreApplication::applicationName());
    mCollectionsUrlsMappingCache = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + u'/' + collectionsMappingCacheBase;
    QFile collectionsMappingsCache(mCollectionsUrlsMappingCache);

    if (collectionsMappingsCache.exists()) {
        if (collectionsMappingsCache.open(QIODevice::ReadOnly)) {
            QDataStream cache(&collectionsMappingsCache);
            cache >> mCollectionsUrlsMapping;
            collectionsMappingsCache.close();
        }
    } else if (!collectionsUrlsMappings().isEmpty()) {
        QByteArray rawMappings = QByteArray::fromBase64(collectionsUrlsMappings().toLatin1());
        QDataStream stream(&rawMappings, QIODevice::ReadOnly);
        stream >> mCollectionsUrlsMapping;
        setCollectionsUrlsMappings(QString());
    }
}

void Settings::updateRemoteUrls()
{
    QStringList newUrls;
    newUrls.reserve(mUrls.count());

    QMapIterator<QString, UrlConfiguration *> it(mUrls);
    while (it.hasNext()) {
        it.next();
        newUrls << it.value()->serialize();
    }

    setRemoteUrls(newUrls);
}

void Settings::savePassword(const QString &key, const QString &user, const QString &password)
{
    const QString entry = key + u',' + user;
    mPasswordsCache[entry] = password;

    auto writeJob = new WritePasswordJob(QStringLiteral("Passwords"));
    connect(writeJob, &QKeychain::Job::finished, this, [](QKeychain::Job *baseJob) {
        if (baseJob->error()) {
            qCWarning(DAVRESOURCE_LOG) << "Error writing password using QKeychain:" << baseJob->errorString();
        }
    });
    writeJob->setKey(entry);
    writeJob->setTextData(password);
    writeJob->start();
}

QString Settings::loadPasswordFromWallet(const QString &key, const QString &user)
{
    QString entry;
    QString pass;

    if (user == QLatin1StringView("$default$")) {
        entry = mDefaultPasswordKey + u',' + user;
    } else {
        entry = key + u',' + user;
    }

    if (mPasswordsCache.contains(entry)) {
        return mPasswordsCache[entry];
    }

    QKeychain::ReadPasswordJob job(QStringLiteral("Passwords"));
    job.setAutoDelete(false);
    job.setKey(entry);
    QEventLoop loop; // Ideally we should have an async API
    QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
    job.start();
    loop.exec();

    if (job.error() == QKeychain::Error::NoError) {
        pass = QString::fromLatin1(job.binaryData());
    }

    if (!pass.isNull()) {
        mPasswordsCache[entry] = pass;
    }

    return pass;
}

QString Settings::loadPasswordFromOnlineAccount(KDAV::Protocol protocol)
{
    QString interface;

    if (protocol == KDAV::Protocol::CalDav) {
        interface = u"org.kde.KOnlineAccounts.CalDAV"_s;
    } else if (protocol == KDAV::Protocol::CardDav) {
        interface = u"org.kde.KOnlineAccounts.CardDAV"_s;
    } else {
        Q_UNREACHABLE();
    }

    Q_ASSERT(!mAccountId.isEmpty());

    QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.KOnlineAccounts"_s, mAccountId, interface, u"password"_s);

    QDBusReply<QDBusUnixFileDescriptor> reply = QDBusConnection::sessionBus().call(msg);

    if (!reply.isValid()) {
        qCWarning(DAVRESOURCE_LOG) << "error reading password from account" << reply.error().message();
        return QString();
    }

    QFile file;
    const bool result = file.open(reply.value().fileDescriptor(), QFile::ReadOnly, QFile::AutoCloseHandle);

    if (!result) {
        qCWarning(DAVRESOURCE_LOG) << "Could not open password fd" << file.errorString();
        return QString();
    }

    return QString::fromUtf8(file.readAll());
}

#include "moc_settings.cpp"
