#include "CheckTask.h"
#include "Utils.h"
#include "FsTab.h"
#include "Device.h"
#include <QJsonObject>
#include <QDir>
#include <QDebug>

static const QString FSTAB_PATH = "/etc/fstab";
static const QString OS_VERSION = "/etc/os-version";
static const int MAJOR_VERSION_V20 = 20;
static const int MAJOR_VERSION_V23 = 23;

CheckTask::CheckTask() : AsyncTask(), m_opType(OperateType::Invalid), m_recoveryType(RecoveryType::InvalidRecoveryType)
{
    m_isFullDiskInstall = Device::isFullDiskInstall(FSTAB_PATH, m_sysMountPointList);
    m_osMajorVer = Utils::getOSMajorVersion(OS_VERSION);
}

CheckTask::CheckTask(OperateType opType, RecoveryType recoveryType) : AsyncTask(),
    m_opType(opType), m_recoveryType(recoveryType)
{
    m_isFullDiskInstall = Device::isFullDiskInstall(FSTAB_PATH, m_sysMountPointList);
    m_osMajorVer = Utils::getOSMajorVersion(OS_VERSION);
}

CheckTask::~CheckTask()
{}

bool CheckTask::buildArgumentsForBackup()
{
    return true;
}

bool CheckTask::buildArgumentsForRestore(const QString &)
{
    return true;
}

void CheckTask::doResult()
{
    switch (m_opType) {
        case OperateType::CheckFullSystemBackupSpace: {
            this->checkFullSystemBackupSpace();
            break;
        }
        case OperateType::CheckIncSystemBackupSpace: {
            this->checkIncSystemBackupSpace();
            break;
        }
        case OperateType::CheckGhostBackupSpace: {
            this->checkGhostBackupSpace();
            break;
        }
        case OperateType::CheckDimFileUseSpace: {
            this->checkDimFileUseSpace();
            break;
        }
        default:
            break;
    }

    m_rootUUID = "";
    m_destUUID = "";
    m_destPath = "";
}

void CheckTask::setRootUUID(const QString &rootUUID)
{
    m_rootUUID = rootUUID;
}

void CheckTask::setDestUUID(const QString &destUUID)
{
    m_destUUID = destUUID;
}

void CheckTask::setDestPath(const QString &destPath)
{
    m_destPath = destPath;
}

