/*
 * Copyright (c) KylinSoft  Co., Ltd. 2024. All rights reserved.
 *
 * kaiming is licensed under the GPL v2.0+.
 * 
 * See the LICENSE file for more details.
 */

#include "KMBuildInit.h"

#include <fstream>
#include <set>
#include <nlohmann/json.hpp>

#include "common/KMLogger.h"
#include "common/KMBuildinOptions.h"
#include "common/kmtranslation.h"
#include "common/KMException.h"
#include "common/KMInstalledAppQuery.h"
#include "common/KMInfoJson.h"
#include "common/KMStringUtils.h"
#include "common/KMFileUtils.h"
#include "common/KMUtil.h"
#include "common/KMVersion.h"

class KMBuildInit::Options : public KMOption::Options
{
public:
    Options() = default;
    ~Options() override = default;

protected:
    void preParseHook() override;
    void postParseHook() override;

private:
    void addOptions();

public:
    std::string m_arch;
    std::string m_runtime;
    std::string m_base;
    std::string m_type;
    std::string m_channel;
    std::string m_version;
    std::vector<std::string> m_depends;
    bool m_help = false;

    std::string m_buildDir;
    std::string m_appid;
    std::string m_cacheDir;

    bool m_kylin_privilege = false;
    bool m_kylin_sysapp = false;

private:
    std::set<std::string> m_setTypes{ "app", "runtime", "extension", "base", "depend" };
};

void KMBuildInit::Options::preParseHook()
{
    addOptions();
}

void KMBuildInit::Options::postParseHook()
{
    if (m_help)
    {
        showUsage();
        exit(EXIT_SUCCESS);
    }


    //检查app的version
    if (!m_version.empty())
    {
        KMVersion ver = KMVersion::parse(m_version);
    }
    
    if (m_buildDir.empty() || m_appid.empty())
    {
        KMError(_("DIRECTORY APPID must be specified"));
        showUsage();
        exit(EXIT_FAILURE);
    }

    m_buildDir = fs::absolute(m_buildDir);

    if (m_setTypes.end() == m_setTypes.find(m_type))
    {
        KMError(m_type + _(" is not a valid build type name, use app, runtime or extension"));
        exit(EXIT_FAILURE);
    }

    std::string errMsg;
    if (!KMBuildinsUtils::isValidAppName(m_appid, errMsg))
    {
        KMError(errMsg);
        exit(EXIT_FAILURE);
    }
}

void KMBuildInit::Options::addOptions()
{
    setDescription(_("\nUsage:\n \tkaiming build-init [OPTIONS] DIRECTORY APPID"
                     "\nExample:\n \tkaiming build-init --base=stable:top.openkylin.base/1.0.0 --runtime=stable:top.openkylin.ukui/1.0.0 build top.openkylin.demo\n"));

    //帮助选项
    addOption("help","h", KMOption::value<bool>(&m_help), _("Show help options"));

    //应用选项
    addOption("arch", "", KMOption::value<std::string>(&m_arch)->defaultValue(KMUtil::kmGetArch()), _("Arch to use"));
    addOption("runtime", "", KMOption::value<std::string>(&m_runtime), _("Runtime info, format : ${runtime-channel}:${runtime-id}/${runtime-version}, such as : stable:top.openkylin.ukui/1.0.0"));
    addOption("base", "", KMOption::value<std::string>(&m_base), _("Base info, format : ${base-channel}:${base-id}/${base-version}, such as : stable:top.openkylin.base/1.0.0"));
    addOption("kind", "", KMOption::value<std::string>(&m_type)->defaultValue("app"), _("Specify the build type (app, runtime, extension, base, depend)"));
    addOption("channel", "", KMOption::value<std::string>(&m_channel)->defaultValue("stable"), _("Release channel, such as (main, stable, etc.)"));
    addOption("depend", "", KMOption::value<std::vector<std::string>>(&m_depends), _("Format as: {id}/{version}. There can be multiple."));
    addOption("version", "", KMOption::value<std::string>(&m_version), _("app version"));
    addOption("kylin-privilege", "", KMOption::value(&m_kylin_privilege), _("annotations->kylin.privilege"), true);
    addOption("kylin-sysapp", "", KMOption::value(&m_kylin_sysapp), _("annotations->kylin.sysapp"), true);
    addOption("cacheDir", "", KMOption::value(&m_cacheDir), _("The directory of cache"));

    //位置选项
    addPositionOption("DIRECTORY", KMOption::value<std::string>(&m_buildDir), 1, _("The directory for building"));
    addPositionOption("APPID", KMOption::value<std::string>(&m_appid), 1, _("Appid"));
}

