#include "RsyncSystem.h"
#include "utils/Device.h"
#include "utils/Process.h"
#include "utils/RsyncTask.h"
#include "utils/SquashfsTask.h"
#include "utils/CheckTask.h"
#include "utils/global.h"
#include "utils/Utils.h"
#include <QDebug>
#include <QDir>
#include <QDateTime>
#include <QJsonObject>
#include <QUuid>
#include <QException>
#include <QSettings>
#include <QRegularExpression>

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

RsyncSystem::RsyncSystem()
{

}

RsyncSystem::~RsyncSystem()
{

}

bool RsyncSystem::supported(FSTabInfoList &fsTabInfoList)
{
    bool support = false;
    DeviceList deviceList;
    for (auto &info : fsTabInfoList) {
        if (info->isComment || info->isEmptyLine) {
            continue;
        }

        QString device = info->device;
        if (device.startsWith("UUID=")) {
            if (info->mountPoint != "/boot" &&
                info->mountPoint != "/boot/efi" &&
                info->mountPoint != "/recovery" &&
                info->mountPoint != "/tmp" &&
                info->mountPoint != "none" &&
                !info->options.contains("nofail")) {
                support = Device::isFsTypeSupported(info->type);
                if (!support) {
                    qCritical()<<"not support mountPoint = "<<info->mountPoint<<", fsType = "<<info->type;
                    return false;
                }
            }

            const int colSize = 2;
#if QT_VERSION >= QT_VERSION_CHECK(5,15,0)
            QStringList col = device.split("=", Qt::SkipEmptyParts);
#else
            QStringList col = device.split("=", QString::SkipEmptyParts);
#endif
            if (colSize == col.size()) {
                device = col[1].trimmed();
            } else {
                qWarning()<<"RsyncSystem::supported, invalid uuid, device = "<<device;
            }
        }

        if (info->device.startsWith("UUID=") &&
            info->mountPoint != "/boot" &&
            info->mountPoint != "/boot/efi" &&
            info->mountPoint != "/recovery" &&
            info->mountPoint != "none" &&
            info->type != "swap") {

            DevicePtr pDevice(new Device(device));
            pDevice->calculateDiskSpace();
            deviceList.append(pDevice);
        }
    }
    std::sort(deviceList.begin(), deviceList.end(), deviceCompare);
    if (!deviceList.isEmpty()) {
        m_defaultBackupDeviceUUID = deviceList.first()->getDeviceInfo()->uuid;
    }
    return support;
}

QString RsyncSystem::getDefaultBackupDeviceUUID(const QString &rootPath)
{
    return m_defaultBackupDeviceUUID;
}

QStringList RsyncSystem::getRsyncSystemBackupArgs()
{
    QStringList args = {"-aAXHi", "--verbose", "--delete", "--force", "--sparse", "--stats", "--delete-excluded",
                        "--info=progress2", "--ignore-missing-args", "--no-inc-recursive"};

    return args;
}

QStringList RsyncSystem::getRsyncSystemBackupExcludes()
{
    QStringList commonExcludes = {
            "/lost+found",
            "/cdrom/*",
            "/sdcared/*",
            "/system/*",
            "/.snapshots/*",
            "/recovery/*",
            "/dev/*",
            "/proc/*",
            "/sys/*",
            "/run/*",
            "/mnt/*",
            "/media/*",
            "/etc/uos-recovery/*",
            "/backup/system/snapshots/*",
            "/persistent/osroot/*",
            "/data/osroot/*",
            "/osroot/*",
            "/tmp/*"
    };

    QStringList varExcludes = {
            "/var/lib/docker/*",
            "/var/lib/schroot/*",
            "/var/swapfile",
            "/var/lib/deepin/reporter/*",
            "/var/lib/deepin/event-log/*",
            "/var/deepin/uos-recovery/"
    };

    QStringList homeExcludes = {
            "/home/*/**"
    };

    QStringList rootExcludes = {
            "/root/**"
    };

    QStringList totalExcludes = commonExcludes + varExcludes + homeExcludes + rootExcludes;

    QString fileName = "/etc/fstab";
    FSTabInfoList fstabInfos = FSTab::getFSTabFromFile(fileName);
    QMap<QString, QString> bindDirMap;
    FSTab::getFstabBindDir(fstabInfos, bindDirMap);
    for (auto it =  bindDirMap.begin(); it != bindDirMap.end(); ++it) {
        if (it.key() == "/home") {
            for (auto &itExclude : homeExcludes) {
                itExclude.replace(QRegularExpression("^/home/"), it.value());
            }
            totalExcludes += homeExcludes;
        } else if (it.key() == "/var") {
            for (auto &itExclude : varExcludes) {
                itExclude.replace(QRegularExpression("^/var/"), it.value());
            }
            totalExcludes += varExcludes;
        } else if (it.key() == "/root") {
            for (auto &itExclude : rootExcludes) {
                itExclude.replace(QRegularExpression("^/root/"), it.value());
            }
            totalExcludes += rootExcludes;
        }
    }

    return totalExcludes;
}

