#include "legalizationagent.h"

#include <QNetworkRequest>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QCoreApplication>
#include <QStandardPaths>
#include <QNetworkConfiguration>
#include <QtMath>
#include <QUuid>
#include <QTimer>
#include <QProcess>
#include <QDateTime>

#include <jcon/json_rpc_tcp_server.h>
#include <jcon/json_rpc_tcp_client.h>
#include <jcon/json_rpc_debug_logger.h>
#include <DLog>
#include <QrCode.hpp>

#ifdef Q_OS_LINUX
#include "platform/linux/linuxsystemdetector.h"
#endif

#ifdef Q_OS_WIN
#include "platform/win/winsystemdetector.h"
#endif

#include "tool.h"
#include "util/networkagent.h"
#include "util/libcurl.h"
#include "util/config.h"
#include "updater/updater.h"
#include "util/pushclient.h"
#include "software/filter_manager.h"

using namespace qrcodegen;

namespace Tool
{

QString releaseQRC(const QString &resourceName, const QDir &dir)
{
    QFileInfo fi(resourceName);
    auto fileName = fi.fileName();
    auto destName = dir.filePath(fileName);

    auto ret = QFile::copy(resourceName, destName);
    if (ret) {
        return destName;

    }
    qDebug() << "failed copy" << resourceName << "to" << destName << ret;
    return resourceName;
}

int random(int start, int end)
{
    return start + (QDateTime::currentDateTime().toMSecsSinceEpoch()  % 10) * (end - start) / 10;
}

}

class LegalizationAgentPrivate
{
public:
    LegalizationAgentPrivate(LegalizationAgent *parent) : q_ptr(parent)
    {
        Q_Q(LegalizationAgent);
#ifdef Q_OS_LINUX
        detector = new LinuxSystemDetector(parent);
#endif
#ifdef Q_OS_WIN
        detector = new WinSystemDetector(parent);
#endif
        NetworkAgent::instance()->setSystemDetector(detector, q);

        Config::load();
        for (auto org : Config::organizations()) {
            orgcodes[org.id] = org.name;
        }

        upgrade2v12();

        QFile cityCodeData(":/city_code.json");
        if (cityCodeData.open(QIODevice::ReadOnly)) {
            auto cityCodeList = QJsonDocument::fromJson(cityCodeData.readAll()).array();
            parseCitiesDataJson(cityCodeList);
        }
        cityCodeData.close();

        updater = new Updater(Config::versionURL(),
                              qApp->applicationVersion(),
                              Config::autoUpgrade(),
                              Config::autoUpgradeInterval(),
                              parent);

        autoCommitInfoTimer = new QTimer(parent);
        autoCommitInfoTimer->setInterval(Config::autoCommitInterval());
        pushCheckTimer = new QTimer(parent);
        pushCheckTimer->setInterval(Config::autoMultivariateInterval());
    }

    void parseCitiesDataJson(const QJsonArray &jsonData)
    {
        for (auto const &value : jsonData) {
            QMap<QString, QString> info;
            QString area = value.toObject().value("area").toString();
            QString code = value.toObject().value("code").toString();
            int level = value.toObject().value("level").toInt();
            QString name = value.toObject().value("name").toString();
            QString prefix = value.toObject().value("prefix").toString();
            info.insert("area", area);
            info.insert("code", code);
            info.insert("level", QString::number(level));
            info.insert("name", name);
            info.insert("prefix", prefix);
            cityInfos.insert(code, info);
            QJsonArray cities = value.toObject().value("cities").toArray();
            if (cities.count() > 0) {
                parseCitiesDataJson(cities);
            }
        }
    }

    QStringList getNamesByAreacode(const QString &areacode)
    {
        QStringList names;

        QString provinceCode = areacode.left(2) + "0000";
        QString cityCode = areacode.left(4) + "00";

        QString provinceName = cityInfos.value(provinceCode).value("name", "");
        qDebug() << "province" << provinceCode << provinceName;
        if (provinceCode == areacode) {
            provinceName = "";
        }
        names.append(provinceName);

        QString cityName = cityInfos.value(cityCode).value("name", "");
        qDebug() << "city" << cityCode << cityName;
        if (cityCode == areacode) {
            cityName = "";
        }
        names.append(cityName);

        QString districtName = cityInfos.value(areacode).value("name", "");
        names.append(districtName);
        qDebug() << "district" << areacode << districtName;

        return names;
    }

