 #include "fmcontrol.h"
#include "fmtools.h"
#include "QsLog.h"
#include "fmdefine.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QCryptographicHash>
#include <QFile>
#include <QDir>
#include <windows.h>
#include <tlhelp32.h>
#include <Userenv.h>
#include <QProcess>
#include "JlCompress.h"

FmControl::FmControl()
{
    bool m_init_flag = true;
    int m_keepsleeptime = 0;
    m_keepTimer.setInterval(KEEP_INTERVAL);
    connect(&m_keepTimer, &QTimer::timeout, this, &FmControl::onKeepProcess);
}

FmControl::~FmControl()
{
    QLOG_INFO() << "fmupdate [exit]";
}

void FmControl::Start()
{
    QLOG_INFO() << QString("fmupdate [start]");
    m_appDir = QCoreApplication::applicationDirPath();

    m_keepTimer.start();
    //QTimer::singleShot(5000, this, &FmControl::onCheckUpdate);
}

void FmControl::onKeepProcess()
{
    QString error;
    QStringList diedProcList;

    QLOG_INFO() << qApp->applicationDirPath() + "/update.flag";

    if(m_init_flag == false)
    {
        QLOG_INFO() << "update.flag : " << m_init_flag;

        QFile file(qApp->applicationDirPath() + "/update.flag");

        QLOG_INFO() << ": " << file.exists();

        if(file.exists())
        {
            ++ m_keepsleeptime;

            m_keepsleeptime %= 121;


            if(m_keepsleeptime < 120)
            {
                QLOG_WARN() << "update.flag file exit wait times : " << m_keepsleeptime;
                return ;
            }
            else
            {
                file.remove();
                m_keepsleeptime = 0;
            }
        }
    }

    m_init_flag = false;
    m_keepsleeptime = 0;

    if(!_GetDiedProcess(FmTools::GetInstance().GetKeepExes(), diedProcList, error))
    {
        QLOG_ERROR() << QString("get died process failed [%1]").arg(error);
        return;
    }
    foreach(QString path, diedProcList)
    {
        if(!path.isEmpty())
        {
            QString exePath = QString("%1/%2").arg(m_appDir, path);
            exePath = QDir::cleanPath(exePath);
            QLOG_INFO() << QString("launch [%1]").arg(exePath);

            if(!_LaunchProcess(exePath, error))
            {
                QLOG_ERROR() << QString("launch process failed [%1]").arg(error);
            }else
            {
                QLOG_INFO() << QString("launch process successful");
            }
        }  
    }
}

bool FmControl::_GetDiedProcess(const QStringList &exeList, QStringList &diedProcList, QString &error)
{
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(hProcessSnap==INVALID_HANDLE_VALUE)
    {
        error = QString::number(::GetLastError());
        return false;
    }
    foreach(QString exe, exeList)
    {
        bool bDied = true;
        BOOL bMore = ::Process32First(hProcessSnap, &pe32);
        while(bMore)
        {
            if(!exe.mid(exe.lastIndexOf('/')+1).compare(QString::fromWCharArray(pe32.szExeFile)))
            {
                bDied = false;
                break;
            }
            bMore = ::Process32Next(hProcessSnap, &pe32);
        }
        if(bDied)
        {
            diedProcList.append(exe);
        }
    }
    ::CloseHandle(hProcessSnap);
    return true;
}