ErrorCode RsyncSystem::fillBackupInfo(const SystemBackupRequest &request)
{
    m_backupInfo.username = request.userName;
    m_backupInfo.operateID = QUuid::createUuid().toString();
    m_backupInfo.startTime = QDateTime::currentSecsSinceEpoch();
    m_backupInfo.remark = request.remark;
    m_backupInfo.operateType = static_cast<int> (OperateType::SystemBackup);
    m_backupInfo.recoveryType = static_cast<int>(RecoveryType::Rsync);
    m_backupInfo.rootUUID = request.rootUUID;
    m_backupInfo.backupDevUUID = request.destUUID;
    m_backupInfo.size = 0;

    DevicePtr backupDevice(new Device(m_backupInfo.backupDevUUID));
    if (backupDevice.isNull()) {
        qCritical() << QString("device is not exist, uuid:%1").arg(m_backupInfo.backupDevUUID);
        return PartitionNotExist;
    }
    if (backupDevice->getDeviceInfo()->label.isEmpty()) {
        QString diskName;
        DevicePtr pDisk(new Device(backupDevice->getDeviceInfo()->pkname));
        if (!pDisk.isNull()) {
            diskName = pDisk->getDeviceInfo()->mode;
        }
        m_backupInfo.backupDevice = QString("%1(%2)")
                .arg(diskName)
                .arg(Utils::byte2DisplaySize(backupDevice->getDeviceInfo()->sizeBytes));
    } else {
        m_backupInfo.backupDevice = backupDevice->getDeviceInfo()->label;
    }

    m_backupInfo.backupDeviceRemovable = backupDevice->getDeviceInfo()->mountPoint.startsWith("/media");
    return OK;
}