void CheckTask::checkFullSystemBackupSpace()
{
    if (m_recoveryType != RecoveryType::Rsync) {
        return;
    }

    if (m_rootUUID.isEmpty() || m_destUUID.isEmpty()) {
        this->reportSpace(InvalidDirectory, 0, 0, "");
        return;
    }

    quint64 expectedBackupSizeBytes = 0;
    int liveFlag = 0;
    QString errMsg;

    QStringList noExcludeDirs;
    quint64 optSizeBytes = 0;
    if (!this->getSystemBackupDirSizeBytes(m_rootUUID, m_destUUID, "/opt", noExcludeDirs, optSizeBytes, errMsg)) {
        qCritical()<<"checkFullSystemBackupSpace: calculateDirSize /opt failed ";
        this->reportSpace(UnKnow, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }

    QStringList varExcludeDirs = {"--exclude=/var/run/*", "--exclude=/var/cache/*"};
    quint64 varSizeBytes = 0;
    if (!this->getSystemBackupDirSizeBytes(m_rootUUID, m_destUUID, "/var", varExcludeDirs, varSizeBytes, errMsg)) {
        qCritical()<<"checkFullSystemBackupSpace: calculateDirSize /var failed";
        this->reportSpace(UnKnow, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }

    quint64 lingLongDirSizeBytes = 0;
    if (m_isFullDiskInstall) {
        QString lingLongLocation = "";
        if (MAJOR_VERSION_V20 == m_osMajorVer) {
            lingLongLocation = "/data";
        } else if (MAJOR_VERSION_V23 == m_osMajorVer) {
            lingLongLocation = "/persistent";
        }

        if (!lingLongLocation.isEmpty() && m_sysMountPointList.contains(lingLongLocation)) {
            QString lingLongDir = lingLongLocation + "/linglong";
            QDir dir(lingLongDir);
            if (dir.exists()) {
                if (!this->getSystemBackupDirSizeBytes(m_rootUUID, m_destUUID, lingLongDir, noExcludeDirs, lingLongDirSizeBytes, errMsg)) {
                    qCritical()<<"checkFullSystemBackupSpace: calculateDirSize "<<lingLongDir<<" failed ";
                    this->reportSpace(UnKnow, expectedBackupSizeBytes, liveFlag, errMsg);
                    return;
                }
            }
        }
    }
    qWarning()<<"m_osMajorVer="<<m_osMajorVer<<", lingLongDirSizeBytes="<<lingLongDirSizeBytes;

    const quint64 reserveSizeBytes = static_cast<quint64> (1 * GiB);
    DevicePtr pRootDevice(new Device(m_rootUUID));
    pRootDevice->calculateDiskSpace();
    DeviceInfoPtr pRootDevInfo = pRootDevice->getDeviceInfo();
    if (nullptr == pRootDevInfo) {
        this->reportSpace(NullPointer, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }
    quint64 backupSizeBytes = optSizeBytes + varSizeBytes + reserveSizeBytes + pRootDevInfo->usedBytes + lingLongDirSizeBytes;
 //   qDebug()<<"backup="<<backupSizeBytes<<", opt="<<optSizeBytes<<", var="<<varSizeBytes;

    if (m_rootUUID == m_destUUID) {
        if (pRootDevInfo->availableBytes < backupSizeBytes) {
            qCritical()<<"root Insufficient disk space, available = "<<pRootDevInfo->availableBytes;
            this->reportSpace(InsufficientDiskSpace, backupSizeBytes, liveFlag, errMsg);
            return;
        }

        this->reportSpace(OK, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }

    DevicePtr pBackupDevice(new Device(m_destUUID));
    pBackupDevice->calculateDiskSpace();
    DeviceInfoPtr pBackupDevInfo = pBackupDevice->getDeviceInfo();
    if (nullptr == pBackupDevInfo) {
        this->reportSpace(NullPointer, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }
    if (pBackupDevInfo->availableBytes < backupSizeBytes) {
        qCritical()<<"backup Insufficient disk space, available = "<<pBackupDevInfo->availableBytes
                   <<", backupSizeBytes = "<<backupSizeBytes;
        this->reportSpace(InsufficientDiskSpace, backupSizeBytes, liveFlag, errMsg);
        return;
    }

    this->reportSpace(OK, expectedBackupSizeBytes, liveFlag, errMsg);
}

void CheckTask::checkIncSystemBackupSpace()
{
    if (m_recoveryType != RecoveryType::Rsync &&
        m_recoveryType != RecoveryType::OSTree &&
        m_recoveryType != RecoveryType::ImmutableMode) {
        return;
    }

    if (m_destUUID.isEmpty()) {
        this->reportSpace(InvalidDirectory, 0, 0, "destUUID is empty");
        return;
    }

    quint64 expectedBackupSizeBytes = 0;
    int liveFlag = 0;
    QString errMsg;

    DevicePtr pBackupDevice(new Device(m_destUUID));
    pBackupDevice->calculateDiskSpace();
    DeviceInfoPtr pBackupDevInfo = pBackupDevice->getDeviceInfo();
    if (nullptr == pBackupDevInfo) {
        this->reportSpace(NullPointer, expectedBackupSizeBytes, liveFlag, errMsg);
        return;
    }

    // 系统增量备份至少需要1Gib剩余可用空间
    const quint64 incrementBackupSizeBytes = static_cast<quint64> (1 * GiB);
    if (pBackupDevInfo->availableBytes < incrementBackupSizeBytes) {
        qCritical()<<"increment backup Insufficient disk space, available = "<<pBackupDevInfo->availableBytes;
        this->reportSpace(InsufficientDiskSpace, incrementBackupSizeBytes, liveFlag, errMsg);
        return;
    }

    this->reportSpace(OK, expectedBackupSizeBytes, liveFlag, errMsg);
}

void CheckTask::checkGhostBackupSpace()
{
    if (m_destUUID.isEmpty() || m_destPath.isEmpty()) {
        this->reportSpace(InvalidDirectory, 0, 0, "destUUID or m_destPath is empty");
        return;
    }

    QStringList usedPartsUuids = {};
    QStringList usedPartMountPoints = {};
    QStringList destPaths = {m_destPath};
    QStringList bindPaths = {};

    // 根据fstab找出当前系统使用的所有分区
    FSTabInfoList fstabInfos = FSTab::getFSTabFromFile("/etc/fstab");
    for (FSTabInfoPtr fstabInfoItem : fstabInfos) {
        QString fstabDeviceItem = fstabInfoItem->device;
        QString fstabMountPointItem = fstabInfoItem->mountPoint;
        QString fstabOptionItem = fstabInfoItem->options;

        if (!fstabDeviceItem.isEmpty()) {
            if (fstabDeviceItem.contains("=")) {
                fstabDeviceItem = fstabDeviceItem.right(fstabDeviceItem.length() - fstabDeviceItem.indexOf("=") - 1);
                usedPartsUuids.append(fstabDeviceItem);
                usedPartMountPoints.append(fstabMountPointItem);
                if (!fstabDeviceItem.compare(m_destUUID)) {
                    QString destMountPath = fstabMountPointItem + m_destPath;
                    destMountPath.replace("//", "/");
                    if (!destPaths.contains(destMountPath)) {
                        destPaths.append(destMountPath);
                    }
                }
            }
            if (fstabOptionItem.contains("bind")) {
                bindPaths.append(fstabMountPointItem);
            }
        }
    }

    // 获取已使用的空间
    QMap<QString, quint64> dirSizeMap;
    qint64 usedDevKib = getUsedSpace(bindPaths, dirSizeMap) + (GiB / KiB);

    // 获取备份目标目录的可用空间大小
    quint64 backupDevAvailableBytes = 0;
    Device usedDev;
    DeviceInfoList usedDevices = usedDev.getDeviceByLsblk();
    for (const DeviceInfoPtr &devItem : usedDevices) {
        if (devItem->uuid != m_destUUID) {
            continue;
        }

        if ((devItem->type == "dm" || devItem->type == "part") && devItem->fsSizeBytes > 0) {
            backupDevAvailableBytes = devItem->availableBytes;
            break;
        }
    }

    // 在挂载点列表中寻找压缩比几乎不会变的文件
    quint64 incompressFileSize = getTotalFileSize(usedPartMountPoints);
    quint64 systemBackupSize = Utils::getAllSystemBackupSize();
    qInfo()<<"usedDevKib: "<<usedDevKib<<", systemBackupSize: "<<systemBackupSize<<", incompressFileSize: "<<incompressFileSize;

    // 预估ghost镜像文件大小，加上其他分区已使用空间大小，与目标分区未使用空间大小做比较
    QString errMsg;
    qint64 ghostFileSize = usedDevKib * 0.37;
    if (Utils::isImmutableSystem()) {
        qint64 sysrootSize = dirSizeMap["sysroot"];
        qint64 persistentSize = dirSizeMap["persistent"];
        qint64 varSize = dirSizeMap["var"];
        qInfo()<<"sysrootSize: "<<sysrootSize<<", persistentSize: "<<persistentSize<<", varSize: "<<varSize;
        ghostFileSize = (usedDevKib - sysrootSize - persistentSize - varSize) * 0.2 +
                sysrootSize * 0.3 + persistentSize * 0.27 + varSize * 0.35;
    }
    quint64 totalUsedSize = usedDevKib + incompressFileSize + ghostFileSize;
    QString sizeLog = QString("totalUsedKib: %1 (%2 Gib), usedDevKib: %3 (%4 Gib), ghostFileSize: %5 (%6 Gib)")
            .arg(totalUsedSize).arg(totalUsedSize / MiB)
            .arg(usedDevKib).arg(usedDevKib / MiB)
            .arg(ghostFileSize).arg(ghostFileSize / MiB);
    qInfo()<<sizeLog;

    // 目标大小比较
    totalUsedSize = totalUsedSize * KiB - systemBackupSize;
    qint64 diffSize = totalUsedSize - backupDevAvailableBytes;
    qWarning() << "availableBytes is : " << backupDevAvailableBytes<<", sysBackupSize: "<<systemBackupSize<<", diff: "<<diffSize;
    if (totalUsedSize < backupDevAvailableBytes) {
        this->reportSpace(OK, totalUsedSize, 0, errMsg);
    } else {
        this->reportSpace(InsufficientDiskSpace, totalUsedSize, 0, errMsg);
    }
}

void CheckTask::checkDimFileUseSpace()
{
    QJsonObject jsonObj;
    jsonObj.insert("errCode", OK);
    jsonObj.insert("operateType", static_cast<int> (m_opType));
    jsonObj.insert("dimFilePath", m_destPath);

    // 获取总空间
    quint64 dimFilesTotalSize = getDimFileTotalSize(m_destPath);

    // 判断当前目录空间是否满足要求
    QJsonObject InfoObj;
    QString err = "";
    QString imgFilePath = "";
    if (Utils::getDestPartInfoByDir(m_destPath, InfoObj, err)) {
        imgFilePath = (InfoObj.value("fsavail").toVariant().toLongLong() > dimFilesTotalSize) ? m_destPath : QString();
    }

    // 如果当前选择的目录，空间不满足要求，则尝试在系统其他地方寻找空间满足要求的分区
    quint64 thresholdBytes = 200 * 1024 * 1024L;
    Device::findSuitablePartition(dimFilesTotalSize, imgFilePath, thresholdBytes);

    if (!imgFilePath.isEmpty()) {
        // imgFilePath不为空，代表，找到了空间满足要求的位置
        imgFilePath += "/dimFileRestore";
        QDir imgFilePathDir;
        imgFilePathDir.mkpath(imgFilePath);
    }

    if (imgFilePath.contains(" ")) {
        imgFilePath = QString("\"%1\"").arg(imgFilePath);
    }
    qWarning()<<"checkDimFileUseSpace, end imgFilePath = "<<imgFilePath;

    jsonObj.insert("imgFilePath", imgFilePath);
    Q_EMIT spaceCheckFinished(jsonObj);

    return;
}

bool CheckTask::getSystemBackupDirSizeBytes(const QString &rootUUID, const QString &backupDeviceUUID,
                                            const QString &dirPath, const QStringList &excludeDir,
                                            quint64 &totalBytes, QString &errMsg)
{
    errMsg = "";
    bool samePartition = false;
    if (!Utils::calculateDirSize(dirPath, excludeDir, totalBytes, errMsg, samePartition)) {
        qCritical()<<"getBackupDirSizeBytes: calc dir: "<<dirPath<<" failed, errMsg = "<<errMsg;
        return false;
    }
//    qInfo()<<"dirPath = "<<dirPath<<", totalBytes = "<<totalBytes;

    return true;
}

void CheckTask::reportSpace(ErrorCode errCode, quint64 backupSizeBytes, int liveFlag, const QString &errMsg)
{
    QJsonObject jsonObj;
    jsonObj.insert("errCode", errCode);
    jsonObj.insert("backupSizeBytes", static_cast<qint64>(backupSizeBytes));
    jsonObj.insert("liveFlag", liveFlag);
    jsonObj.insert("errMsg", errMsg);
    jsonObj.insert("uuid", m_destUUID);
    jsonObj.insert("recoveryType", static_cast<int>(m_recoveryType));
//    sleep(1); // 验证转圈圈的效果

    Q_EMIT spaceCheckFinished(jsonObj);
}

qint64 CheckTask::getUsedSpace(const QStringList &bindPaths, QMap<QString, quint64> &dirSizeMap)
{
    dirSizeMap.clear();
    qint64 usedDevkBytes = 0;
    QStringList spaceNeedToFilter = {"/media","/cdrom","/dev","/proc","/run","/mnt","/sys","/tmp"};
    QStringList noExcludeDirs;
    QString errMsg;

    bool specialVar = false;
    QDir mediaDir("/media");
    QString realMediaDir = mediaDir.canonicalPath();
    if (realMediaDir.startsWith("/var/media")) {
        spaceNeedToFilter << "/var/media" << "/var/mnt";
        specialVar = true;
    }
    // 使用du命令从"/"获取将要备份的文件的总大小
    QDir dir("/");
    dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
    dir.setSorting(QDir::Name);
    //QStringList allFolder = dir.entryList();
    QFileInfoList allFolder = dir.entryInfoList();
    for (QFileInfo dirItem : allFolder) {
        QString dirName = dirItem.fileName();
        if (spaceNeedToFilter.contains("/" + dirName)) {
            continue;
        }
        quint64 dirSizeBytes = 0;
        QStringList varExcludeDirs = {"/var/media/*", "/var/mnt/*", "/var/lib/systemd/coredump/*", "/var/log/journal/*"};
        if (specialVar && ("var" == dirName)) {
            Utils::calculateDirSize("/" + dirName, varExcludeDirs, dirSizeBytes, errMsg);
        } else {
            Utils::calculateDirSize("/" + dirName, noExcludeDirs, dirSizeBytes, errMsg);
        }
        quint64 dirSizeKb = dirSizeBytes / 1024;
        if (dirSizeKb > 1024 * 1024) {
            dirSizeMap[dirName] = dirSizeKb;
        }
        usedDevkBytes += dirSizeKb;
        qInfo()<<"usedDevkBytes: "<<usedDevkBytes<<", dirSizeBytes: "<<dirSizeBytes;
    }

    // 减掉多算的备份目标目录的大小
    for (QString bindPathItem : bindPaths) {
        quint64 bindDirSizeBytes = 0;
        Utils::calculateDirSize("/" + bindPathItem, noExcludeDirs, bindDirSizeBytes, errMsg);
        usedDevkBytes -= bindDirSizeBytes / 1024;
    }

    return usedDevkBytes;
}

quint64 CheckTask::getTotalFileSize(const QStringList &list)
{
    QStringList allKeepFileList = {};
    for (QString usedMountPoint : list) {
        if (!usedMountPoint.compare("none") || !usedMountPoint.compare("/")) {
            continue;
        }
        QString out;
        QString err;
        QStringList args;
        args <<usedMountPoint<<"-name"<<"*.iso"<<"-o"<<"-name"<<"*.zip"<<"-o"<<"-name"<<"*.uimg";
        if (Process::spawnCmd("find", args, out, err)) {
            if (out.isEmpty()) {
                continue;
            }
            out.replace("0B", "0");
            out.replace("none", "0");
            out.replace("null", "0");
#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
            allKeepFileList.append(outList);
        }
    }

    // 处理列表数据，20条为一组，方便后面使用du命令获取文件大小信息
    QStringList splitKeepFileList;
    QStringList splitKeepFileListItem = {};
    for (QString keepFileItem : allKeepFileList) {
        splitKeepFileListItem.append(keepFileItem);
        if (splitKeepFileListItem.size() == 10) {
            splitKeepFileList.append(splitKeepFileListItem.join(" "));
            splitKeepFileListItem.clear();
        }
    }
    if (splitKeepFileListItem.size() > 0) {
        splitKeepFileList.append(splitKeepFileListItem.join(" "));
        splitKeepFileListItem.clear();
    }

    // 计算文件列表中的所有文件的大小总和
    QStringList allFileSizeList = {};
    for (QString splitKeepFileItem : splitKeepFileList) {
        QString out;
        QString err;
        QString cmd = QString("du %1").arg(splitKeepFileItem);
        if (Process::spawnCmd(cmd, out, err)) {
            if (out.isEmpty()) {
                continue;
            }
            out.replace("0B", "0");
            out.replace("none", "0");
            out.replace("null", "0");

#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
            for (QString &line : outList) {
                QStringList col = line.split("\t");
                if (2 != col.size()) {
                    continue;
                }
                allFileSizeList.append(col[0].trimmed());
            }
        }
    }

    quint64 totalFileSize = 0;
    for (QString fileSize : allFileSizeList) {
        totalFileSize = totalFileSize + fileSize.toULongLong();
    }

    return totalFileSize;
}

quint64 CheckTask::getDimFileTotalSize(const QString &dimFilePath)
{
    QDir srcDir(dimFilePath);
    QStringList fileNames = {"*.dim"};
    QFileInfoList dimFileInfos = srcDir.entryInfoList(fileNames, QDir::Files | QDir::Readable, QDir::Name);
    quint64 dimFilesTotalSize = 0;
    for (QFileInfo item : dimFileInfos) {
        QString err = "";
        QJsonObject dimFileJsonInfo;
        if (!Utils::getDimFileJsonInfo(item.absoluteFilePath(), dimFileJsonInfo, err)) {
            continue;
        }
        dimFilesTotalSize += dimFileJsonInfo.value("totalReadableDataSize").toVariant().toULongLong();
    }

    return dimFilesTotalSize;
}
