/*
 *  SPDX-FileCopyrightText: 2021 Felipe Kinoshita <kinofhek@gmail.com>
 *  SPDX-FileCopyrightText: 2022 Nate Graham <nate@kde.org>
 *  SPDX-FileCopyrightText: 2024 Oliver Beard <olib141@outlook.com>
 *  SPDX-FileCopyrightText: 2025 Robert French <frenchrobertm@outlook.com>
 *
 *  SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
 */

#include <QApplication>
#include <QClipboard>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QStandardPaths>
#include <QTextStream>
#include <QTimer>

#include <KLocalizedString>

#include "system_update.h"

#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QtDebug>

#ifdef TESTING_BUILD
#include <algorithm>
#endif

using namespace Qt::Literals::StringLiterals;

SystemUpdateBackend::SystemUpdateBackend(QObject *parent)
    : QObject(parent)
{
    m_console = new Console::Model(this);

#ifdef TESTING_BUILD
    connect(&m_testConsoleTimer, &QTimer::timeout, this, [this]() {
        m_console->newLine(i18n("Test console line %1", ++m_testConsoleLineCounter), Console::LogLevel::Debug);
    });
#endif
}

void SystemUpdateBackend::runUpdate(QJSValue callback, QJSValue callbackErrors)
{
    if (!callback.isCallable()) {
        qDebug() << "Callback is not callable, command run refused";
        return;
    }
#ifndef TESTING_BUILD
    if (!Utils::isServicePresent(u"uupd-manual.service"_s)) {
        setBlockUpdate(true);
        m_console->newLine(i18n("The uupd-manual.service used for this application was not found on the system."), Console::LogLevel::ErrorCritical);
        callback.call({127});
        return;
    }
#endif

    appState()->setUpdateRunning(true);

    QProcess *systemctl = new QProcess(this);
#ifdef TESTING_BUILD
    Utils::startProcess(systemctl, u"sleep"_s, {u"3"_s});
#else
    Utils::startProcess(systemctl, u"systemctl"_s, {u"start"_s, u"uupd-manual.service"_s});
#endif

    // display progress of systemctl to in-GUI console
    logToConsole();

    // When the "systemctl start uupd-manual.service" process completes,
    // check the service result and update the UI accordingly.
    // NOTE: A race condition can likely occur between this and the process in logToConsole()
    connect(systemctl, &QProcess::finished, [=]() {
        QTimer::singleShot(500, this, [=]() {
            if (m_updateErrorStatus.System_Update || m_updateErrorStatus.Brew_Update || m_updateErrorStatus.System_Apps || m_updateErrorStatus.Apps_for_User
                || m_updateErrorStatus.Distroboxes_for_User || m_updateErrorStatus.Unknown_Error) {
                // Used in QML to send notification of which part(s) of the update failed

                QJsonObject errorDetails;
                errorDetails[u"System_Update"_s] = m_updateErrorStatus.System_Update;
                errorDetails[u"Brew_Update"_s] = m_updateErrorStatus.Brew_Update;
                errorDetails[u"System_Apps"_s] = m_updateErrorStatus.System_Apps;
                errorDetails[u"Apps_for_User"_s] = m_updateErrorStatus.Apps_for_User;
                errorDetails[u"Distroboxes_for_User"_s] = m_updateErrorStatus.Distroboxes_for_User;
                errorDetails[u"Unknown_Error"_s] = m_updateErrorStatus.Unknown_Error;

                QJsonDocument errorDoc(errorDetails);
                QString errorJson = QString::fromUtf8(errorDoc.toJson(QJsonDocument::Compact));

                if (callbackErrors.isCallable()) {
                    callbackErrors.call({errorJson});
                }
            }

            const QString result = getServiceResult(u"uupd-manual.service"_s);
            if (result == u"success"_s) {
                appState()->setUpdateRunning(false);
                appState()->setCommandSucceeded(true);

                callback.call({0, result});
                systemctl->deleteLater();
                return;
            } else if (result == u"start-limit-hit"_s) {
                m_console->newLine(i18n("You are updating too many times in a short period!"), Console::LogLevel::Error);
            } else {
                qDebug() << "Result of uupd-manual.service was not success: " << result;
            }
            appState()->setUpdateRunning(false);
            callback.call({1, result});

            systemctl->deleteLater();
            return;
        });
    });
}