bool FmControl::_LaunchProcess(const QString &exePath, QString& error)
{
    // 在XP下用户环境可能没有wtsapi32.dll,XP下可直接调用程序
    typedef BOOL (WINAPI *_pfnWTSQueryUserToken)( ULONG SessionId, PHANDLE phToken );
    _pfnWTSQueryUserToken pfnWTSQueryUserToken = NULL;
    pfnWTSQueryUserToken = (_pfnWTSQueryUserToken)GetProcAddress(LoadLibrary(L"Wtsapi32.dll"), "WTSQueryUserToken");
    if(pfnWTSQueryUserToken == NULL)
    {
        error = QString("get WTSQueryUserToken failed");
        QLOG_ERROR() << error;
        return QProcess::startDetached(QString(qApp->applicationDirPath() + "/upstart.bat"), QStringList(exePath));
    }
    // 进程信息
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(STARTUPINFO));
    ZeroMemory(&pi, sizeof(pi));
    si.cb = sizeof(si);
    // 获得当前Session ID
    DWORD dwSessionID = WTSGetActiveConsoleSessionId();
    HANDLE hToken = NULL;
    // 获得当前Session的用户令牌
    if (pfnWTSQueryUserToken(dwSessionID, &hToken) == FALSE)
    {
        error = QString("get session failed [%1]").arg(QString::number(::GetLastError()));
        QLOG_ERROR() << error;
        return QProcess::startDetached(QString(qApp->applicationDirPath() + "/upstart.bat"), QStringList(exePath));
        //return false;
    }
    // 复制令牌
    HANDLE hDuplicatedToken = NULL;
    if (DuplicateTokenEx(hToken,
    MAXIMUM_ALLOWED, NULL,
    SecurityIdentification, TokenPrimary,
    &hDuplicatedToken) == FALSE)
    {
        if (hToken != NULL)
        {
            CloseHandle(hToken);
        }
        error = QString("copy token failed [%1]").arg(QString::number(::GetLastError()));
        //return false;
        QLOG_ERROR() << error;
        return QProcess::startDetached(QString(qApp->applicationDirPath() + "/upstart.bat"), QStringList(exePath));
    }
    // 创建用户Session环境
    LPVOID lpEnvironment = NULL;
    if (CreateEnvironmentBlock(&lpEnvironment, hDuplicatedToken, FALSE) == FALSE)
    {
        if (hDuplicatedToken != NULL)
        {
            CloseHandle(hDuplicatedToken);
        }
        error = QString("create environment failed [%1]").arg(QString::number(::GetLastError()));
        //return false;
        QLOG_ERROR() << error;
        return QProcess::startDetached(QString(qApp->applicationDirPath() + "/upstart.bat"), QStringList(exePath));
    }
    // 在复制的用户Session下执行应用程序，创建进程。
    if (CreateProcessAsUser(hDuplicatedToken,
    exePath.toStdWString().c_str(), NULL, NULL, NULL, FALSE,
    NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
    lpEnvironment, NULL, &si, &pi) == FALSE)
    {
        if (lpEnvironment != NULL)
        {
            DestroyEnvironmentBlock(lpEnvironment);
        }
        error = QString("CreateProcessAsUser failed [%1]").arg(QString::number(::GetLastError()));
        //return false;
        QLOG_ERROR() << error;
        return QProcess::startDetached(QString(qApp->applicationDirPath() + "/upstart.bat"), QStringList(exePath));
    }
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return true;
}

