#include "module.h"
#include <library/cpp/http/io/stream.h>
#include <util/network/socket.h>
#include <util/stream/file.h>

TServices20Module::TServices20Module(const TDeployManagerConfig& config)
    : Host(config.GetServices20Config().Host)
    , Port(config.GetServices20Config().Port)
    , Uri(config.GetServices20Config().Uri)
    , ClustersByCType(config.GetServices20Config().ClustersByCType)
{
    if (config.GetServices20Config().AuthKeyPath.IsDefined()) {
            Token = TUnbufferedFileInput(config.GetServices20Config().AuthKeyPath.GetPath()).ReadLine();
    }

}

NJson::TJsonValue TServices20Module::SendRequest(const TString& method, const TString& uri, const NJson::TJsonValue* data) const {
    TSocket s(TNetworkAddress(Host, Port));
    TSocketOutput so(s);
    THttpOutput output(&so);

    output.EnableKeepAlive(false);
    output.EnableCompression(false);
    TString req = " " + Uri + uri;
    if (req.back() != '/')
        req.append('/');
    req.append("?fmt=json");
    TString postData;
    if (data)
        postData = data->GetStringRobust();
    TString cl = "Content-Length: " + ToString(postData.size());
    const IOutputStream::TPart parts[] = {
        IOutputStream::TPart(method),
        IOutputStream::TPart(req),
        IOutputStream::TPart(TStringBuf(" HTTP/1.1")),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart(TStringBuf("Host: ")),
        IOutputStream::TPart(Host),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart("Accept-Encoding: application/json"),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart("Content-Type: application/json"),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart(cl),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart("Authorization: OAuth "),
        IOutputStream::TPart(Token),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart::CrLf(),
        IOutputStream::TPart(postData)
    };
    output.Write(parts, sizeof(parts) / sizeof(*parts));
    output.Finish();
    TSocketInput si(s);
    THttpInput httpIn(&si);
    TString response = httpIn.ReadAll();
    int ret = ParseHttpRetCode(httpIn.FirstLine());
    if (ret / 100 != 2)
        ythrow yexception() << ret << ": " << response;
    NJson::TJsonValue result;
    TStringInput strIn(response);
    if (!!response && !NJson::ReadJsonTree(&strIn, &result))
        ythrow yexception() << ret << ": " << response;
    return result;
}

void TServices20Module::PutDataWithSnapshotId(const TString& uri, NJson::TJsonValue& data) const {
    data["snapshot_id"] = SendRequest("GET", uri, nullptr)["_id"].GetStringRobust();
    SendRequest("PUT", uri, &data);
}

TSet<TString> TServices20Module::GetSerivesList() const {
    NJson::TJsonValue resp = SendRequest("GET", "services/?limit=10", nullptr);
    return TSet<TString>();
}

void TServices20Module::CreateSevice(const TString& name) const {
    if (!name)
        ythrow yexception() << "service name not set";
        NJson::TJsonValue req;
        req.InsertValue("id", name);
        req.InsertValue("comment", "New SAAS DM service");
        NJson::TJsonValue& infoAttrs = req["info_attrs"];
        infoAttrs.InsertValue("category", "/saas-cloud/");
        infoAttrs.InsertValue("desc", "Autogenerate service for SAAS service " + name);
    SendRequest("POST", "services", &req);
    }

void TServices20Module::SetServiceAttrsInfo(const TString& name) const {
    NJson::TJsonValue req;
    req["comment"] = "Add recipe";
    NJson::TJsonValue& attrs = req["content"];
    attrs["category"] = "/saas-cloud/";
    attrs["desc"] = "Autogenerate service for SAAS service " + name;
    NJson::TJsonValue& content = attrs["recipes"]["content"][0];
    content["context"] = NJson::JSON_ARRAY;
    content["desc"] = "Common deploy script";
    content["id"] = "common";
    content["name"] = "_activate_service_configuration.yaml";
    PutDataWithSnapshotId("services/" + name + "/info_attrs/", req);
}

