/*
    SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer <brouwer@annejan.com>
    SPDX-FileCopyrightText: 2017 Jason A. Donenfeld <Jason@zx2c4.com>
    SPDX-FileCopyrightText: 2020 Charlie Waters <cawiii@me.com>
    SPDX-FileCopyrightText: 2023 g10 Code GmbH
    SPDX-FileContributor: Sune Stolborg Vuorela <sune@vuorela.dk>

    SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "pass.h"
#include "gpgmehelpers.h"
#include "settings.h"
#include <QDir>
#include <QDirIterator>
#include <QGpgME/DecryptJob>
#include <QGpgME/EncryptJob>
#include <QGpgME/KeyListJob>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QSaveFile>
#include <gpgme++/decryptionresult.h>
#include <gpgme++/encryptionresult.h>
#include <gpgme++/keygenerationresult.h>
#include <gpgme++/keylistresult.h>
#include <QDebug>
#include <KI18n/KLocalizedString>

// TODO remove
#include <QMessageBox>

using namespace std;

// TODO remove
Q_REQUIRED_RESULT static bool ensurePassStore(const QString &file, const char *location)
{
    if (file.startsWith(Settings::getPassStore())) {
        return true;
    }
    QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file not in pass store called from %1 %2 %3").arg(QString::fromLocal8Bit(location),file,Settings::getPassStore()));
    return false;
}

// TODO remove
Q_REQUIRED_RESULT static bool ensureSuffix(const QString &file, const char *location)
{
    if (file.endsWith(QStringLiteral(".gpg"))) {
        return true;
    }
    qWarning() << "file without suffix called from " << location;
    QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file without suffix called from %1 %2").arg(QString::fromLocal8Bit(location),file));
    return false;
}

/**
 * @brief Pass::Pass wrapper for using either pass or the pass imitation
 */
Pass::Pass()
{
}

/**
 * @brief Pass::Generate use either pwgen or internal password
 * generator
 * @param length of the desired password
 * @param charset to use for generation
 * @return the password
 */
QString Pass::Generate_b(unsigned int length, const QString &charset)
{
    if (charset.length() > 0) {
        return generateRandomPassword(charset, length);
    } else {
        Q_EMIT critical(i18n("No characters chosen"),
                        i18n("Can't generate password, there are no characters to choose from "
                           "set in the configuration!"));
    }
    return {};
}

/**
 * @brief Pass::listKeys list users
 * @param keystrings
 * @param secret list private keys
 * @return QList<UserInfo> users
 */
QList<UserInfo> Pass::listKeys(const QStringList& keystrings, bool secret)
{
    auto job = protocol->keyListJob();
    std::vector<GpgME::Key> keys;
    job->addMode(GpgME::KeyListMode::WithSecret);
    auto result = job->exec(keystrings, secret, keys);
    if (!isSuccess(result.error())) {
        return {};
    }
    QList<UserInfo> users;
    for (const auto &key : keys) {
        UserInfo ui;
        ui.created = QDateTime::fromTime_t(key.subkey(0).creationTime());
        ui.key_id = fromGpgmeCharStar(key.keyID());
        ui.name = createCombinedNameString(key.userID(0));
        ui.validity = key.userID(0).validityAsString();
        ui.expiry = QDateTime::fromTime_t(key.subkey(0).expirationTime());
        ui.have_secret = key.hasSecret();
        users.append(ui);
    }
    return users;
}

/**
 * @brief Pass::listKeys list users
 * @param keystring
 * @param secret list private keys
 * @return QList<UserInfo> users
 */
QList<UserInfo> Pass::listKeys(const QString& keystring, bool secret)
{
    return listKeys(QStringList(keystring), secret);
}

/**
 * @brief Pass::getRecipientList return list of gpg-id's to encrypt for
 * @param for_file which file (folder) would you like recepients for
 * @return recepients gpg-id contents
 */
QStringList Pass::getRecipientList(const QString& for_file)
{
    if (!ensurePassStore(for_file, Q_FUNC_INFO)) {
        return {};
    }
    QDir gpgIdPath(QFileInfo(for_file).absoluteDir());
    bool found = false;
    while (gpgIdPath.exists() && gpgIdPath.absolutePath().startsWith(Settings::getPassStore())) {
        if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) {
            found = true;
            break;
        }
        if (!gpgIdPath.cdUp())
            break;
    }
    QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : Settings::getPassStore() + QStringLiteral(".gpg-id"));
    if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text))
        return QStringList();
    QStringList recipients;
    while (!gpgId.atEnd()) {
        QString recipient(QString::fromLocal8Bit(gpgId.readLine()));
        recipient = recipient.trimmed();
        if (!recipient.isEmpty())
            recipients += recipient;
    }
    return recipients;
}