void FmControl::onCheckUpdate()
{
    QByteArray postData;
    QByteArray recvData;
    QJsonObject recvJson;
    int interval(CHECK_INTERVAL);
    QMap<QString, QString> sFilesMap;
    QMap<QString, QString> dFilesMap;
    QMap<QString, QString>::const_iterator i;
    QString error, reportError;
    bool result = true;
    QFile planIdFile(QString("%1/download/planid.txt").arg(m_appDir));

    // 如果有更新结果向服务器汇报结果
    QFile resultFile(QString("%1/download/updateresult.txt").arg(m_appDir));
    if(resultFile.exists())
    {
        QByteArray reportData;
        QByteArray reportRecv;
        reportData = FmTools::GetInstance().GetReportData();
        QLOG_INFO() << QString("report [%1]").arg(QString(reportData));
        if(_RequestData(FmTools::GetInstance().GetReportUrl(), reportRecv,
                         error, POST, reportData))
        {
            QLOG_INFO() << QString("report successful [%1]").arg(QString(reportRecv));
            resultFile.remove();
        }else
        {
            QLOG_ERROR() << QString("report failed [%1]").arg(error);
        }
    }

    // 向服务器发送请求
    postData = FmTools::GetInstance().GetUpdateData();
    QLOG_INFO() << QString("check update [%1]").arg(QString(postData));
    if(!_RequestData(FmTools::GetInstance().GetUpdateUrl(), recvData,
                     error, POST, postData))
    {
        reportError = QString("check update failed [%1]").arg(error);
        QLOG_ERROR() << reportError;
        result = false;
        goto end;
    }
    QLOG_INFO() << QString("check update successful [%1]").arg(QString(recvData));
    recvJson = QJsonDocument::fromJson(recvData).object();
    interval = recvJson[JSON_NEXTCHECK].toInt()*1000;
    if(recvJson[JSON_STATUSCODE].toInt() != 100)
    {
        goto end;
    }
    if(!recvJson[JSON_NEEDUPDATE].toString().compare("no"))
    {
        goto end;
    }

    //  获取服务器文件信息
    if(!_RequestData(QString("%1/upgradeInfos.txt").arg(recvJson[JSON_UPDATEPATH].toString()),
                     recvData, error))
    {
        reportError = QString("get filesInfo failed [%1]").arg(error);
        QLOG_ERROR() << reportError;
        result = false;
        goto end;
    }
    QLOG_INFO() << QString("get filesInfo successful [%1]").arg(QString(recvData));

    // 找出差异文件
    foreach(QJsonValue jv, QJsonDocument::fromJson(recvData).array())
    {
        QJsonObject fileObj = jv.toObject();
        sFilesMap.insert(fileObj[JSON_PATH].toString(),  fileObj[JSON_MD5].toString());
    }
    _CheckFilesDiff(sFilesMap, dFilesMap);
    if(dFilesMap.count() == 0)
    {
        QLOG_ERROR() << reportError;
        goto end;
    }

    // 下载差异文件
    for(i=dFilesMap.begin(); i!=dFilesMap.end(); i++)
    {
        QString downloadUrl = QString("%1/%2.zip").arg(recvJson[JSON_UPDATEPATH].toString(), i.key());
        QString savePath = QString("%1/download/%2.zip").arg(m_appDir, i.key());
        QLOG_INFO() << QString("downloading file [%1]").arg(downloadUrl);
        // 下载之前先检测文件是否下载过
        if(_CompareFileMd5(savePath.left(savePath.lastIndexOf(".")), i.value()))
        {
            continue;
        }
        if(!_DownloadFile(downloadUrl, savePath, error))
        {
            reportError = QString("download file failed [%1]").arg(error);
            QLOG_ERROR() << reportError;
            result = false;
            goto end;
        }
        _UnzipFile(savePath);
        if(!_CompareFileMd5(savePath.left(savePath.lastIndexOf(".")), i.value()))
        {
            reportError = QString("file md5 error");
            QLOG_ERROR() << reportError;
            result = false;
            goto end;
        }
        QLOG_INFO() << QString("downloading file successful");
    }
    QLOG_INFO() << QString("download files successful [%1]").arg(dFilesMap.count());
    // 记录升级任务编号
    if(planIdFile.open(QIODevice::WriteOnly))
    {
        planIdFile.write(QString::number(recvJson[JSON_PLANID].toInt()).toLatin1());
        planIdFile.close();
    }

    // 关闭所有守护的进程
    m_keepTimer.stop();
    if(_KillProcess(FmTools::GetInstance().GetKeepExes(), error))
    {
        // 将需要替换的文件写入./download/dfilesInfo.txt
        QFile file(QString("%1/download/dfilesInfo.txt").arg(m_appDir));
        if(file.open(QIODevice::WriteOnly))
        {
            for(i=dFilesMap.begin(); i!=dFilesMap.end(); i++)
            {
                file.write(QString("%1,").arg(i.key()).toUtf8());
            }
            file.close();

            // 调用文件替换程序
            QString porterPath = QString("%1/fmporter.exe").arg(m_appDir);
            QLOG_INFO() << QString("process be killed launch [%1]").arg(porterPath);
            if(QProcess::startDetached(porterPath, QStringList(porterPath)))
            {
                return;
            }else
            {
                reportError = QString("launch failed");
                QLOG_ERROR() << reportError;
                result = false;
            }

        }else
        {
            QLOG_ERROR() << QString("create dfilesInfo.txt failed");
        }
    }
    m_keepTimer.start();

end:
    if(!result)
    {
        // 如果失败就要汇报错误
        QByteArray reportData;
        QByteArray reportRecv;
        reportData = FmTools::GetInstance().GetReportData(QString::number(recvJson[JSON_PLANID].toInt()), reportError, "0");
        QLOG_INFO() << QString("report [%1]").arg(QString(reportData));
        if(_RequestData(FmTools::GetInstance().GetReportUrl(), reportRecv,
                         error, POST, reportData))
        {
            QLOG_INFO() << QString("report successful [%1]").arg(QString(reportRecv));
        }else
        {
            QLOG_ERROR() << QString("report failed [%1]").arg(error);
        }
    }
    QLOG_INFO() << QString("check update after %1MS").arg(interval);
    QTimer::singleShot(interval, this, &FmControl::onCheckUpdate);
}