void TServices20Module::SetServiceAttrsAuth(const TString& name) const {
    const char* logins[] = {"iddqd", "ivanmorozov", "svshevtsov", "amich", "marchael"};
    const char* roles[] = {"conf_managers", "observers", "ops_managers", "owners"};
    NJson::TJsonValue req;
    req["comment"] = "Add rcps";
    for (ui32 r = 0; r < Y_ARRAY_SIZE(roles); ++r) {
        NJson::TJsonValue& role = req["content"][roles[r]];
        role["groups"] = NJson::JSON_ARRAY;
        NJson::TJsonValue& loginsJson = role["logins"];
        for (ui32 l = 0; l < Y_ARRAY_SIZE(logins); ++l)
            loginsJson.AppendValue(logins[l]);
    }
    PutDataWithSnapshotId("services/" + name + "/auth_attrs/", req);
}

void TServices20Module::SetServiceAttrsRuntime(const TString& name) const {
    struct TGencfgGroup {
        TString Name;
        TString Tag;
    };

    struct TSandboxFile {
        TString LocalPath;
        TString ResourceType;
        TString TaskId;
        TString TaskType;
    };

    struct TUrlFile {
        TString LocalPath;
        TString Url;
    };

    const TSandboxFile sandboxFiles[] = {
        { "gdb_toolkit.tgz", "GDB_SEARCH_TOOLKIT", "25449542", "GDB_SEARCH_TOOLKIT" },
        { "rtyserver", "RTYSERVER", "35179245", "RTYSERVER" },
        { "loop-httpsearch", "INSTANCECTL", "27642694", "INSTANCECTL" }
    };

    const TUrlFile urlFiles[] = {
        { "apache.ywsearch.cfg", "http://cmsearch.yandex.ru/res/saas/updater/loop.conf" },
        { "daemon.conf", "http://cmsearch.yandex.ru/res/saas/updater/daemon.conf" },
        { "httpsearch", "http://cmsearch.yandex.ru/res/saas/common/rtyserver.conf-default" },
        { "dict.dict", "http://cmsearch.yandex.ru/res/saas/common/dict.1589842/dict.dict" },
        { "loop.conf", "http://cmsearch.yandex.ru/res/saas/updater/loop.conf" }
    };

    NJson::TJsonValue req;
    req["comment"] = "Add binaries";
    NJson::TJsonValue& content = req["content"];
    content["engines"]["engine_type"] = "BSCONFIG";
    NJson::TJsonValue& instances = content["instances"];
    instances["chosen_type"] = "ALLOCATIONS";
    instances["instance_list"] = NJson::JSON_ARRAY;
    instances["gencfg_groups"] = NJson::JSON_ARRAY;
    NJson::TJsonValue& resources = content["resources"];
    resources["static_files"] = NJson::JSON_ARRAY;
    for (ui32 s = 0; s < Y_ARRAY_SIZE(sandboxFiles); ++s) {
        NJson::TJsonValue& file = resources["sandbox_files"].AppendValue(NJson::JSON_MAP);
        file["local_path"] = sandboxFiles[s].LocalPath;
        file["resource_type"] = sandboxFiles[s].ResourceType;
        file["task_id"] = sandboxFiles[s].TaskId;
        file["task_type"] = sandboxFiles[s].TaskType;
    }

    for (ui32 u = 0; u < Y_ARRAY_SIZE(urlFiles); ++u) {
        NJson::TJsonValue& file = resources["url_files"].AppendValue(NJson::JSON_MAP);
        file["local_path"] = urlFiles[u].LocalPath;
        file["url"] = urlFiles[u].Url;
    }
    PutDataWithSnapshotId("services/" + name + "/runtime_attrs/", req);
}

void TServices20Module::SetServiceAttrs(const TString& name) const {
    SetServiceAttrsInfo(name);
    SetServiceAttrsAuth(name);
    SetServiceAttrsRuntime(name);
}