    QString queryOrgName(const QString orgcode)
    {
        return orgcodes.value(orgcode);
    }

    QString queryOrgCode(const QString orgname)
    {
        for (auto key : orgcodes.keys()) {
            if (orgcodes.value(key) == orgname) {
                return key;
            }
        }
        qCritical() << "unknow org" << orgname << orgcodes;
        return "";
    }

    void upgrade2v12()
    {
        Q_Q(LegalizationAgent);
        QFile file(Config::UserDataPathLessThanV_1_2());
        if (!file.open(QIODevice::ReadOnly)) {
            qDebug() << "couldn't read old version form data from file:" << file.fileName();
            qDebug() << "skip upgrade";
            return;
        }
        QByteArray data = file.readAll();
        file.close();

        QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
        QJsonObject obj = jsonDoc.object();
        auto orgname = obj.value("orgcode").toString();
        userInfo.orgcode = queryOrgCode(orgname);
        if (!userInfo.orgcode.isEmpty()) {
            userInfo.ownner = obj.value("username").toString();
            userInfo.department = obj.value("department").toString();
            userInfo.orgname = orgname;

            q->saveUserData(userInfo);
            QFile::copy(file.fileName(), Config::UserDataBackupPathLessThanV_1_2());
            file.remove();
            qDebug() << "upgrade success";
        }
    }

    QString genUUID()
    {
        return QUuid::createUuid().toString();
    }

    void loadClientData()
    {
        QFile file(Config::ClientDataPath());
        if (!file.open(QIODevice::ReadOnly)) {
            qDebug() << "Couldn't read form client data from file:" << file.fileName();
        }
        QByteArray data = file.readAll();
        file.close();

        QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
        QJsonObject obj = jsonDoc.object();
        uuid = obj.value("uuid").toString();

        if (uuid.isEmpty()) {
            uuid = genUUID();
            saveClientData();
        }
    }

    void loadUserData();

    void saveClientData()
    {
        QJsonObject data = {
            {"uuid", uuid},
        };

        QFile file(Config::ClientDataPath());
        if (!file.open(QIODevice::WriteOnly)) {
            qDebug() << "couldn't save client data to file:" << file.fileName();
            return;
        }

        QJsonDocument jsonDoc(data);
        file.write(jsonDoc.toJson());
        file.close();

        qDebug() << "save client data" << data << "in" << file.fileName();
    }

    bool needMultivariate(int *port)
    {
        std::string response;
        std::string err;
        auto url = Config::multivariateURL() + "/" + detector->platform();
        libcurl_get(url.toStdString().data(), response, err);

        qDebug() << "check multivariate\t"
                 << "orgcode:" << userInfo.orgcode
                 << "uuid:" << uuid
                 << "response:" << response.data()
                 << "error:"  << err.data();

        auto obj = QJsonDocument::fromJson(response.data()).object();
        auto update_list = obj.value("update_list").toArray();
        *port = obj.value("update_id").toInt();

        if (update_list.contains(userInfo.orgcode)) {
            return true;
        }

        if (update_list.contains(uuid)) {
            return true;
        }
        return false;
    }

    void checkMultivariate()
    {
        Q_Q(LegalizationAgent);
        int port = 12688;
        if (needMultivariate(&port)) {
            if (!pushClient) {
                QUrl hostUrl(Config::multivariateURL());
                pushClient = new PushClient(uuid, hostUrl.host(), port, q);
            } else {
                pushClient->reset();
            }
        }
    }

    QNetworkAccessManager       networkManager;

    jcon::JsonRpcTcpClient      *checkerRpcClient   = Q_NULLPTR;
    int                         checkerClientPort   = 0;

    jcon::JsonRpcTcpServer      *rpcServer          = Q_NULLPTR;
    CommonSystemDetector        *detector           = Q_NULLPTR;
    QSharedPointer<SystemInfo>  systemInfo;

    QMap<QString, QMap<QString, QString>> cityInfos;
    QMap<QString, QString> orgcodes = {};

    QString uuid;
    UserInfo userInfo;

    Updater *updater            = Q_NULLPTR;
    PushClient *pushClient      = Q_NULLPTR;
    QTimer *autoCommitInfoTimer = Q_NULLPTR;
    QTimer *pushCheckTimer      = Q_NULLPTR;