ErrorCode RsyncSystem::systemBackup(const SystemBackupRequest &request)
{

    if (request.rootUUID.isEmpty()) {
    //    qCritical() << "error: rootUUID isEmpty";
        return ErrorCode::RootUuidIsEmpty;
    }

    if (request.destUUID.isEmpty()) {
    //    qCritical() << "error: destUUID isEmpty";
        return ErrorCode::DestUuidIsEmpty;
    }

    m_request = request;
    auto ret = fillBackupInfo(request);
    if (ret != OK) {
        return ret;
    }

    QString fromDir;
    if (!Device::getMountPointByUUID(request.rootUUID, fromDir)) {
    //    qCritical() << "getMountPointByUUID failed, rootUUID = "<< request.rootUUID;
        return PartitionNotMount;
    }

    QString toDir;
    if (!Device::getMountPointByUUID(request.destUUID, toDir)) {
    //    qCritical() << "getMountPointByUUID failed, destUUID = "<< request.destUUID;
        return PartitionNotMount;
    }

    QString backupVersion;
    QString err;
    if(!Process::spawnCmd("deepin-boot-kit", {"-action=version"}, backupVersion, err)) {
        qCritical()<<Q_FUNC_INFO<<" get version failed, err = "<<err<<", backupVersion = "<<backupVersion;
        return InvalidVersion;
    }

    //每次备份都创建一个备份任务，任务执行完毕后自动删除
    m_pSystemBackupTask = new RsyncTask;
    m_pSystemBackupTask->setOperateType(OperateType::SystemBackup);
    connect(m_pSystemBackupTask, &RsyncTask::progressChanged, [=](const QJsonObject &jsonObject) {
        m_backupInfo.status = Running;
        QJsonObject progress = jsonObject;
        progress.insert("operateType", static_cast<int>(OperateType::SystemBackup));
        progress.insert("operateID", request.operateID);
        Q_EMIT progressChanged(Utils::JsonToQString(progress));
    });

    connect(m_pSystemBackupTask, &AsyncTask::success, [=] {
        m_backupInfo.status = Success;
        writeBackupInfo();
        Process::spawnCmd("rm", {"-fr", m_last});
        QString out;
        QString errMsg;
        Process::spawnCmd("ln", {"-s", m_destDir, m_last}, out, errMsg);
        m_pSystemBackupTask->updateGrub();
        qInfo() << "errMsg = "<< errMsg <<", out = "<<out;
        ResultInfo retInfo(0, "success", "");
        this->reportEventLog(retInfo, OperateType::SystemBackup, RecoveryType::Rsync);
        QJsonObject jsonObject;
        jsonObject.insert("operateType", static_cast<int>(OperateType::SystemBackup));
        jsonObject.insert("errCode", 0);
        jsonObject.insert("operateID", request.operateID);
        Q_EMIT success(Utils::JsonToQString(jsonObject));
        qInfo() << "AsyncTask::success, destDir = " << m_destDir;
    });

    connect(m_pSystemBackupTask, &RsyncTask::error, [=](const QJsonObject &jsonObject) {
        m_backupInfo.status = Failed;
        QJsonObject errJson = jsonObject;
        errJson.insert("operateType", static_cast<int>(OperateType::SystemBackup));
        errJson.insert("operateID", request.operateID);
        Process::spawnCmd("rm", {"-fr", m_destDir});

        int errCode = jsonObject.value("errCode").toInt(-1);
        QString errMsg = jsonObject.value("errMsg").toString();
        ResultInfo retInfo(errCode, "failed", errMsg);
        this->reportEventLog(retInfo, OperateType::SystemBackup, RecoveryType::Rsync);
        Q_EMIT error(Utils::JsonToQString(errJson));
    });

    connect(m_pSystemBackupTask, &QThread::finished, [=] {
    //    qInfo() << "task finish";
        m_pSystemBackupTask->deleteLater();
    });

    QString grubDisplayTime = QDateTime::fromSecsSinceEpoch(m_backupInfo.startTime).toString("yyyy/MM/dd hh:mm:ss");
    m_backupInfo.versionDisplay = Utils::getOSVersionDisplay(true) + " (" + grubDisplayTime + ")";
    m_backupInfo.backupVersion = backupVersion;
    m_backupInfo.backupPath = BACKUP_PATH + QString("%1/localhost").arg(backupVersion);
    if (backupVersion.isEmpty()) {
        QString curTime = QDateTime::fromSecsSinceEpoch(m_backupInfo.startTime).toString("yyyy-MM-dd_hh-mm-ss");
        m_backupInfo.backupPath = BACKUP_PATH + QString("%1/localhost").arg(curTime);
    }

    static QString systemVersion = Utils::getOSVersion();
    m_backupInfo.systemVersion = systemVersion;
    m_destDir = QDir(toDir + m_backupInfo.backupPath).absolutePath();
    m_pSystemBackupTask->setOptions(this->getRsyncSystemBackupArgs());
    m_pSystemBackupTask->setSourcePath(fromDir);
    m_pSystemBackupTask->setDestinationPath(m_destDir);
    m_pSystemBackupTask->setCheckPath(m_destDir);
    QString curBackupDirHome = QDir(toDir + BACKUP_PATH).absolutePath();
    QStringList sysBackupExcludes = this->getRsyncSystemBackupExcludes();
    sysBackupExcludes << QString(curBackupDirHome + "/*");
    m_pSystemBackupTask->setExcludes(sysBackupExcludes);
    m_pSystemBackupTask->enableDryRun(false);

    QString linkDest = QDir(toDir + BACKUP_PATH + "last/").absolutePath();
    m_last = linkDest;
    m_pSystemBackupTask->setLinkDest(linkDest);
   // qInfo()<<"RsyncSystem::systemBackup, destDir = " << m_destDir;
    QDir dir(m_destDir);
    if (!dir.exists()) {
    //    qInfo()<<"RsyncSystem::systemBackup, mkpath destDir = "<< m_destDir;
        dir.mkpath(m_destDir);
    }
    m_pSystemBackupTask->buildArgumentsForBackup();
    m_pSystemBackupTask->start();
    return OK;
}