class KMBuildInit::Private
{
public:
    std::unique_ptr<Options> m_kmOptions;

    KMStorageDir m_baseDir;
    KMStorageDir m_runtimeDir;
    std::optional<KMRef> m_originalBaseRef;
    std::optional<KMRef> m_originalRuntimeRef;
    KMRef m_baseRef;
    KMRef m_runtimeRef;

    std::string m_filesDir;
    std::string m_metadataFile;
};

REGISTER_SUBCOMMAND_DYNCREATE(build-init, KMBuildInit)

bool KMBuildInit::checkBaseOrRuntime(const fs::path &infoPath , const bool runtimeFlag) const
{
    KMInfoJson::InfoBase app, runOrDep;
    
    KMInfoJson infoJson;
    infoJson.loadFile(infoPath);

    //校验base
    if (!runtimeFlag)
    {
        app.m_baseId = d->m_baseRef.id;
        app.m_baseVersion= d->m_baseRef.version;

        runOrDep.m_baseString = infoJson.base;
    }
    else if (infoJson.runtime.empty())
    {
        return false;
    }
    //校验runtime
    else
    {
        app.m_baseId = d->m_runtimeRef.id;
        app.m_baseVersion= d->m_runtimeRef.version;

        runOrDep.m_baseString = infoJson.runtime;
    }

    runOrDep.baseParse();
    app.versionParse();
    runOrDep.versionParse();
    
    if (app.m_baseId != runOrDep.m_baseId || ! app.checkVersion(runOrDep))
    {
        return false;
    }

    return true;

}

KMBuildInit::KMBuildInit()
    : d(std::make_unique<Private>())
{
    d->m_kmOptions = std::make_unique<Options>();
}

KMBuildInit::~KMBuildInit() = default;

int KMBuildInit::dispose(int argc, char **argv)
{
    KMTrace("KMBuildInit::dispose invoke begin");

    init(argc, argv);

    int ret = run();

    KMTrace("KMBuildInit::dispose invoke end");
    return ret;
}