    LegalizationAgent *q_ptr;
    Q_DECLARE_PUBLIC(LegalizationAgent)
};

LegalizationAgent::LegalizationAgent(QObject *parent) :
    QObject(parent), dd_ptr(new LegalizationAgentPrivate(this))
{
    Q_D(LegalizationAgent);

    d->loadClientData();
    d->loadUserData();
    loadSoftwareUsedHistory();

    initConnect();

    curl_global_init(CURL_GLOBAL_DEFAULT);

    auto delayMin = Tool::randBetween(5, 120) * 60 * 1000;
    if (!Config::delayCommit()) {
        delayMin = 5 * 1000;
    }
    qDebug() << "delay commit time info after" << delayMin / (60 * 1000) << "min";

    QTimer::singleShot(delayMin, [ = ] {
        qDebug() << "begin post info and log to server";
        FilterManager::instance()->update();
        postSystemInfo();
        postlog();
        d->autoCommitInfoTimer->start();
    });

    QTimer::singleShot(delayMin / 2, [ = ] {
        qDebug() << "begin save os history";
        d->detector->saveOSHistory();
    });

    QTimer::singleShot(2 * 1000, [ = ] {
        d->checkMultivariate();
    });

    d->pushCheckTimer->start();
}

LegalizationAgent::~LegalizationAgent()
{
    curl_global_cleanup();
}

void LegalizationAgent::initConnect()
{
    Q_D(LegalizationAgent);
    connect(d->updater, &Updater::downloadFinished, this, &LegalizationAgent::onDownloadFinished);
    connect(d->detector, &SystemDetectorInterface::softwareInfoCached, this, &LegalizationAgent::cacheSoftwareInfo);

    connect(d->autoCommitInfoTimer, &QTimer::timeout, this, [ = ] {
        FilterManager::instance()->update();

        postSystemInfo();
        postlog();
    });

    connect(d->pushCheckTimer, &QTimer::timeout, this, [ = ] {
        d->checkMultivariate();
    });
}

void LegalizationAgent::saveUserData(const UserInfo &userInfo)
{
    QJsonObject data = userInfo.toObject();

    QFile file(Config::UserDataPath());
    if (!file.open(QIODevice::WriteOnly)) {
        qDebug() << "couldn't save user data to file:" << file.fileName();
        return;
    }

    QJsonDocument jsonDoc(data);
    file.write(jsonDoc.toJson());
    file.close();

    qDebug() << "save user data" << data << "in" << file.fileName();
}


void LegalizationAgentPrivate::loadUserData()
{
    QFile file(Config::UserDataPath());
    if (!file.open(QIODevice::ReadOnly)) {
        qDebug() << "Couldn't read form data from file:" << file.fileName();
        return;
    }
    QByteArray data = file.readAll();
    file.close();

    QJsonDocument jsonDoc(QJsonDocument::fromJson(data));
    QJsonObject obj = jsonDoc.object();
    UserInfo fileUserInfo = UserInfo::fromObject(obj);
    fileUserInfo.orgname = queryOrgName(fileUserInfo.orgcode);

    userInfo = fileUserInfo;

    qDebug() << "load data in:" << Config::UserDataPath();
}

void LegalizationAgent::saveSoftwareUsedHistory(const QByteArray &data)
{
    QFile file(Config::HistoryPath());
    if (!file.open(QIODevice::WriteOnly)) {
        qWarning() << "couldn't save software used history to file:" << file.fileName();
        return;
    }
    file.write(data);
    file.close();
    qDebug() << "save system info in:" << file.fileName();
}

void LegalizationAgent::loadSoftwareUsedHistory()
{
    Q_D(LegalizationAgent);
    QFile file(Config::HistoryPath());
    if (!file.open(QIODevice::ReadOnly)) {
        qWarning() << "couldn't read software used history from file:" << file.fileName();
        return;
    }
    QByteArray data = file.readAll();
    file.close();

    auto doc = QJsonDocument::fromJson(data);
    qDebug() << "load history" << doc.object() << doc.isObject();
    if (doc.isObject()) {
        d->detector->setSoftareUsedHistory(doc.object());
    }
}

void LegalizationAgent::updateSystemInfo(const UserInfo &userInfo)
{
    Q_D(LegalizationAgent);
    d->userInfo = userInfo;

    d->systemInfo = d->detector->fetchSystemInfo();
    d->systemInfo->setUserInfo(d->userInfo);
    d->systemInfo->setAreaCode(Config::areacode());
    d->systemInfo->setUUID(d->uuid);
    saveUserData(userInfo);
}

