/*
    SPDX-FileCopyrightText: 2025-2026 Mike Noe <noeerover@gmail.com>

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

#include "MarkerLevelChecker.h"
#include "pmkded_log.h"

#include <KLocalizedString>
#include <KNotification>

#include <KCupsRequest.h>
#include <ProcessRunner.h>

using namespace Qt::StringLiterals;

MarkerLevelChecker::MarkerLevelChecker(QObject *parent)
    : QObject(parent)
{
    connect(KCupsConnection::global(), &KCupsConnection::serverStarted, this, [](const QString &msg) {
        qCDebug(PMKDED) << "CUPS Started:" << msg;
    });

    connect(KCupsConnection::global(), &KCupsConnection::serverStopped, this, [](const QString &msg) {
        qCDebug(PMKDED) << "CUPS Stopped:" << msg;
    });

    connect(KCupsConnection::global(), &KCupsConnection::serverRestarted, this, [](const QString &msg) {
        qCDebug(PMKDED) << "CUPS Restarted:" << msg;
    });

    connect(KCupsConnection::global(), &KCupsConnection::jobCreated, this, &MarkerLevelChecker::jobHandler);
}

void MarkerLevelChecker::checkMarkerLevels(const QString &printerName)
{
    static const QStringList s_attrs({KCUPS_MARKER_NAMES,
                                      KCUPS_MARKER_LEVELS,
                                      KCUPS_MARKER_HIGH_LEVELS,
                                      KCUPS_MARKER_LOW_LEVELS,
                                      KCUPS_MARKER_TYPES,
                                      KCUPS_PRINTER_INFO,
                                      KCUPS_PRINTER_TYPE});

    /**
     *  Marker low level indicates the near empty warning value, but it can be 0.
     *  Let's be more conservative and warn if actual level is at a higher pct
     *  https://openprinting.github.io/cups/doc/spec-ipp.html#marker-low-levels
     */
    static const int s_threshold = 3;

    qCDebug(PMKDED) << "checkMarkerLevels: getting printer attributes" << printerName;

    const auto request = new KCupsRequest;
    connect(request, &KCupsRequest::finished, this, [this](KCupsRequest *req) {
        // let's make sure to not leave dangling requests
        auto cleanupReq = qScopeGuard([req] {
            req->deleteLater();
        });

        // it's possible attributes from a temporary queue could be empty
        if (req->printers().isEmpty()) {
            qCDebug(PMKDED) << "No printers found for marker level check";
            return;
        }

        const auto printer = req->printers().at(0);

        // QVariant::toList converts an int to a null list
        // So, create a QList<int> if only one entry (int)
        const auto getLevels = [printer](const QString &key) -> QList<QVariant> {
            QList<QVariant> list;
            const auto levels = printer.argument(key);
            if (levels.isValid()) {
                if (levels.canConvert<QList<int>>()) {
                    list = levels.toList();
                    qCDebug(PMKDED) << "Valid levels list" << key << list;
                } else {
                    list = QList<QVariant>{levels};
                    qCDebug(PMKDED) << "Valid level value set to a list" << key << list;
                }
            } else {
                // Valid levels entry not found
                qCDebug(PMKDED) << "Invalid levels entry:" << key;
            }

            return list;
        };

        const auto currentLevels = getLevels(KCUPS_MARKER_LEVELS);
        const auto lowLevels = getLevels(KCUPS_MARKER_LOW_LEVELS);
        const auto highLevels = getLevels(KCUPS_MARKER_HIGH_LEVELS);
        const auto typesList = printer.argument(KCUPS_MARKER_TYPES).toStringList();
        const auto namesList = printer.argument(KCUPS_MARKER_NAMES).toStringList();

        // All lists should have values for a valid marker check
        if (currentLevels.isEmpty() || highLevels.isEmpty() || lowLevels.isEmpty() || typesList.isEmpty() || namesList.isEmpty()) {
            qCWarning(PMKDED) << "At least one marker level attribute is empty, aborting level check" << currentLevels << lowLevels << highLevels << typesList
                              << namesList;
            return;
        }

        // We address all lists by index so they had better be of uniform length.
        const auto levelCounts = {currentLevels.count(), lowLevels.count(), highLevels.count(), typesList.count(), namesList.count()};
        const auto levelsMaxCount = std::max(levelCounts);
        if (std::ranges::any_of(levelCounts, [levelsMaxCount](auto count) {
                return count != levelsMaxCount;
            })) {
            QString message;
            QDebug(&message) << "Marker level lists have different counts, aborting level check" << currentLevels << lowLevels << highLevels << typesList
                             << namesList;
            qCWarning(PMKDED) << message;
            // This is a fairly serious problem we should inspect. Fail an assertion on it!
            // Can be safely demoted when we have some more confidence that we understand why the check may fail.
            Q_ASSERT_X(false, Q_FUNC_INFO, message.toUtf8().constData());
            return;
        }

        /** CUPS supports devices with both types of markers, consumables and receptacles
         *
         *  https://openprinting.github.io/cups/doc/spec-ipp.html#marker-types
         *
         *  Consumables are generally toners/cartridges
         *  levels decrease from 100 => 0 (high to low)
         *
         *  Receptacles are generally waste containters
         *  levels increase from 0 => 100 (low to high)
         */
        QStringList msgs;
        constexpr int empty = 0;
        constexpr int full = 100;
        // For each marker
        for (uint i = 0; i < namesList.count(); ++i) {
            int levelIndex = -1;
            int checkerValue = 0;

            const auto level = currentLevels.at(i).toInt();
            const auto low = lowLevels.at(i).toInt();
            const auto high = highLevels.at(i).toInt();
            // per CUPS
            // low-level == 0 && high-level != 0 && high-level < 100 => waste receptacle
            // high-level == 100 && low-level != 100 && low-level >= 0 => consumable
            bool consumable = false;
            if (high == full && low != full && low >= empty) {
                consumable = true;
                qCDebug(PMKDED) << "Found consumable, checking" << typesList.at(i);
            } else if (low == empty && high != empty && high < full) {
                consumable = false;
                qCDebug(PMKDED) << "Found receptacle, checking" << typesList.at(i);
            } else {
                qCDebug(PMKDED) << "low/high range invalid (" << low << "," << high << "), failure to determine marker type:" << typesList.at(i);
                continue;
            }

            if (consumable) {
                // Because printers, level can be 0 and low boundary can be > zero
                // Also, level can be < low and not zero, ie. low=2, level=1
                // level < 0 is unknown or unavailable, so ignore
                if (level == empty || (level <= low && level > empty)) {
                    levelIndex = i;
                    if (level <= low) {
                        checkerValue = level;
                    }
                } else {
                    // CUPS seems to handle this, but just in case don't divide by zero
                    if (high == empty) {
                        qCWarning(PMKDED) << "Found a high boundary == 0";
                        break;
                    }
                    if (level > low && level <= high) {
                        auto result = div((level - low) * 100, high);
                        if (result.quot <= s_threshold) {
                            levelIndex = i;
                            checkerValue = level;
                        }
                    }
                }
                // Receptacle
            } else {
                if (level >= high) {
                    levelIndex = i;
                    checkerValue = level;
                }
            }

            // found a marker level at or below the threshold pct
            if (levelIndex >= 0) {
                const auto name = namesList.at(levelIndex);
                const auto type = typesList.at(levelIndex);

                if (consumable) {
                    if (checkerValue == 0) {
                        msgs << i18nc("@info:usagetip The name and type of the ink cartridge", "%1 (%2) appears to be empty.", name, type);
                    } else {
                        msgs << i18nc("@info:usagetip The name and type of the ink cartridge and percent ink remaining",
                                      "%1 (%2) appears to be low (%3% remaining).",
                                      name,
                                      type,
                                      checkerValue);
                    }
                } else {
                    msgs << i18nc("@info:usagetip The name, type, and percentage full of the waste receptacle",
                                  "%1 (%2) is almost full (%3%)",
                                  name,
                                  type,
                                  checkerValue);
                }
                qCDebug(PMKDED) << "Found marker-level at threshold" << type << name << checkerValue;
            }
        }

        if (!msgs.isEmpty()) {
            auto notify = new KNotification(u"MarkerLevel"_s, KNotification::Persistent);
            notify->setComponentName(u"printmanager"_s);
            notify->setTitle(printer.info());
            notify->setText(msgs.join(u"\n"_s));

            auto checkMarkers = notify->addDefaultAction(i18nc("@action:button check printer ink levels", "Check Levels…"));
            connect(checkMarkers, &KNotificationAction::activated, this, [pn = printer.name()]() {
                ProcessRunner::kcmConfigurePrinter(pn);
            });

            notify->sendEvent();
        }
    });

    request->getPrinterAttributes(printerName, false, s_attrs);
}

void MarkerLevelChecker::jobHandler(const QString &text,
                                    const QString &printerUri,
                                    const QString &printerName,
                                    uint printerState,
                                    const QString &printerStateReasons,
                                    bool printerIsAcceptingJobs,
                                    uint jobId,
                                    uint jobState,
                                    const QString &jobStateReasons,
                                    const QString &jobName,
                                    uint jobImpressionsCompleted)
{
    Q_UNUSED(text)
    Q_UNUSED(printerUri)
    Q_UNUSED(printerState)
    Q_UNUSED(printerStateReasons)
    Q_UNUSED(printerIsAcceptingJobs)
    Q_UNUSED(jobId)
    Q_UNUSED(jobState)
    Q_UNUSED(jobStateReasons)
    Q_UNUSED(jobName)
    Q_UNUSED(jobImpressionsCompleted)

    checkMarkerLevels(printerName);
}

#include "moc_MarkerLevelChecker.cpp"
