//
// Created by zxm on 2022/8/22.
//
#include "BootVersionMgr.h"
#include <QDebug>
#include <QSettings>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <QProcess>
#include <QStorageInfo>

static const QString BACKUP_PATH = "/backup/system/snapshots/";

static const QString UOS_RECOVERY_INI = "etc/uos-recovery/uos-recovery.ini";
static const QString BACKUP_GROUP = "backup";
static const QString BACKUP_DEVICE_UUID_KEY = "device_uuid";
static const QString BACKUP_HISTORY_DEVICE_UUID_KEY = "history_device_uuid";

QJsonObject BootVersion::marshal()
{
    QJsonObject jsonObject;
    jsonObject.insert("version", version);
    jsonObject.insert("kernel", kernel);
    jsonObject.insert("initrd", initrd);
    jsonObject.insert("scheme", scheme);
    jsonObject.insert("display", display);
    jsonObject.insert("uuid", uuid);

    return jsonObject;
}

void BootVersion::unmarshal(const QJsonObject &jsonObject)
{
    version = jsonObject.value("version").toString();
    kernel = jsonObject.value("kernel").toString();
    initrd = jsonObject.value("label").toString();
    scheme = jsonObject.value("scheme").toString();
    display = jsonObject.value("display").toString();
    uuid = jsonObject.value("uuid").toString();
}

BootVersionMgr::BootVersionMgr()
{}

BootVersionMgr::~BootVersionMgr()
{}

QString BootVersionMgr::getBootList()
{
    QString bootListJson;

    QStringList backupUUIDList;
    getRecoveryBackupUUIDList(backupUUIDList);

    QList<BootVersion> versionList;
    getBootVersionList(backupUUIDList, versionList);

    bootListJson = versionList2Json(versionList);
    return bootListJson;
}

void BootVersionMgr::getRecoveryBackupUUIDList(QStringList &backupUUIDList)
{
    backupUUIDList.clear();
    QString rootMountPoint = "/root/";  // in initrd
    if (this->isRootMounted()) {
        rootMountPoint = "/";  // in real system
    }

    QString recoveryConf = rootMountPoint + UOS_RECOVERY_INI;
    QSettings settings(recoveryConf, QSettings::IniFormat);
    settings.beginGroup(BACKUP_GROUP);
    QString deviceUUID = settings.value(BACKUP_DEVICE_UUID_KEY).toString();
    QString historyDeviceUUID = settings.value(BACKUP_HISTORY_DEVICE_UUID_KEY).toString();
    settings.endGroup();

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
    backupUUIDList = historyDeviceUUID.split(",", Qt::SkipEmptyParts);
#else
    backupUUIDList = historyDeviceUUID.split(",", QString::SkipEmptyParts);
#endif
    backupUUIDList.append(deviceUUID);
    backupUUIDList.removeDuplicates();
}

void BootVersionMgr::getBootVersionList(const QStringList &backupUUIDList, QList<BootVersion> &versionList)
{
    versionList.clear();
    if (backupUUIDList.isEmpty()) {
        return;
    }

    const int backupFileSuccess = 1;
    BootVersion bootVer;
    bootVer.scheme = "recovery";
    getBootKernelAndInitrd(bootVer.kernel, bootVer.initrd);

    for(auto &backupUUID : backupUUIDList) {
        QString mountPoint;
        this->getMountPointByUUID(backupUUID, mountPoint);
        if (mountPoint.isEmpty()) {
            continue;
        }

        QDir dir(mountPoint + BACKUP_PATH);
        for (auto &file : dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::NoSymLinks)) {
            QString filename = file.absolutePath() + "/" + file.fileName() + "/backup_info.json";
            QFile backupInfoFile(filename);
            if (backupInfoFile.open(QIODevice::Text | QIODevice::ReadOnly)) {
                QJsonObject jsonObject = this->QStringToJson(backupInfoFile.readAll());
                int backupFileStatus = -1;
                if (jsonObject.contains("status")) {
                    backupFileStatus = jsonObject.value("status").toInt(-1);
                }

                if (backupFileStatus != backupFileSuccess) {
                    continue;
                }

                if (jsonObject.contains("backupVersion")) {
                    bootVer.version = jsonObject.value("backupVersion").toString();
                }

                if (jsonObject.contains("versionDisplay")) {
                    bootVer.display = jsonObject.value("versionDisplay").toString();
                }

                if (jsonObject.contains("backupDevUUID")) {
                    bootVer.uuid = jsonObject.value("backupDevUUID").toString();
                }

                versionList.push_back(bootVer);
                backupInfoFile.close();
            }
        }
    }
}

void BootVersionMgr::getBootKernelAndInitrd(QString &kernel, QString &initrd)
{
    QString fstab = "/etc/fstab";
    bool existBootPartition = this->isExistBootPartition(fstab);
    QString kernelPrefix = existBootPartition ? "/vmlinuz-" : "/boot/vmlinuz-";
    QString initrdPrefix = existBootPartition ? "/initrd.img-" : "/boot/initrd.img-";

    QStringList args;
    args <<"-r";
    QString out;
    QString err;
    if (!this->spawnCmd("uname", args, out, err)) {
        return;
    }

    QStringList kernelReleaseList = out.split("\n");
    if (!kernelReleaseList.isEmpty()) {
        kernel = kernelPrefix + kernelReleaseList.at(0);
        initrd = initrdPrefix + kernelReleaseList.at(0);
    }
}