ErrorCode RsyncSystem::systemRestore(const SystemRestoreRequest &request)
{
    QString rootPath;
    if (!Device::getMountPointByUUID(request.backupInfo.rootUUID, rootPath)) {
    //    qCritical() << "getMountPointByUUID failed, rootUUID = " << request.backupInfo.rootUUID;
        return PartitionNotMount;
    }

    QString recoveryConf = rootPath + UOS_RECOVERY_INI;
    QSettings settings(recoveryConf, QSettings::IniFormat);
    settings.beginGroup(RESTORE_GROUP);
    settings.setValue(RESTORE_DO_RESTORE_KEY, true);
    settings.setValue(RESTORE_RECOVERY_TYPE_KEY, request.backupInfo.recoveryType);
    settings.setValue(RESTORE_BACKUP_DEV_UUID_KEY, request.backupInfo.backupDevUUID);
    settings.setValue(RESTORE_BACKUP_POINT_KEY, request.backupInfo.backupPath);
    // set value to RestoreConst::RESTORE_SUCCESS after real restore success
    settings.setValue(RESTORE_ROLLBACK_STATUS, RestoreConst::RESTORE_FAILED);
    settings.setValue(RESTORE_ROLLBACK_VERSION, request.backupInfo.versionDisplay);
    settings.endGroup();

    ResultInfo retInfo(0, "success", "");
    this->reportEventLog(retInfo, OperateType::SystemRestore, RecoveryType::Rsync);

    return OK;
}

ErrorCode RsyncSystem::createUImg(const QString &backupDir)
{
    return OK;
}

BackupInfoList RsyncSystem::listSystemBackup(QStringList &destUUIDs)
{
    BackupInfoList backupInfoList;
    for(auto &uuid : destUUIDs) {
        QString mountPoint;
        Device::getMountPointByUUID(uuid, mountPoint);
        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";
            qInfo() << filename;
            QFile backupInfoFile(filename);
            if (backupInfoFile.open(QIODevice::Text | QIODevice::ReadOnly)) {
                QJsonObject jsonObject = Utils::QStringToJson(backupInfoFile.readAll());
                BackupInfo backupInfo;
                backupInfo.unmarshal(jsonObject);
                backupInfoList.append(backupInfo);
                backupInfoFile.close();
            }
        }
    }
    return backupInfoList;
}

ErrorCode RsyncSystem::removeSystemBackup(const RemoveUserDataBackupRequest &request)
{
    m_pRemoveTask = new RemoveTask;
    connect(m_pRemoveTask, &RemoveTask::error, [=] (const QJsonObject &jsonObject) {
        QJsonObject errJson = jsonObject;
        errJson.insert("operateType", static_cast<int>(OperateType::removeBackup));
        errJson.insert("operateID", request.operateID);
        Q_EMIT error(Utils::JsonToQString(errJson));
    });

    connect(m_pRemoveTask, &RemoveTask::success, [=] (const QJsonObject &jsonObject) {

        //更新last指向
        QString mountPoint;
        if (!Device::getMountPointByUUID(request.backupInfo.backupDevUUID, mountPoint)) {
        //    qCritical() << "getMountPointByUUID failed, backupDevUUID = "<< request.backupInfo.backupDevUUID;
            QJsonObject errJson = jsonObject;
            errJson.insert("operateType", static_cast<int>(OperateType::removeBackup));
            errJson.insert("operateID", request.operateID);
            Q_EMIT error(Utils::JsonToQString(errJson));
        }

        QDir backupPath(mountPoint + BACKUP_PATH);
        if (!Process::spawnCmd("rm", QStringList() << "-rf" << backupPath.absolutePath() + "/last")) {
            qCritical() << QString("delete last failed:%1").arg(backupPath.absolutePath() + "/last");
        }

        auto backupInfos = listSystemBackup(QStringList() << request.backupInfo.backupDevUUID);
        if (!backupInfos.isEmpty()) {
            std::sort(backupInfos.begin(), backupInfos.end());
            const QString cmd = "ln";
            QStringList args = {"-s",
                                QDir(mountPoint + backupInfos.last().backupPath).absolutePath(),
                                backupPath.absolutePath() + "/last"};
            if (!Process::spawnCmd(cmd, args)) {
                qCritical() << QString("exec command failed:%1 %2").arg(cmd).arg(args.join(" "));
            }
        }

        QString out;
        QString errMsg;
        if (Process::spawnCmd("update-grub", {}, out, errMsg)) {
            qInfo()<<"removeSystemBackup update-grub success ...";
        } else {
            qInfo()<<"removeSystemBackup update-grub failed, errMsg = "<<errMsg<<", out = "<<out;
        }

        QJsonObject msg = jsonObject;
        msg.insert("operateType", static_cast<int>(OperateType::removeBackup));
        msg.insert("operateId", request.backupInfo.operateID);
        msg.insert("operateID", request.operateID);
        Q_EMIT success(Utils::JsonToQString(msg));
    });

    connect(m_pRemoveTask, &QThread::finished, [=] {
    //    qInfo() << "task finish";
        m_pRemoveTask->deleteLater();
    });

    return m_pRemoveTask->remove(request.backupInfo);
}

