#include "graph.h"
#include "embed.h"

#include <apphost/lib/compression/compression.h>
#include <apphost/lib/compression/compression_codecs.h>

#include <util/string/split.h>

static bool AddNode(NSv::NAppHost::TGraph& g, const YAML::Node& node, NSv::TAuxData& aux) {
    CHECK_NODE(node, node.IsScalar() || (node.IsMap() && node.size() >= 1), "nodes must be maps");
    TStringBuf name = node.IsScalar() ? node.Scalar() : node.begin()->first.Scalar();
    CHECK_NODE(node, name, "node names should be non-empty strings");
    CHECK_NODE(node, g.Index.emplace(name, g.Nodes.size()).second, "duplicate node " << name);
    g.Nodes.push_back({
        .Name = TString(name),
        .Visits       = aux.Signal(TString::Join(name, "-visits_dmmm")),
        .CriticalPath = aux.Signal(TString::Join(name, "-crit-path_dmmm")),
        .RequestSize  = aux.Signal<NSv::THistogram>(TString::Join(name, "-req-size_dhhh")),
        .ResponseSize = aux.Signal<NSv::THistogram>(TString::Join(name, "-rsp-size_dhhh")),
        .IsInput = node.IsScalar(),
	    .IsTerminal = (name == "RESPONSE"),
    });
    if (node.IsMap() && node.size() > 1) {
        YAML::Node args;
        // `YAML::Clone` erases location info, making config parse errors look weird.
        for (auto it = node.begin(); ++it != node.end();) {
            args[it->first] = it->second;
        }
        if (aux.CollectingSignals()) {
            args.begin()->first.SetTag("!" + std::string(name));
        }
        if (EqualToOneOf(args.begin()->first.Scalar(), "proxy", "apphost-to-http")) {
#define DEFAULT(k, v) if (!args[k]) {args[k] = YAML::Load(v);}
            DEFAULT("attempts", "2");
            DEFAULT("attempts-fast", "5");
            DEFAULT("sync", "true");
            DEFAULT("retry-codes", "{503: fast, 5xx: normal}");
            DEFAULT("send-buffer", "524288");
            DEFAULT("recv-buffer", "524288");
            DEFAULT("override", "x-apphost-srcrwr");
#undef DEFAULT
        }
        auto& n = g.Nodes.back();
        n.Action   = args.size() > !!args["streaming"] ? aux.Action(args) : NSv::TAction{};
        n.Handler  = NSv::Optional<TString>(args["path"], "/", [](auto& p) { return p.StartsWith("/"); });
        n.Grpc     = NSv::Optional(args["grpc"], false);
        n.StreamIn = NSv::Optional(args["streaming"], false);
    }
    return true;
}

static void AddInput(NSv::NAppHost::TGraph& g, size_t to, const YAML::Node& dep, NSv::NAppHost::TExpression expr = {}) {
    if (dep.Tag().size() > 1) {
        CHECK_NODE(dep, dep.IsMap(), "constant items must be maps");
        // Embeds do not increase indegree because they are always ready.
        g.Nodes[to].I.emplace_back(g.Edges.size());
        g.Edges.push_back({
            .Source = g.Nodes.size() + g.Embeds.size(),
            .Target = to,
            .Expr = std::move(expr)
        });
        g.Embeds.emplace_back();
        // TODO source names (for direct inclusion as inputs of streaming subgraphs)?
        auto type = TStringBuf(dep.Tag()).Skip(1);
        auto data = type.SkipPrefix("proto!") ? "p_" + NSv::NAppHost::YamlToProto(dep) : NSv::NAppHost::YamlToJson(dep);
        auto item = g.Embeds.back().AddAnswers();
        item->SetType(TString(type));
        item->SetData(NAppHost::NCompression::Encode(data, NAppHost::NCompression::TCodecs::Default));
        return;
    }

    CHECK_NODE(dep, dep.IsScalar(), "node names must be scalars");
    TStringBuf name = dep.Scalar();
    TStringBuf rename;
    TStringBuf types;
    TVector<std::pair<TString, TString>> parsedTypes;
    auto filter = name.SkipPrefix("^") ? NSv::NAppHost::TEdge::FirstOfType
                : name.SkipPrefix("$") ? NSv::NAppHost::TEdge::LastOfType : NSv::NAppHost::TEdge::AllItems;
    bool negate = false;
    if (name.TrySplit("@", name, types)) {
        for (TStringBuf typeName : StringSplitter(types).Split(',')) {
            negate |= typeName.StartsWith("-");
        }
        for (TStringBuf typeName : StringSplitter(types).Split(',')) {
            TStringBuf typeRename;
            CHECK_NODE(dep, !typeName.TrySplit("->", typeName, typeRename) || !negate, "@-A->B is not a valid syntax");
            CHECK_NODE(dep, typeName.SkipPrefix("-") == negate, "@A,-B is not a valid syntax");
            parsedTypes.emplace_back(typeName, typeRename);
        }
        std::sort(parsedTypes.begin(), parsedTypes.end());
    }
    name.TrySplit("->", name, rename);

    auto it = g.Index.find(name);
    CHECK_NODE(dep, it != g.Index.end(), "invalid node name");
    g.Nodes[to].I.emplace_back(g.Edges.size());
    g.Nodes[it->second].O.emplace_back(g.Edges.size());
    g.Edges.push_back({
        .Source = it->second,
        .Target = to,
        .Expr = std::move(expr),
        .Rename = TString(rename),
        .Types = std::move(parsedTypes),
        .Filter = filter,
        .Negate = negate
    });
}