void Pass::Show(const QString& file)
{
    if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) {
            return;
    }
    QFile f(file);
    f.open(QIODevice::ReadOnly);
    auto data = f.readAll();
    auto job = protocol->decryptJob();
    connect(job, &QGpgME::DecryptJob::result, this, [this](auto &&result, QByteArray plainText, QString auditLog, auto &&logError) {
        if (isSuccess(result.error())) {
            Q_EMIT finishedShow(QString::fromUtf8(plainText));
        } else {
            Q_EMIT errorString(QString::fromLocal8Bit(result.error().asString()));
            Q_EMIT decryptionError();
        }
        // Q_EMIT log(auditLog);
        Q_UNUSED(logError);
        Q_UNUSED(auditLog);
    });

    job->start(data);
}

void Pass::Insert(const QString& file, const QString& newValue)
{
    if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) {
            return;
    }
    QStringList recipients = Pass::getRecipientList(file);
    auto job = protocol->encryptJob();
    std::vector<GpgME::Key> keys;
    auto ctx = QGpgME::Job::context(job);
    for (const auto &keyId : recipients) {
        GpgME::Error error;
        auto key = ctx->key(keyId.toUtf8().data(), error, false);
        if (!error && !key.isNull()) {
            keys.push_back(key);
        }
    }
    if (keys.empty()) {
        Q_EMIT critical(i18n("Can not edit"),
                        i18n("Could not read encryption key to use, .gpg-id "
                           "file missing or invalid."));
        return;
    }

    connect(job, &QGpgME::EncryptJob::result, this, [this, file](auto &&result, const QByteArray &ciphertext, const QString &log, auto &&auditResult) {
        if (isSuccess(result.error())) {
            QSaveFile f(file);
            bool open = f.open(QIODevice::WriteOnly | QIODevice::Truncate);
            if (!open) {
                Q_EMIT errorString(QStringLiteral("File open failed: %1").arg(f.errorString()));
                return;
            }
            f.write(ciphertext);
            if (f.error() == QFileDevice::NoError) {
                f.commit();
                Q_EMIT finishedInsert();
            } else {
                Q_EMIT errorString(QStringLiteral("File write failed: %1").arg(f.errorString()));
            }
        } else {
            Q_EMIT errorString(QString::fromUtf8(result.error().asString()));
        }
        Q_UNUSED(log);
        Q_UNUSED(auditResult);
    });
    job->start(keys, newValue.toUtf8());
}

void Pass::Remove(const QString& file, bool isDir)
{
    if (!ensurePassStore(file, Q_FUNC_INFO)) {
        return;
    }

    if (!isDir) {
        if(!ensureSuffix(file, Q_FUNC_INFO)) {
            return;
        }
        QFile(file).remove();
    } else {
        QDir dir(file);
        dir.removeRecursively();
    }
}

void Pass::Init(const QString& path, const QList<UserInfo> &users)
{
    if (!ensurePassStore(path, Q_FUNC_INFO)) {
        return;
    }
    QString gpgIdFile = path + QStringLiteral(".gpg-id");
    QFile gpgId(gpgIdFile);
    if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
        Q_EMIT critical(i18n("Cannot update"), i18n("Failed to open .gpg-id for writing."));
        return;
    }
    bool secret_selected = false;
    for (const UserInfo &user : users) {
        if (user.enabled) {
            gpgId.write((user.key_id + QStringLiteral("\n")).toUtf8());
            secret_selected |= user.have_secret;
        }
    }
    gpgId.close();
    if (!secret_selected) {
        Q_EMIT critical(i18n("Check selected users!"),
                        i18n("None of the selected keys have a secret key available.\n"
                           "You will not be able to decrypt any newly added passwords!"));
        return;
    }

    reencryptPath(path);
}

/**
 * @brief ImitatePass::reencryptPath reencrypt all files under the chosen
 * directory
 *
 * This is stil quite experimental..
 * @param dir
 */