void KMBuildInit::init(int argc, char **argv)
{
    KMTrace("KMBuildInit::init invoke begin");

    d->m_kmOptions->parseCommandLine(argc, argv);

    // 解析base、runtime，并校验是否已经安装
    d->m_originalBaseRef = KMBuildInit::parsePref(d->m_kmOptions->m_base);
    if (!d->m_originalBaseRef)
    {
        throw KMException("The config of base '" + d->m_kmOptions->m_base + "' is wrong");
    }

    // 校验base是否已安装
    KMInstalledAppQuery query;
    std::vector<KMRef> bases = query.channel(d->m_originalBaseRef->channel)
                                    .arch(d->m_kmOptions->m_arch)
                                    .kind(BASE_TYPE_BASE)
                                    .id(d->m_originalBaseRef->id)
                                    .module(MODULE_NAME_DEV)
                                    .version(d->m_originalBaseRef->version)
                                    .query(KMInstalledAppQuery::All);
    if (bases.empty())
    {
        KMInfo("Preparing to install development base:  "
            + KMStringUtils::buildFilename(d->m_originalBaseRef->id, d->m_originalBaseRef->version, MODULE_NAME_DEV, d->m_originalBaseRef->channel)); 
        KMDbusClient client;
        std::string name = KMStringUtils::buildFilename(d->m_originalBaseRef->id, d->m_originalBaseRef->version, MODULE_NAME_DEV);
        if (!KMBuildInit::installDev(client, name, d->m_originalBaseRef->channel))
        {
            throw KMException("Please install it manually: sudo kaiming install " 
                            + KMStringUtils::buildFilename(d->m_originalBaseRef->id, d->m_originalBaseRef->version, MODULE_NAME_DEV, d->m_originalBaseRef->channel));
        }                
    }
    d->m_baseRef = bases.at(0);

    // 解析runtime，并校验是否已经安装
    if (!d->m_kmOptions->m_runtime.empty())
    {
        d->m_originalRuntimeRef = KMBuildInit::parsePref(d->m_kmOptions->m_runtime);
        if (!d->m_originalRuntimeRef)
        {
            throw KMException("The config of runtime '" + d->m_kmOptions->m_runtime + "' is wrong");
        }

        std::vector<KMRef> runtimes = query.channel(d->m_originalRuntimeRef->channel)
                                           .arch(d->m_kmOptions->m_arch)
                                           .kind(BASE_TYPE_RUNTIME)
                                           .id(d->m_originalRuntimeRef->id)
                                           .module(MODULE_NAME_DEV)
                                           .version(d->m_originalRuntimeRef->version)
                                           .query(KMInstalledAppQuery::All);
        if (runtimes.empty())
        {
            KMInfo("Preparing to install development runtime:  "
                 + KMStringUtils::buildFilename(d->m_originalRuntimeRef->id, d->m_originalRuntimeRef->version, MODULE_NAME_DEV, d->m_originalRuntimeRef->channel)); 
            
            KMDbusClient client;
            std::string name = KMStringUtils::buildFilename(d->m_originalRuntimeRef->id, d->m_originalRuntimeRef->version, MODULE_NAME_DEV);
            if (!KMBuildInit::installDev(client, name, d->m_originalBaseRef->channel))
            {
                throw KMException("Please install it manually: sudo kaiming install " 
                                 + KMStringUtils::buildFilename(d->m_originalRuntimeRef->id, d->m_originalRuntimeRef->version, MODULE_NAME_DEV, d->m_originalRuntimeRef->channel));
            }              
        }
        d->m_runtimeRef = runtimes.at(0);

        //  app的base和runtime的base要保持一致

        std::string runtimeDeployPath = KMStorageDir::getDeployedDir(d->m_runtimeRef);
        std::string infoPath = KMStringUtils::buildFilename(runtimeDeployPath, "info.json");

        if (!checkBaseOrRuntime(infoPath))
        {
            throw KMException("The appBase and the runtimeBase is not matched");
        }
    }

    d->m_filesDir = d->m_kmOptions->m_buildDir + "/files";
    d->m_metadataFile = d->m_kmOptions->m_buildDir + "/info.json";

    if (KMFileUtils::pathExists(d->m_filesDir))
    {
        throw KMException(std::string(_("Build directory already initialized : ")) + d->m_kmOptions->m_buildDir);
    }

    KMTrace("KMBuildInit::init invoke end");
}

int KMBuildInit::run()
{
    KMTrace("KMBuildInit::run invoke begin");

    // 创建构建目录
    std::error_code ec;
    if (!KMFileUtils::mkpath(d->m_kmOptions->m_buildDir, ec))
    {
        throw KMException(ec.message());
    }

    // 创建files目录
    if (!KMFileUtils::mkpath(d->m_filesDir, ec))
    {
        throw KMException(ec.message());
    }

    //depends是否为空
    if (!d->m_kmOptions->m_depends.empty())
    {
        if (d->m_kmOptions->m_type == BASE_TYPE_APP)
        {
            std::vector<std::string> all_depends;
            addDependsForApp(d->m_kmOptions->m_depends, all_depends);
        }
        else if (d->m_kmOptions->m_type == BASE_TYPE_DEPEND)
        {
            addDependsForDepend(d->m_kmOptions->m_depends);
        }
    }

    // 生成info.json文件
    generateMetadata();

    KMTrace("KMBuildInit::run invoke end");
    return EXIT_SUCCESS;
}