static NSv::NAppHost::TExpression MakeExpression(NSv::NAppHost::TGraph& g, const YAML::Node& expr, TMaybe<size_t> edge = {}) {
    auto resolver = [&](TStringBuf node, TStringBuf item) {
        auto it = g.Index.find(node);
        CHECK_NODE(expr, it != g.Index.end(), "unknown node name " << node);
        auto id = g.ItemIndex.emplace(std::make_pair(it->second, item), g.Items.size()).first->second;
        g.Items.resize(g.ItemIndex.size());
        if (edge) {
            g.Items[id].push_back(*edge);
        }
        return id;
    };
    return {expr, resolver};
}

NSv::NAppHost::TGraph::TGraph(const YAML::Node& nodes, NSv::TAuxData& aux) {
    CHECK_NODE(nodes, nodes.IsSequence(), "argument must be a sequence of nodes");
    for (const auto& node : nodes) {
        AddNode(*this, node, aux);
    }

    auto response = Index.find("RESPONSE");
    CHECK_NODE(nodes, response != Index.end(), "RESPONSE is not defined");
    ResponseId = response->second;

    for (size_t i = 0; i < nodes.size(); i++) {
        if (Nodes[i].IsInput) {
            continue;
        }
        auto list = nodes[i].begin()->second;
        CHECK_NODE(list, list.IsSequence(), "dependencies must be a sequence");
        for (const auto& dep : list) {
            if (dep.IsMap() && dep.size() == 1 && dep.Tag().size() <= 1) {
                AddInput(*this, i, dep.begin()->first, MakeExpression(*this, dep.begin()->second, Edges.size()));
            } else {
                AddInput(*this, i, dep);
            }
        }
        if (const auto& async = nodes[i]["async"]) {
            Nodes[i].Async = MakeExpression(*this, async);
            Nodes[i].IsTerminal = true;
        }
    }

    for (size_t i = 0; i < nodes.size(); i++) {
        CHECK_NODE(nodes[i], Nodes[i].IsTerminal || Nodes[i].O, "node not used for RESPONSE or any async nodes");
        Nodes[i].NeedOutput = Nodes[i].O || ItemIndex.lower_bound({i, 0}) != ItemIndex.lower_bound({i + 1, 0});
    }

    // FIXME ugly
    auto dfs = [&, visited = TVector<bool>(Nodes.size()), path = TVector<size_t>()](auto&& dfs, size_t i) mutable {
        if (auto cycle = std::find(path.begin(), path.end(), i); cycle != path.end()) {
            TStringBuilder error;
            for (; cycle != path.end(); cycle++) {
                error << Nodes[*cycle].Name << " -> ";
            }
            FAIL_NODE(nodes, "dependency cycle: " << error << Nodes[i].Name);
        }
        if (visited[i]) {
            return;
        }
        visited[i] = true;
        path.push_back(i);
        for (size_t edge : Nodes[i].O) {
            dfs(dfs, Edges[edge].Target);
        }
        path.pop_back();
    };
    for (size_t i = 0; i < Nodes.size(); i++) {
        dfs(dfs, i);
    }
}