QJsonObject LegalizationAgent::pushSystemInfoByCurl()
{
    Q_D(LegalizationAgent);
    std::string response;
    qDebug() << "post to" << Config::infoPostURL();
    QByteArray postData = d->systemInfo->full();
    qDebug().noquote() << postData;
    int errcode = libcurl_post(Config::infoPostURL().toStdString().data(),
                               postData.data(), response);

    QPair<int, std::string> ret = qMakePair(errcode, response);
    if (ret.first == 0) {
        d->detector->clearCache();
        saveSoftwareUsedHistory(QByteArray());
    }

    QJsonObject commitResult;
    commitResult["errcode"] = ret.first;
    commitResult["response"] = QString::fromStdString(ret.second);
    qDebug() << "libcurl_post result" << commitResult;

    commitResult["data"] = QJsonDocument::fromJson(postData).object();
    return commitResult;
}

void LegalizationAgent::startServer()
{
    Q_D(LegalizationAgent);

    d->rpcServer = new jcon::JsonRpcTcpServer(this, std::make_shared<jcon::JsonRpcDebugLogger>());
    d->rpcServer->registerServices({this});
    d->rpcServer->listen(9300);
}

void LegalizationAgent::postSystemInfo()
{
    Q_D(LegalizationAgent);
    d->loadUserData();
    if (!d->userInfo.orgcode.isEmpty()) {
        updateSystemInfo(d->userInfo);
        pushSystemInfoByCurl();
    }
}

void LegalizationAgent::postlog()
{
    Q_D(LegalizationAgent);
    QString url = Config::logPostURL();

    d->systemInfo = d->detector->fetchSystemInfo();

    QString logFile = QString("%1.log").arg(QDateTime::currentDateTime().toString(Qt::ISODate));
    std::map<std::string, std::string> headers;
    headers.insert(std::pair<std::string, std::string>("X-Client-Filetype", "log"));
    headers.insert(std::pair<std::string, std::string>("X-Client-Macaddress", d->systemInfo->getMacAddress().toStdString()));
    headers.insert(std::pair<std::string, std::string>("X-Client-Filename", logFile.toStdString()));

    QFile f(Dtk::Core::DLogManager::getlogFilePath());
    if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
        return;
    }
    QList<QByteArray> lines;
    while (!f.atEnd()) {
        QByteArray line = f.readLine();
        lines.append(line);
    }
    QList<QByteArray>  loglines;
    if (lines.length() > 500) {
        loglines = lines.mid(lines.length() - 500);
    } else {
        loglines = lines;
    }
    QString data = QString::fromLocal8Bit(loglines.join(""));

    qDebug() << "libcurl post to" << url;
    std::string response;
    int errcode = libcurl_post(url.toStdString().data(),
                               headers,
                               data.toStdString().data(),
                               response);
    QPair<int, std::string> ret = qMakePair(errcode, response);
    qDebug() << "libcurl_post result" << ret.first;
}


void LegalizationAgent::onDownloadFinished(const QString &filePath)
{
    Q_D(LegalizationAgent);
    qDebug() << filePath;

    bool isFinished = false;

//    if (!d->checkerRpcClient && d->checkerClientPort != 0) {
//        d->checkerRpcClient = new jcon::JsonRpcTcpClient();
//        bool isSuccessed = d->checkerRpcClient->connectToServer("127.0.0.1",
//                           d->checkerClientPort);
//        qDebug() << "connect to checker client" << isSuccessed;
//    }

//    if (d->checkerRpcClient) {
//        try {
//            std::shared_ptr<jcon::JsonRpcResult> reply = d->checkerRpcClient->call("StartUpdate");
//        } catch (...) {
//            qCritical() << "startUpdate failed";
//            // TODO: fallback
//            return;
//        }
//    }


#ifdef Q_OS_LINUX
    QFileInfo fi(filePath);
    auto workDir = fi.absoluteDir();
    auto updatePath = Tool::releaseQRC(QString("%1/update.sh").arg(PROJECT_INSTALL_PATH), workDir);
    QStringList args;
    args << updatePath << filePath;
    qDebug() << "start with args" << args;
    QProcess::startDetached("/bin/bash", args);
#endif

#ifdef Q_OS_WIN
    QFileInfo fi(filePath);
    auto workDir = fi.absoluteDir();
//    Tool::releaseQRC(":/svc.exe", workDir);
    Tool::releaseQRC(":/unzip.exe", workDir);
    auto updaterPath = qApp->applicationDirPath() + "/deepin-legalization-updater.exe";
    updaterPath = Tool::releaseQRC(updaterPath, workDir);
#endif

    if (d->checkerRpcClient) {
        if (isFinished) {
            try {
                std::shared_ptr<jcon::JsonRpcResult> reply = d->checkerRpcClient->call("FinishUpdate");
            } catch (...) {
                qCritical() << "startUpdate failed";
                // TODO: fallback
                return;
            }
        }
    }
#ifdef Q_OS_LINUX
//    qApp->quit();
#endif

#ifdef Q_OS_WIN
    // run update service
    qDebug() << "start update" << updaterPath << filePath << qApp->applicationDirPath();

    QProcess::startDetached(updaterPath, QStringList() << filePath << qApp->applicationDirPath());
#endif
}