void RsyncSystem::writeBackupInfo()
{
    QDir dir(m_destDir);
    if (dir.cdUp()) {
        QString backupInfoFile = dir.absolutePath() + "/backup_info.json";
        QFile file(backupInfoFile);
        try {
            if (file.open(QIODevice::Text | QIODevice::ReadWrite)) {
                QJsonObject jsonObject = m_backupInfo.marshal();
                auto content = Utils::JsonToQString(jsonObject);
                file.write(content.toUtf8());
                file.close();
            }
        } catch (QException &e) {
            qCritical() << e.what();
            file.close();
        }
    }
}

ErrorCode RsyncSystem::checkFullSystemBackupSpace(SystemBackupRequest &request)
{
    if (nullptr == m_checkTask) {
        m_checkTask = new CheckTask(OperateType::CheckFullSystemBackupSpace, RecoveryType::Rsync);

        connect(m_checkTask, &CheckTask::spaceCheckFinished, [=](const QJsonObject &jsonObject) {
            QJsonObject spaceJson = jsonObject;
            spaceJson.insert("operateType", static_cast<int>(OperateType::CheckFullSystemBackupSpace));
            spaceJson.insert("operateID", request.operateID);
            QString spaceInfo = Utils::JsonToQString(spaceJson);
            Q_EMIT spaceCheckFinished(spaceInfo);
        });

        connect(m_checkTask, &CheckTask::error, [=](const QJsonObject &jsonObject) {
            QJsonObject errJson = jsonObject;
            errJson.insert("operateType", static_cast<int>(OperateType::CheckFullSystemBackupSpace));
            errJson.insert("operateID", request.operateID);
            Q_EMIT error(Utils::JsonToQString(errJson));
        });

        connect(m_checkTask, &QThread::finished, [=] {
            m_checkTask->deleteLater();
            m_checkTask = nullptr;
        });
    }

    if (nullptr != m_checkTask) {
        m_checkTask->setRootUUID(request.rootUUID);
        m_checkTask->setDestUUID(request.destUUID);
        m_checkTask->start();
    }

    return OK;
}

ErrorCode RsyncSystem::checkIncSystemBackupSpace(const SystemBackupRequest &request)
{
    if (nullptr == m_checkTask) {
        m_checkTask = new CheckTask(OperateType::CheckIncSystemBackupSpace, RecoveryType::Rsync);

        connect(m_checkTask, &CheckTask::spaceCheckFinished, [=](const QJsonObject &jsonObject) {
            QJsonObject spaceJson = jsonObject;
            spaceJson.insert("operateType", static_cast<int>(OperateType::CheckIncSystemBackupSpace));
            spaceJson.insert("operateID", request.operateID);
            QString spaceInfo = Utils::JsonToQString(spaceJson);
            Q_EMIT spaceCheckFinished(spaceInfo);
        });

        connect(m_checkTask, &CheckTask::error, [=](const QJsonObject &jsonObject) {
            QJsonObject errJson = jsonObject;
            errJson.insert("operateType", static_cast<int>(OperateType::CheckIncSystemBackupSpace));
            errJson.insert("operateID", request.operateID);
            Q_EMIT error(Utils::JsonToQString(errJson));
        });

        connect(m_checkTask, &QThread::finished, [=] {
            m_checkTask->deleteLater();
            m_checkTask = nullptr;
        });
    }

    if (nullptr != m_checkTask) {
        m_checkTask->setDestUUID(request.destUUID);
        m_checkTask->start();
    }

    return OK;
}