QString BootVersionMgr::versionList2Json(const QList<BootVersion> &versionList)
{
    QString jsonList;
    QJsonArray versionJsonArray;
    for (auto bv : versionList) {
        QJsonObject bvObj = bv.marshal();
        versionJsonArray.append(bvObj);
    }

    QJsonObject versionListObj;
    versionListObj["version_list"] = versionJsonArray;

    QJsonDocument versionJsonDoc(versionListObj);
    jsonList = versionJsonDoc.toJson(QJsonDocument::Compact);
    return jsonList;
}

bool BootVersionMgr::spawnCmd(const QString &cmd, const QStringList &args, QString &output, QString &err)
{
    QProcess process;
    process.setProgram(cmd);
    process.setArguments(args);

    process.start();
    // Wait for Process to finish without timeout.
    process.waitForFinished(-1);
    output += process.readAllStandardOutput();
    err += process.readAllStandardError();

    if (process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0) {
        return true;
    }
    return false;
}

bool BootVersionMgr::getMountPointByUUID(const QString &uuid, QString &mountPointPath)
{
    bool retCode = false;
    QStringList args;
    args <<"--output"<<"UUID,PATH";
    QString out;
    QString err;
    retCode = this->spawnCmd("lsblk", args, out, err);
    if (!retCode) {
        qInfo()<<Q_FUNC_INFO<<", spawnCmd failed, args = "<<args<<", err = "<<err;
        return retCode;
    }

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
    QStringList outList = out.split("\n", Qt::SkipEmptyParts);
#else
    QStringList outList = out.split("\n", QString::SkipEmptyParts);
#endif
    QString devPath;
    for (QString line : outList) {
        if (!line.contains(uuid)) {
            continue;
        }
        line = line.trimmed();
        int index = line.indexOf("/");
        devPath = line.right(line.length() - index);
        break;
    }

    if (!devPath.isEmpty()) {
        QList<QStorageInfo> storeInfoList = QStorageInfo::mountedVolumes();
        for (auto &storeInfo : storeInfoList) {
            if (devPath == storeInfo.device()) {
                mountPointPath = storeInfo.rootPath();
                retCode = true;
                break;
            }
        }
    }

    return retCode;
}

QJsonObject BootVersionMgr::QStringToJson(const QString &jsonString)
{
    QJsonParseError jsonErr;
    QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toLocal8Bit().data(), &jsonErr);
    if (jsonErr.error != QJsonParseError::ParseError::NoError) {
        return QJsonObject();
    }

    return jsonDocument.object();
}

bool BootVersionMgr::isRootMounted()
{
    // get rootUUID if root partition has been mounted
    QStringList args;
    //args <<"-c"<< "lsblk --output MOUNTPOINT,UUID | grep \"^/ \" | awk '{print $2}'";
    args <<"--output"<< "MOUNTPOINT,UUID";
    QString out;
    QString err;
    if (!this->spawnCmd("lsblk", args, out, err)) {
        return false;
    }

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
    QStringList outList = out.split("\n", Qt::SkipEmptyParts);
#else
    QStringList outList = out.split("\n", QString::SkipEmptyParts);
#endif
    QString rootUUID;
    for (QString &line : outList) {
        line = line.trimmed();
        if (!line.startsWith("/ ")) {
            continue;
        }
        rootUUID = line.right(line.length()-1).trimmed();
        break;
    }

    if (rootUUID.isEmpty()) {
        return false;
    }

    // check rootUUID in fstab
    args.clear();
    QString rootMountPoint;
    QString fstabOut;
    if (this->spawnCmd("cat",{"/etc/fstab"}, fstabOut, err)) {
        return false;
    }

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
    QStringList fstabOutList = fstabOut.split("\n", Qt::SkipEmptyParts);
#else
    QStringList fstabOutList = fstabOut.split("\n", QString::SkipEmptyParts);
#endif
    for (QString &line : fstabOutList) {
        if (!line.contains(rootUUID)) {
            continue;
        }
        line = line.trimmed();
        if (line.startsWith("#")) {
            continue;
        }
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
        QStringList lineList = line.split("\t", Qt::SkipEmptyParts);
#else
        QStringList lineList = line.split("\t", QString::SkipEmptyParts);
#endif
        rootMountPoint = lineList[1];
        break;
    }

    rootMountPoint = rootMountPoint.trimmed();
    return ("/" == rootMountPoint);
}

bool BootVersionMgr::isExistBootPartition(const QString &fstabPath)
{
    bool exitBootPartition = false;
    QFile file(fstabPath);
    if (!file.exists()) {
        return exitBootPartition;
    }

    try {
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            return exitBootPartition;
        }

        auto lines = file.readAll().split('\n');
        for (QString line : lines) {
            line = line.trimmed();
            if (!line.startsWith("UUID=")) {
                continue;
            }

#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
            QStringList cols = line.replace("\t", " ").split(" ", Qt::SkipEmptyParts);
#else
            QStringList cols = line.replace("\t", " ").split(" ", QString::SkipEmptyParts);
#endif
            if (cols.size() != 6) {
                continue;
            }

            QString boot = cols.at(1);
            if ("/boot" == boot) {
                exitBootPartition = true;
                break;
            }
        }
        file.close();
    } catch (...) {
        file.close();
    }

    return exitBootPartition;
}