TServices20Module::TAllocatedSlot TServices20Module::CreateOneSlotAllocation(const TString& datacenterAlias, const TString& service, const TString& ctype) const {
    NJson::TJsonValue req;
    auto ct = ClustersByCType.find(ctype);
    if (ct == ClustersByCType.end())
        ythrow yexception() << "there is no cluster for ctype " << ctype;
    auto cluster = ct->second.find(datacenterAlias);
    if (cluster == ct->second.end())
        ythrow yexception() << "unknown location " << datacenterAlias;
    req["cluster"] = cluster->second;
    req["instances_count"] = 1;
    req["max_instances_per_host"] = 1;
    req["cpu"] = 2000;
    req["memory"] = 24 * 1024;
    req["disk"] = 100;
    req["ipv4_address_required"] = false;
    req["ipv6_address_required"] = false;
    req["allocate_ports_in_block"] = true;
    const char* reqPorts[] = { "search", "basesearch", "index", "controller" };
    NJson::TJsonValue& ports = req["ports"];
    for (ui32 p = 0; p < Y_ARRAY_SIZE(reqPorts); ++p) {
        NJson::TJsonValue& port = ports.AppendValue(NJson::JSON_MAP);
        port["name"] = reqPorts[p];
        port["protocol"] = "TCP";
    }
    NJson::TJsonValue allocation = SendRequest("POST", "services/" + service + "/allocations/", &req);
    TAllocatedSlot result;
    result.Id = allocation["_id"].GetStringRobust();
    const NJson::TJsonValue& instances = allocation["instances"];
    if (instances.GetArray().size() != 1)
        ythrow yexception() << "invalid allocation: allocated " << instances.GetArray().size() << " slots";
    for (const auto& instace : instances.GetArray()) {
        result.Host = instace["host"].GetStringRobust();
        TVector<ui16> ports;
        for (const auto& p : instace["ports"].GetArray())
            ports.push_back(p["port"].GetUIntegerRobust());
        if (ports.size() != 4)
            ythrow yexception() << "invalid allocation: allocated " << ports.size() << " ports";
        Sort(ports.begin(), ports.end());
        result.SearchPort = ports[0];
        result.BaseSearchPort = ports[1];
        result.IndexPort = ports[2];
        result.ControllerPort = ports[3];
    }
    NJson::TJsonValue serviceRuntime = SendRequest("GET", "services/" + service + "/runtime_attrs/", nullptr);
    serviceRuntime["comment"] = "add slot " + result.Host + ":" + ToString(result.SearchPort) + " (" + result.Id + ")";
    serviceRuntime["content"]["instances"]["allocation_ids"].AppendValue(result.Id);
    serviceRuntime["snapshot_id"] = serviceRuntime["_id"];
    SendRequest("PUT", "services/" + service + "/runtime_attrs/", &serviceRuntime);
    return result;
}

void TServices20Module::RemoveAllocation(const TString& id, const TString& service) const {
    NJson::TJsonValue serviceRuntime = SendRequest("GET", "services/" + service + "/runtime_attrs/", nullptr);
    NJson::TJsonValue newIds(NJson::JSON_ARRAY);
    for(const auto& allocId : serviceRuntime["content"]["instances"]["allocation_ids"].GetArray())
        if (allocId.GetStringRobust() != id)
            newIds.AppendValue(allocId);
    serviceRuntime["content"]["instances"]["allocation_ids"] = newIds;
    serviceRuntime["snapshot_id"] = serviceRuntime["_id"];
    serviceRuntime["comment"] = "remove allocation " + id;
    SendRequest("PUT", "services/" + service + "/runtime_attrs/", &serviceRuntime);
    SendRequest("DELETE", "services/" + service + "/allocations/" + id, nullptr);
}

void TServices20Module::Deploy(const TString& service) const {
    NJson::TJsonValue req;
    req["type"] = "SET_SNAPSHOT_STATE";
    NJson::TJsonValue& content = req["content"];
    content["comment"] = "deploy new version";
    content["is_enebled"] = true;
    content["recipe"] = "common";
    content["state"] = "ACTIVE";
    content["snapshot_id"] = SendRequest("GET", "services/" + service + "/runtime_attrs/", nullptr)["_id"].GetStringRobust();
    ythrow yexception() << SendRequest("POST", "services/" + service + "/events/", &req).GetStringRobust();
}

bool TServices20Module::CheckDeployComplite(const TString& /*service*/, const TString& /*id*/) const {
    return true;
}