bool FmControl::_CheckFilesDiff(const QMap<QString, QString> &sFilesMap, QMap<QString, QString> &dFilesMap)
{
    QMap<QString, QString>::const_iterator i;
    for(i=sFilesMap.begin(); i!=sFilesMap.end(); i++)
    {
        QFile file(QString("%1/%2").arg(m_appDir, i.key()));
        if(file.open(QFile::ReadOnly))
        {
            QString lFileMd5(QCryptographicHash::hash(file.readAll(),QCryptographicHash::Md5).toHex());
            if(!lFileMd5.compare(i.value()))
            {
                file.close();
                continue;
            }
            file.close();
        }
        dFilesMap.insert(i.key(), i.value());
    }
    return true;
}

bool FmControl::_CompareFileMd5(const QString &filePath, const QString &md5)
{
    bool result = false;
    QFile file(filePath);
    if(file.open(QFile::ReadOnly))
    {
        QString fileMd5 = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5).toHex();
        if(!fileMd5.compare(md5))
        {
            result = true;
        }
        file.close();
    }
    return result;
}

bool FmControl::_RequestData(const QString &url, QByteArray &recvData, QString &error, FmControl::RequestType type, const QByteArray &postData)
{
    QNetworkAccessManager manger;
    QNetworkRequest request;
    QNetworkReply* reply;
    QEventLoop loop;

    request.setUrl(url);
    reply = (type==RequestType::GET) ? manger.get(request) : manger.post(request, postData);
    reply->setReadBufferSize(0);
    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), &loop, &QEventLoop::quit);
    QTimer::singleShot(FmTools::GetInstance().GetRequestTimeout(), &loop, &QEventLoop::quit);
    loop.exec();

    reply->deleteLater();
    if(reply->error() != QNetworkReply::NoError)
    {
        error = reply->errorString();
        return false;
    }
    recvData = reply->readAll();
    if(recvData.length() == 0)
    {
        error = QString("timeout recv 0 bytes");
        return false;
    }
    return true;
}

bool FmControl::_DownloadFile(const QString &downloadUrl, const QString &savePath, QString &error)
{
    QDir().mkpath(savePath.left(savePath.lastIndexOf("/")));
    QFile file(savePath);
    if(!file.open(QIODevice::WriteOnly))
    {
        error = file.errorString();
        return false;
    }
    QNetworkAccessManager manger;
    QNetworkRequest request;
    QNetworkReply* reply;
    QEventLoop loop;

    request.setUrl(downloadUrl);
    reply = manger.get(request);
    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
    connect(reply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), &loop, &QEventLoop::quit);
    connect(reply, &QNetworkReply::readyRead, [=, &file]()
    {
        file.write(reply->readAll());
    });
    QTimer::singleShot(FmTools::GetInstance().GetDownloadTimeout(), &loop, &QEventLoop::quit);
    loop.exec();

    file.close();
    reply->deleteLater();
    if(reply->error() != QNetworkReply::NoError)
    {
        error = reply->errorString();
        return false;
    }
    if(!reply->isFinished())
    {
        error = QString("download time out");
        return false;
    }
    return true;
}

bool FmControl::_UnzipFile(const QString &zipFile)
{
    JlCompress::extractDir(zipFile, zipFile.left(zipFile.lastIndexOf("/")));
    return true;
}

bool FmControl::_KillProcess(const QStringList &exeList, QString &error)
{
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);
    HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(hProcessSnap==INVALID_HANDLE_VALUE)
    {
        error = QString::number(::GetLastError());
        return false;
    }
    foreach(QString exe, exeList)
    {
        BOOL bMore = ::Process32First(hProcessSnap, &pe32);
        while(bMore)
        {
            if(!exe.mid(exe.lastIndexOf('/')+1).compare(QString::fromWCharArray(pe32.szExeFile)))
            {
                HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
                if(hProcess != NULL)
                {
                    QLOG_ERROR() << QString("kill process %1").arg(QString::fromWCharArray(pe32.szExeFile));
                    TerminateProcess(hProcess,0);
                    WaitForSingleObject(hProcess,10000);
                    CloseHandle(hProcess);
                }else
                {
                    ::CloseHandle(hProcessSnap);
                    return false;
                }
            }
            bMore = ::Process32Next(hProcessSnap, &pe32);
        }
    }
    ::CloseHandle(hProcessSnap);
    return true;
}