/**
 * @brief : 校验depend并获取其所在路径
 * @param : depend.id
 * @return : depend所在路径
 */
std::string KMBuildInit::getDependPath(const std::string &depend)
{
    std::vector<std::string> splits = KMStringUtils::splitString(depend, "/");
    if (splits.size() == 1)
    {
        splits.emplace_back("");
    }
    KMInstalledAppQuery query;
    std::vector<KMRef> depends = query.arch(d->m_kmOptions->m_arch)
                                    .kind(BASE_TYPE_DEPEND)
                                    .id(splits.at(0))
                                    .module(MODULE_NAME_DEV)
                                    .version(splits.at(1))
                                    .query(KMInstalledAppQuery::All);
    if (depends.empty())
    {
        std::string name = splits.at(0) + "/" + splits.at(1) + "/" + MODULE_NAME_DEV; // KMStringUtils::buildFilename(splits.at(0), splits.at(1), MODULE_NAME_DEV);

        KMInfo("Preparing to install development depend:  "
                + name
                + "..."); 
        
        KMDbusClient client;
        if (!KMBuildInit::installDev(client, name, d->m_originalBaseRef->channel))
        {
            throw KMException("Please install it manually: sudo kaiming install " 
                                + splits.at(0) + (splits.at(1).empty() ? "" : ("/" + splits.at(1))));
        }
    }

    KMRef dependRef = depends.at(0);
    std::string dependDeployPath = KMStorageDir::getDeployedDir(dependRef);
    std::string dependInfo = KMStringUtils::buildFilename(dependDeployPath, "info.json");

    //先校验runtime，runtime匹配可不校验base
    if (d->m_runtimeRef.id.empty() || !checkBaseOrRuntime(dependInfo,true))
    {
        if (!checkBaseOrRuntime(dependInfo))
        {
            throw KMException("The appBase and the dependBase is not matched");
        }
    }
    return dependDeployPath;
}

/**
 * @brief: 处理depend依赖
 */

void KMBuildInit::addDependsForApp(const std::vector<std::string> &depends, std::vector<std::string> &all_depends)
{
    std::string depend_file_target = "files";
    for (auto const &depend : depends)
    {
        if (std::find(all_depends.begin(), all_depends.end(), depend) != all_depends.end())
        {
            continue;
        }
        std::string dependDeployPath = getDependPath(depend);
        std::string dependInfo = KMStringUtils::buildFilename(dependDeployPath, "info.json");
        //决定拷贝到哪个文件中
        KMInfoJson dependJson;
        dependJson.loadFile(dependInfo);
        if (d->m_kmOptions->m_kylin_sysapp)
        {
            depend_file_target = "files";
        }
        else if (dependJson.annotations.sysapp)
        {
            depend_file_target = "depends";
        }
        else
        {
            depend_file_target = "files";
        }

        std::string dependFiles = KMStringUtils::buildFilename(dependDeployPath, "files");
        std::string parent_dir = fs::path(d->m_filesDir).parent_path().string();
        std::string dest_file = KMStringUtils::buildFilename(parent_dir,depend_file_target);

        //创建depends文件夹
        if (!fs::exists(dest_file))
        {
            fs::create_directories(dest_file);
        }
        KMFileUtils::cpRfAllStar(KMStringUtils::buildFilename(dependFiles,"*"), dest_file);    
        all_depends.push_back(depend);
        //递归
        std::vector<std::string> depends_tmp = dependJson.annotations.depends;
        if (!depends_tmp.empty())
        {
            addDependsForApp(depends_tmp,all_depends);
        }
    }
}

