// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: The Monero Project

#include "Updater.h"

#include <common/util.h>
#undef config
#include <openpgp/hash.h>

#include "utils/config.h"
#include "Utils.h"
#include "utils/AsyncTask.h"
#include "utils/Networking.h"
#include "utils/SemanticVersion.h"

Updater::Updater(QObject *parent) :
    QObject(parent)
{
    std::string featherWallet = Utils::fileOpen(":/assets/gpg_keys/featherwallet.asc").toStdString();
    m_maintainers.emplace_back(featherWallet);

    qDebug() << "Platform tag: " << this->getPlatformTag();
}

void Updater::checkForUpdates() {
    Networking network{this};
    QNetworkReply *reply = network.getJson(this, QString("%1/updates.json").arg(this->getWebsiteUrl()));
    if (!reply) {
        emit updateCheckFailed("offline mode enabled");
        return;
    }

    connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onUpdateCheckResponse, this, reply));
}

void Updater::onUpdateCheckResponse(QNetworkReply *reply) {
    const QString err = reply->errorString();

    QByteArray data = reply->readAll();
    reply->deleteLater();

    QJsonObject updates;
    if (!data.isEmpty() && Utils::validateJSON(data)) {
        auto doc = QJsonDocument::fromJson(data);
        updates = doc.object();
    }
    else {
        qWarning() << err;
        emit updateCheckFailed(err);
        return;
    }

    this->wsUpdatesReceived(updates);
}

void Updater::wsUpdatesReceived(const QJsonObject &updates) {
    QString featherVersionStr{FEATHER_VERSION};

    auto featherVersion = SemanticVersion::fromString(featherVersionStr);

    QString platformTag = getPlatformTag();
    if (platformTag.isEmpty()) {
        QString err{"Unsupported platform, unable to fetch update"};
        emit updateCheckFailed(err);
        qWarning() << err;
        return;
    }

    QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject();
    if (platformData.isEmpty()) {
        QString err{"Unable to find current platform in updates data"};
        emit updateCheckFailed(err);
        qWarning() << err;
        return;
    }

    QString newVersion = platformData["version"].toString();
    if (SemanticVersion::fromString(newVersion) <= featherVersion) {
        emit noUpdateAvailable();
        return;
    }

    // Hooray! New update available

    QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(this->getWebsiteUrl(), newVersion);
    qDebug() << hashesUrl;

    Networking network{this};
    QNetworkReply *reply = network.get(this, hashesUrl);

    connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onSignedHashesReceived, this, reply, platformTag, newVersion));
}

void Updater::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) {
    if (reply->error() != QNetworkReply::NoError) {
        QString err{QString("Unable to fetch signed hashed: %1").arg(reply->errorString())};
        emit updateCheckFailed(err);
        qWarning() << err;
        return;
    }

    QByteArray armoredSignedHashes = reply->readAll();
    reply->deleteLater();

    const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag);
    QByteArray signedHash{};
    QString signer;
    try {
         signedHash = this->verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer);
    }
    catch (const std::exception &e) {
        QString err{QString("Failed to fetch and verify signed hash: %1").arg(e.what())};
        emit updateCheckFailed(err);
        qWarning() << err;
        return;
    }

    QString hash = signedHash.toHex();
    qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer;

    this->state = Updater::State::UPDATE_AVAILABLE;
    this->version = version;
    this->binaryFilename = binaryFilename;
    this->downloadUrl = QString("%1/files/releases/%2/%3").arg(this->getWebsiteUrl(), platformTag, binaryFilename);
    this->hash = hash;
    this->signer = signer;
    this->platformTag = platformTag;

    emit updateAvailable();
}

QString Updater::getPlatformTag() {
#ifdef Q_OS_MACOS
    if (QString(FEATHER_TARGET_TRIPLET) == "arm64-apple-darwin") {
        return "mac-arm64";
    } else {
        return "mac";
    }
#endif
#ifdef Q_OS_WIN
    #ifdef PLATFORM_INSTALLER
    return "win-installer";
#endif
    return "win";
#endif
#ifdef Q_OS_LINUX
    QString tag = "";

    QString arch = QSysInfo::buildCpuArchitecture();
    if (arch == "arm64") {
        tag += "linux-arm64";
    } else if (arch == "arm") {
        tag += "linux-arm";
    } else if (arch == "riscv64") {
        tag += "linux-riscv64";
    } else {
        tag += "linux";
    }

    if (!qEnvironmentVariableIsEmpty("APPIMAGE")) {
        tag += "-appimage";
    }

#if !defined(HAS_TOR_BIN)
    tag += "-a";
#endif

    return tag;
#endif
    return "";
}

QString Updater::getWebsiteUrl() {
        if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) {
            return "http://featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion";
        }
        else if (conf()->get(Config::proxy).toInt() == Config::Proxy::i2p) {
            return "http://rwzulgcql2y3n6os2jhmhg6un2m33rylazfnzhf56likav47aylq.b32.i2p";
        }
        else {
            return "https://featherwallet.org";
        }
}

QByteArray Updater::verifyParseSignedHashes(
        const QByteArray &armoredSignedHashes,
        const QString &binaryFilename,
        QString &signers) const
{
    const QString signedMessage = verifySignature(armoredSignedHashes, signers);

    return parseShasumOutput(signedMessage, binaryFilename);
}

QByteArray Updater::getHash(const void *data, size_t size) const
{
    QByteArray hash(sizeof(crypto::hash), 0);
    tools::sha256sum(static_cast<const uint8_t *>(data), size, *reinterpret_cast<crypto::hash *>(hash.data()));
    return hash;
}

QByteArray Updater::parseShasumOutput(const QString &message, const QString &filename) const
{
    for (const auto &line : message.split("\n"))
    {
        const auto trimmed = line.trimmed();
        if (trimmed.endsWith(filename))
        {
            const int pos = trimmed.indexOf(' ');
            if (pos != -1)
            {
                return QByteArray::fromHex(trimmed.left(pos).toUtf8());
            }
        }
        else if (trimmed.startsWith(filename))
        {
            const int pos = trimmed.lastIndexOf(' ');
            if (pos != -1)
            {
                return QByteArray::fromHex(trimmed.right(trimmed.size() - pos).toUtf8());
            }
        }
    }

    throw std::runtime_error("hash not found");
}

QString Updater::verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const
{
    const std::string messageString = armoredSignedMessage.toStdString();

    const openpgp::message_armored signedMessage(messageString);
    signer = verifySignature(signedMessage, openpgp::signature_rsa::from_armored(messageString));

    const epee::span<const uint8_t> message = signedMessage;
    return QString(QByteArray(reinterpret_cast<const char *>(&message[0]), message.size()));
}

QString Updater::verifySignature(const epee::span<const uint8_t> data, const openpgp::signature_rsa &signature) const
{
    for (const auto &maintainer : m_maintainers)
    {
        for (const auto &public_key : maintainer)
        {
            try {
                if (signature.verify(data, public_key))
                {
                    return QString::fromStdString(maintainer.user_id());
                }
            }
            catch (const std::exception &e) {
                qWarning() << e.what();
            }
        }
    }

    throw std::runtime_error("not signed by a maintainer");
}