void LegalizationAgent::cacheSoftwareInfo()
{
    Q_D(LegalizationAgent);
    QJsonDocument doc(d->detector->getSoftwareUsedHistory());
    saveSoftwareUsedHistory(doc.toJson());
}

void LegalizationAgent::ReportUserInfo(const QString &jsonStr)
{
    qDebug() << "get user session info" << jsonStr;
}

void LegalizationAgent::RegisterClientPort(const int &port)
{
    Q_D(LegalizationAgent);
    d->checkerClientPort = port;
    qDebug() << "client port" << port;
    if (!d->checkerRpcClient) {
        d->checkerRpcClient = new jcon::JsonRpcTcpClient();
        bool isSuccessed = d->checkerRpcClient->connectToServer("127.0.0.1", d->checkerClientPort);
        qDebug() << "connect to checker client" << isSuccessed;
    } else {
        d->checkerRpcClient->connectToServer("127.0.0.1", d->checkerClientPort);
    }
}

QString LegalizationAgent::QuerySystemInfo()
{
    Q_D(LegalizationAgent);
    return QString::fromUtf8(d->detector->fetchSystemInfo()->toJson());
}

QString LegalizationAgent::QueryAreacode()
{
    return Config::areacode();
}

QString LegalizationAgent::QueryOrgcode()
{
    Q_D(LegalizationAgent);
    return d->userInfo.orgcode;
}

QMap<QString, QString> LegalizationAgent::QueryOrgcodes()
{
    Q_D(LegalizationAgent);
    return d->orgcodes;
}

QString LegalizationAgent::QueryApiURL()
{
    return Config::infoPostURL();
}

QStringList LegalizationAgent::QueryCityNames()
{
    Q_D(LegalizationAgent);
    return d->getNamesByAreacode(QueryAreacode());
}

QJsonObject LegalizationAgent::QueryUserInfo()
{
    Q_D(LegalizationAgent);
    d->loadUserData();
    QJsonObject data = d->userInfo.toObject();
    qDebug() << "userinfo:" << data;
    return data;
}

// return base64 string
QString LegalizationAgent::SubmitSystemInfo(QByteArray userInfoJson)
{
    qDebug() << QString::fromLocal8Bit(userInfoJson);

    updateSystemInfo(UserInfo::fromJson(userInfoJson));

    auto ret = pushSystemInfoByCurl();
    return QString(QJsonDocument(ret).toJson().toBase64());
}

QString LegalizationAgent::GetQrMessage(QByteArray userInfoJson)
{
    Q_D(LegalizationAgent);
    updateSystemInfo(UserInfo::fromJson(userInfoJson));

    QByteArray jsonData;
    jsonData = d->systemInfo->briefV2();
    qDebug() << jsonData << jsonData.size();
    jsonData = jsonData.toBase64();
    qDebug() << jsonData << jsonData.size();

    std::vector<uint8_t> data;
    for (int i = 0; i < jsonData.size(); ++i) {
        data.push_back(static_cast<uint8_t>(jsonData.at(i)));
    }
    QrCode qr0 = QrCode::encodeBinary(data, QrCode::Ecc::MEDIUM);
    std::string svg = qr0.toSvgString(4);

    return QString::fromStdString(svg);
}