void SystemUpdateBackend::logToConsole()
{
    // Journalctl only needs to be running once
    if (m_journalctlProcess.state() == QProcess::Running)
        return;

#ifdef TESTING_BUILD
    Utils::startProcess(&m_journalctlProcess, u"cat"_s, {u"uupd-output.txt"_s});
#else
    Utils::startProcess(&m_journalctlProcess, u"journalctl"_s, {u"--follow"_s, u"--unit=uupd-manual.service"_s, u"--lines=0"_s, u"-o"_s, u"cat"_s});
#endif

    connect(&m_journalctlProcess, &QProcess::readyReadStandardOutput, [this]() {
        while (m_journalctlProcess.canReadLine()) {
            const QByteArray message = m_journalctlProcess.readLine().trimmed();

            if (message.isEmpty())
                continue;

            if (!message.startsWith('{') && message != QByteArray::fromStdString("Updating") && message != QByteArray::fromStdString("scanned progress")) {
                m_console->newLine(QString::fromUtf8(message), Console::LogLevel::Info);
                continue;
            }

            const QJsonObject json = QJsonDocument::fromJson(message).object();

            // Ignore bootc output, as it heavily spams the logs
            if (json.contains(u"bytes"_s))
                continue;

            if (json.contains(u"level"_s) && json.contains(u"msg"_s)) {
                QString level = json.value(u"level"_s).toString();
                QString msg = json.value(u"msg"_s).toString();

                using namespace Console;
                LogLevel log_level = LogLevel::Warn;

                if (level == u"INFO"_s)
                    log_level = LogLevel::Info;
                else if (level == u"DEBUG"_s)
                    log_level = LogLevel::Debug;
                else if (level == u"WARN"_s)
                    log_level = LogLevel::Warn;
                else if (level == u"ERROR"_s) {
                    log_level = LogLevel::Error;

                    if (msg == u"module_fail"_s) {
                        QString context = json.value(u"output"_s).toObject().value(u"Context"_s).toString();
                        msg = i18n("Module Failed: ") + context;

                        if (context.contains(u"System Update"_s, Qt::CaseInsensitive)) {
                            setBlockUpdate(true);
                            log_level = Console::LogLevel::ErrorCritical;
                            m_updateErrorStatus.System_Update = true;
                        } else if (context.contains(u"Brew Update"_s, Qt::CaseInsensitive))
                            m_updateErrorStatus.Brew_Update = true;
                        else if (context.contains(u"System Apps"_s, Qt::CaseInsensitive))
                            m_updateErrorStatus.System_Apps = true;
                        else if (context.contains(u"Apps for User"_s, Qt::CaseInsensitive))
                            m_updateErrorStatus.Apps_for_User = true;
                        else if (context.contains(u"Distroboxes for User"_s, Qt::CaseInsensitive))
                            m_updateErrorStatus.Distroboxes_for_User = true;
                        else
                            m_updateErrorStatus.Unknown_Error = true;
                    }
                }

                { // Remove ANSI color codes from msg (e.g. [0m[1;31m)
                    static QRegularExpression ansiEscapePattern(QStringLiteral(R"(\x1b\[[0-9;]*m)"));
                    msg.replace(ansiEscapePattern, u""_s);
                }

                if (!msg.isEmpty())
                    m_console->newLine(msg, log_level);
            }
        }
    });
}

QString SystemUpdateBackend::getServiceState(const QString &service) const
{
    QProcess process;
    Utils::startProcess(process, u"systemctl"_s, {u"is-active"_s, service});
    process.waitForFinished();
    QString state = QString::fromUtf8(process.readAllStandardOutput()).trimmed();

    return state;
}

// Return the result of systemctl show {service} -p Result --value.
// For example, may return "start-limit-hit" or "success"
QString SystemUpdateBackend::getServiceResult(const QString &service) const
{
    QProcess check;
    Utils::startProcess(check, u"systemctl"_s, {u"show"_s, service, u"-p"_s, u"Result"_s, u"--value"_s});

    check.waitForFinished();

    const QString output = QString::fromUtf8(check.readAllStandardOutput()).trimmed();

    return output;
}

void SystemUpdateBackend::setProgressLevel(int progressLevel)
{
    m_progressLevel = progressLevel;
    Q_EMIT progressLevelChanged();
}

void SystemUpdateBackend::setBlockUpdate(bool updateError)
{
    m_blockUpdate = updateError;
    Q_EMIT blockUpdateChanged();
}

#ifdef TESTING_BUILD
void SystemUpdateBackend::setTestConsoleLinesPerSecond(int linesPerSecond)
{
    if (linesPerSecond < 0) {
        linesPerSecond = 0;
    }

    if (m_testConsoleLinesPerSecond == linesPerSecond) {
        return;
    }

    m_testConsoleLinesPerSecond = linesPerSecond;

    if (m_testConsoleLinesPerSecond == 0) {
        m_testConsoleTimer.stop();
    } else {
        const int intervalMs = std::max(1, 1000 / m_testConsoleLinesPerSecond);
        m_testConsoleTimer.start(intervalMs);
    }

    Q_EMIT testConsoleLinesPerSecondChanged();
}
#endif

#include "moc_system_update.cpp"