void Pass::reencryptPath(const QString &dir)
{
    if (!ensurePassStore(dir, Q_FUNC_INFO)) {
        return;
    }
    Q_EMIT startReencryptPath();
    QDir currentDir;
    QDirIterator gpgFiles(dir, QStringList() << QStringLiteral("*.gpg"), QDir::Files, QDirIterator::Subdirectories);
    QStringList gpgId;
    while (gpgFiles.hasNext()) {
        QString fileName = gpgFiles.next();
        if (gpgFiles.fileInfo().path() != currentDir.path()) {
            gpgId = getRecipientList(fileName);
            gpgId.sort();
        }

        QByteArray plainText;
        QFile f(fileName);
        if (!f.open(QIODevice::ReadOnly)) {
            Q_EMIT errorString(QStringLiteral("Failed to open file: %1").arg(fileName));
            continue;
        }
        QByteArray cipherText = f.readAll();
        f.close();
        auto decryptJob = protocol->decryptJob();
        auto context = QGpgME::Job::context(decryptJob);
        auto decryptResult = decryptJob->exec(cipherText, plainText);
        if (!isSuccess(decryptResult.error())) {
            Q_EMIT errorString(fromGpgmeCharStar(decryptResult.error().asString()));
            continue;
        }

        auto actualRecipients = decryptResult.recipients();

        QStringList actualIds;
        for (auto &recipient : actualRecipients) {
            GpgME::Error error;
            auto key = context->key(recipient.keyID(), error, false);
            if (!error) {
                actualIds.append(fromGpgmeCharStar(key.keyID()));
            }
        }

        actualIds.sort();
        if (actualIds != gpgId) {
            auto encryptJob = protocol->encryptJob();
            std::vector<GpgME::Key> keys;
            auto ctx = QGpgME::Job::context(encryptJob);
            for (const auto &keyId : qAsConst(gpgId)) {
                GpgME::Error error;
                auto key = ctx->key(keyId.toUtf8().data(), error, false);
                if (!error && !key.isNull()) {
                    keys.push_back(key);
                }
            }
            auto encryptResult = encryptJob->exec(keys, plainText, false, cipherText);

            if (!isSuccess(encryptResult.error())) {
                Q_EMIT errorString(fromGpgmeCharStar(encryptResult.error().asString()));
                continue;
            }
            QSaveFile save(fileName);
            bool open = save.open(QIODevice::WriteOnly | QIODevice::Truncate);
            if (!open) {
                Q_EMIT errorString(QStringLiteral("open file failed %1").arg(fileName));
                continue;
            }
            save.write(cipherText);
            if (save.error() != QFileDevice::NoError) {
                Q_EMIT errorString(QStringLiteral("Writing file failed: %1").arg(fileName));
                continue;
            } else {
                save.commit();
            }
        }
    }
    Q_EMIT endReencryptPath();
}

void Pass::Move(const QString& src, const QString& dest, const bool force)
{
    QFileInfo srcFileInfo(src);
    QFileInfo destFileInfo(dest);
    QString destFile;
    QString srcFileBaseName = srcFileInfo.fileName();

    if (srcFileInfo.isFile()) {
        if (destFileInfo.isFile()) {
            if (!force) {
                return;
            }
        } else if (destFileInfo.isDir()) {
            destFile = QDir(dest).filePath(srcFileBaseName);
        } else {
            destFile = dest;
        }

        if (destFile.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive))
            destFile.chop(4); // make sure suffix is lowercase
        destFile.append(QStringLiteral(".gpg"));
    } else if (srcFileInfo.isDir()) {
        if (destFileInfo.isDir()) {
            destFile = QDir(dest).filePath(srcFileBaseName);
        } else if (destFileInfo.isFile()) {
            return;
        } else {
            destFile = dest;
        }
    } else {
        return;
    }

    QDir qDir;
    if (force) {
        qDir.remove(destFile);
    }
    qDir.rename(src, destFile);
}

void Pass::Copy(const QString& src, const QString& dest, const bool force)
{
    QFileInfo destFileInfo(dest);
    QDir qDir;
    if (force) {
        qDir.remove(dest);
    }
    if (!QFile::copy(src, dest)) {
        Q_EMIT errorString(QStringLiteral("Failed to copy file"));
    }
    // reecrypt all files under the new folder
    if (destFileInfo.isDir()) {
        reencryptPath(destFileInfo.absoluteFilePath());
    } else if (destFileInfo.isFile()) {
        reencryptPath(destFileInfo.dir().path());
    }
}

quint32 Pass::boundedRandom(quint32 bound)
{
    if (bound < 2) {
        return 0;
    }

    quint32 randval;
    const quint32 max_mod_bound = (1 + ~bound) % bound;

    do {
        randval = QRandomGenerator::system()->generate();
    } while (randval < max_mod_bound);

    return randval % bound;
}

QString Pass::generateRandomPassword(const QString &charset, unsigned int length)
{
    QString out;
    for (unsigned int i = 0; i < length; ++i) {
        out.append(charset.at(static_cast<int>(boundedRandom(static_cast<quint32>(charset.length())))));
    }
    return out;
}