void KMBuildInit::addDependsForDepend(const std::vector<std::string> &depends)
{
    std::string cacheFile = KMStringUtils::buildFilename(d->m_kmOptions->m_cacheDir, "stage-cache.json");
    nlohmann::json cache_root = KMJsonHelper::loadFile(cacheFile);
    KMJsonHelper helper(cache_root);
    for (auto const &depend : depends)
    {
        std::string dependDeployPath = getDependPath(depend);
        std::string dependFiles = KMStringUtils::buildFilename(dependDeployPath, "files");
        KMFileUtils::cpRfAllStar(KMStringUtils::buildFilename(dependFiles,"*"),d->m_filesDir);

        std::vector<std::string> dependFilesList = KMFileUtils::getDirFiles(dependFiles);
        KMStringUtils::replace(dependFilesList, dependFiles, d->m_filesDir);
        helper.setStringListValue(depend, dependFilesList);
    }
    helper.toJson(cacheFile);
}

/**
 * @brief : 解析base/runtime等的pref
 * @param : [in] pref, 格式如：top.openkylin.base/stable/2.0.0
 * @return: std::optional<KMRef>
 */ 
std::optional<KMRef> KMBuildInit::parsePref(const std::string& pref)
{
    std::optional<KMRef> ref;

    std::regex reg{ "[:/]" };
    std::vector<std::string> splits{ std::sregex_token_iterator(pref.begin(), pref.end(), reg, -1), std::sregex_token_iterator() };
    if (splits.size() != 3)
    {
        KMError("The format of base and runtime must be : ${channel}:${id}/${version}");
        return ref;
    }

    
    if (std::string errMsg; !KMBuildinsUtils::isValidAppName(splits.at(1), errMsg))
    {
        KMError(errMsg);
        return ref;
    }

    ref = {
        .id = splits.at(1),
        .channel = splits.at(0),
        .version = splits.at(2),
        .arch = "",
        .module = "",
        .branch = "",
        .kind = "",
        .baseInstallDir = ""
    };

    return ref;
}

void KMBuildInit::generateMetadata()
{
    KMTrace("KMBuildInit::generateMetadata invoke begin");

    KMInfoJson doc;
    
    doc.id = d->m_kmOptions->m_appid;
    doc.archs.push_back(d->m_kmOptions->m_arch);
    doc.kind = d->m_kmOptions->m_type;
    doc.channel = d->m_kmOptions->m_channel;
    doc.base = d->m_originalBaseRef->channel + ":" + d->m_originalBaseRef->id + "/" + d->m_originalBaseRef->version + "/" + d->m_kmOptions->m_arch;
    if (d->m_originalRuntimeRef)
    {
        doc.runtime = d->m_originalRuntimeRef->channel + ":" + d->m_originalRuntimeRef->id + "/" + d->m_originalRuntimeRef->version + "/" + d->m_kmOptions->m_arch;
    }
    else
    {
        doc.runtime = "";
    }
    doc.annotations.privilege = d->m_kmOptions->m_kylin_privilege;
    doc.annotations.sysapp = d->m_kmOptions->m_kylin_sysapp;
    if (d->m_kmOptions->m_type == BASE_TYPE_DEPEND)
    {
        doc.annotations.depends = d->m_kmOptions->m_depends;
    }

    doc.saveFile(KMStringUtils::buildFilename(d->m_kmOptions->m_buildDir, "info.json"));

    KMTrace("KMBuildInit::generateMetadata invoke end");
}

bool KMBuildInit::installDev(KMDbusClient& client, const std::string& name, const std::string& channel)
{
    std::thread eventLoopThread([&client](){
        client.run();
    });

    bool installSuccess = client.install(name, channel);
    if (installSuccess)
    {
        KMInfo(name + " Install success!");
    } 
    else 
    {
        KMError(name + " Install failed!");
    }

    client.stop();
    eventLoopThread.join();

    return installSuccess;
}
