/* app/builder/KMBuildInit.cpp
 *
 * Copyright (C) 2023 OpenKylin, Inc
 */

#include "KMBuildInit.h"

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

#include "common/KMLogger.h"
#include "common/KMOptions.h"
#include "common/kmtranslation.h"
#include "common/KMException.h"
#include "common/KMInstalledAppQuery.h"
#include "common/KMInfoJson.h"

class KMBuildInit::Options : public KMOptions
{
public:
    Options() = default;
    virtual ~Options() = default;

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

private:
    void addOptions();
    void checkOptions(const opt::variables_map &varsMap);

public:
    std::string m_arch;
    std::string m_runtime;
    std::string m_base;
    std::string m_type;
    std::string m_channel;
    bool m_help = false;

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

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

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

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

    if (m_buildDir.empty() || m_appid.empty())
    {
        KMError(_("DIRECTORY APPID must be specified"));
        showUsage();
        exit(EXIT_FAILURE);
    }

    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()
{
    m_optionsDescription = std::make_unique<opt::options_description>(_("\nUsage:\n \tkaiming build-init [OPTIONS] DIRECTORY APPID - Initialize a directory for building "
                                                                        "\nExample:\n \tkaiming build-init --base=top.openkylin.base/stable/2.0.0 --runtime=top.openkylin.runtime/stable/2.0.0 build top.openkylin.demo"
                                                                        "\n\nOptions"));

    opt::options_description helpOptions(_("Help Options"));
    helpOptions.add_options()("help,h", opt::bool_switch(&m_help), _("Show help options"));
    opt::options_description appOptions(_("Application Options"));
    appOptions.add_options()("arch", opt::value<std::string>(&m_arch)->default_value(KMUtil::kmGetArch()), _("Arch to use"))
                            ("runtime", opt::value<std::string>(&m_runtime), _("Runtime info, format : ${runtime-id}/${runtime-channel}/${runtime-version}, such as : top.openkylin.runtime/stable/2.0.0"))
                            ("base", opt::value<std::string>(&m_base), _("Base info, format : ${base-id}/${base-channel}/${base-version}, such as : top.openkylin.base/stable/2.0.0"))
                            ("kind", opt::value<std::string>(&m_type)->default_value("app"), _("Specify the build type (app, runtime, extension)"))
                            ("channel", opt::value<std::string>(&m_channel)->default_value("stable"), _("Release channel, such as (main, stable, etc.)"));

    opt::options_description positionalOptions(_("Positional Options"));
    positionalOptions.add_options()("DIRECTORY", opt::value<std::string>(&m_buildDir), _("The directory for building"));
    positionalOptions.add_options()("APPID", opt::value<std::string>(&m_appid), _("Appid"));

    m_optionsDescription->add(helpOptions).add(appOptions).add(positionalOptions);

    m_optionsPositionalDescription->add("DIRECTORY", 1);
    m_optionsPositionalDescription->add("APPID", 1);
}

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_varDir;
    std::string m_varTmpDir;
    std::string m_varRunDir;
    std::string m_metadataFile;
    std::string m_usrDir;
    std::string m_varDeployFiles;
};

REGISTER_SUBCOMMAND_DYNCREATE(build-init, KMBuildInit)

void KMBuildInit::checkBase()
{
    KMInfoJson::InfoBase appBase, runtimeBase;
    fs::path runtimePathBoot = KMStorageDir::getSystemDefaultBaseDirLocation();

    //加载baseId与baseVersion  /opt/kaiming/layers/${channel}/${arch}/${kind}/${id}/{module}/${version}/info.json
    fs::path infoPath = runtimePathBoot / d->m_runtimeRef.channel / d->m_runtimeRef.arch /d->m_runtimeRef.kind / d->m_runtimeRef.id / d->m_runtimeRef.module / d->m_runtimeRef.version /"info.json"; 

    KMInfoJson infoJson;
    infoJson.loadFile(infoPath);

    runtimeBase.m_baseString = infoJson.base;
    runtimeBase.baseParse();

    //解析appBase
    appBase.m_baseId = d->m_baseRef.id;
    appBase.m_baseVersion= d->m_baseRef.version;
    
    if (appBase.m_baseId != runtimeBase.m_baseId)
    {
        throw KMException("The appBaseId '" + appBase.m_baseId + "' and the runtimeBaseId '" + runtimeBase.m_baseId + "' is not matched" );
    }

    //版本号解析,判断version的x.y部分
    
    appBase.versionParse();
    runtimeBase.versionParse();

    if(! appBase.checkVersion(runtimeBase))
    {
        throw KMException("The appBaseVersion '" + appBase.m_baseVersion + "' and the runtimeVersion '" + runtimeBase.m_baseVersion + "' is not matched" );
    }

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

    enableVfs();
}

KMBuildInit::~KMBuildInit() { }

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

    init(argc, argv);

    int ret = run();

    KMTrace("KMBuildInit::despose 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();
    if (bases.empty())
    {
        throw KMException("No matching development base found, please install it : kaiming install " 
                        + KMUtil::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();
        if (runtimes.empty())
        {
            throw KMException("No matching development runtime found, please install it : kaiming install " 
                            + KMUtil::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要保持一致
        checkBase();
    }

    d->m_filesDir = d->m_kmOptions->m_buildDir + "/files";
    d->m_varDir = d->m_kmOptions->m_buildDir + "/var";
    d->m_varTmpDir = d->m_kmOptions->m_buildDir + "/var/tmp";
    d->m_varRunDir = d->m_kmOptions->m_buildDir + "/var/run";
    d->m_metadataFile = d->m_kmOptions->m_buildDir + "/info.json";

    if (KMUtil::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 (!KMUtil::mkpath(d->m_kmOptions->m_buildDir, ec))
    {
        throw KMException(ec.message());
    }

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

    // 创建/var/tmp目录
    if (!KMUtil::mkpath(d->m_varTmpDir, ec))
    {
        throw KMException(ec.message());
    }

    // 创建/var/run软连接
    if (!KMUtil::createDirectorySymlink("/run", d->m_varRunDir, ec))
    {
        throw KMException(ec.message());
    }

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

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

/**
     * @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::vector<std::string> splits = KMUtil::splitString(pref, "/");
    if (splits.size() != 3)
    {
        KMError("Format such as : ${id}/${channel}/${version}");
        return ref;
    }

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

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

    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.saveFile(KMUtil::buildFilename(d->m_kmOptions->m_buildDir, "info.json"));

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