#include "graph.h"
#include "stream.h"

#include <library/cpp/eventlog/events_extension.h>

#include <util/generic/array_ref.h>
#include <util/generic/scope.h>
#include <util/random/random.h>
#include <util/string/split.h>

using namespace NSv::NAppHost;

namespace {
    struct TNodeState {
        // Merged contents of all chunks. (There's only one chunk for now.)
        TResponse Data;
        // A request stream passed to the node's action, if it has one. Only set
        // while the task for that action is executing.
        std::shared_ptr<TStream> Stream;
        // Backend list overrides, in the format expected by `proxy`'s `override` option.
        // Parsed from `app_host_params`, forwarded to the action as `x-apphost-srcrwr`.
        TVector<TString> Backends;
        // Something that could be used for hashing load balancing modes, normally a number.
        // Nodes provide hints for other nodes in their responses. Forwarded as `x-apphost-hint`.
        TString Hint;
        // Number of input edges still not evaluated.
        size_t Wait = 0;
        // Index of the node that was the last to trigger an edge. Traverse these pointers
        // from RESPONSE to reconstruct the critical path.
        size_t Prev = 0;
        // Whether this node should not be evaluated. Parsed from `app_host_params`.
        bool Skip = false;
        // Whether this node has already finished evaluating.
        bool Done = false;
    };

    struct TGraphState {
        NSv::TLogFrame Log;
        // Number of *terminal* nodes yet to finish. Non-terminal nodes are not counted
        // because they can be cancelled if all terminal ones are already done.
        size_t Remaining = 0;
        // Stuff that should be added to requests to each node, or to the first chunk
        // for streaming nodes. Contains the request GUID and `app_host_params` items.
        TRequest Params;
        // *The* response, or the unsent part of it.
        TResponse Response;
        // Whether `dump=eventlog` has been set in `app_host_params`.
        bool DumpLogs = false;
        // Search request ID from `app_host_params`. Was supposed to be replaced by the GUID...
        TString ReqId;
        // Per-node state.
        TVector<TNodeState> Nodes;
        // Current values of all variables used by edge expressions.
        TVector<TTernary> Flags;
        // Whether an edge has already been visited. While normally edges are visited
        // exactly when the source node finishes, sometimes they have conditions attached
        // that become `false` earlier; in that case, the edge is visited right away
        // because we don't need a node's output to transfer exactly nothing.
        TVector<bool> EdgeDone;
        // With dump=eventlog, the contents of `Log` obtained so far.
        TVector<std::tuple<TInstant, ui32, ui32, TString, bool>> LogTrace;
        // Emitted when a terminal node is done. Check whether it's the RESPONSE (and
        // send it to the client) or the last terminal node (destroy the state).
        cone::event OnTerminal;
        // Coroutines that evaluate actions for nodes that have them.
        cone::mguard Tasks;

        TGraphState(const TGraph& g)
            : Nodes(g.Nodes.size())
            , Flags(g.Items.size())
            , EdgeDone(g.Edges.size())
        {
            for (size_t i = 0; i < Nodes.size(); i++) {
                Nodes[i].Wait = g.Nodes[i].I.size();
                Remaining += g.Nodes[i].IsTerminal;
            }
        }
    };
}

// Construct a tracer function for dump=eventlog. To actually write `TResponse::LogLines`
// for apphost or _STATS for report, use the `SaveLog` function.
static NSv::TLogFrame::TTracer LogTracer(TGraphState& s) {
    return [&, evs = NProtoBuf::TEventFactory::Instance()](TInstant t, ui32 ctx, ui32 id, const NProtoBuf::Message& m) {
        TStringBuilder buf;
        evs->PrintEvent(m.GetDescriptor()->options().GetExtension(NSv::message_id), &m, buf.Out);
        s.LogTrace.emplace_back(t, ctx, id, std::move(buf), m.GetDescriptor() == NSv::NEv::TFrameEnd::descriptor());
    };
}

// Add log lines for all events that are direct descendants of a particular one. The log
// format mimics tools/logdump with -t, except without the "parent" and "id" fields.
static void SaveLogSubtree(TGraphState& s, ui32 ctx, int level) {
    auto a = std::lower_bound(s.LogTrace.begin(), s.LogTrace.end(), ctx, [](auto& it, ui32 c) {
        return std::get<1>(it) < c;
    });
    auto b = std::upper_bound(s.LogTrace.begin(), s.LogTrace.end(), ctx, [](ui32 c, auto& it) {
        return c < std::get<1>(it);
    });
    for (; a != b; a++) {
        auto& [t, ctx, id, m, isEnd] = *a;
        TStringOutput out(*s.Response.AddLogLines());
        out << t.MicroSeconds() << '\t';
        for (int i = level; i --> 0;) {
            out << (i ? "| " : isEnd ? "\\-" : "+-");
        }
        out << m;
        if (id) {
            SaveLogSubtree(s, id, level + 1);
        }
    }
}

static void SaveLog(TGraphState& s) {
    if (!s.LogTrace) {
        return;
    }
    std::sort(s.LogTrace.begin(), s.LogTrace.end(), [](auto& a, auto& b) {
        return std::tie(std::get<1>(a), std::get<0>(a)) < std::tie(std::get<1>(b), std::get<0>(b));
    });
    SaveLogSubtree(s, std::get<1>(s.LogTrace[0]), 0);
    // Unfortunately, in `dump=eventlog` mode report does not read `LogLines`, but
    // instead wants a JSON item with source name = _STATS. (It has no type, yeah.)
    NJsonWriter::TBuf meta{NJsonWriter::HEM_UNSAFE};
    meta.BeginObject().WriteKey("events").BeginList();
    for (auto& line : s.Response.GetLogLines()) {
        meta.WriteString(line);
    }
    meta.EndList().EndObject();
    auto stats = s.Response.AddAnswers();
    stats->SetSourceName("_STATS");
    stats->SetData(NAppHost::NCompression::Encode(meta.Str(), NAppHost::NCompression::TCodecs::Default));
}

static void HandleParams(const TGraph& g, TGraphState& s, const TAnswer& item) {
    // TODO encode as a TRequest field & parse that instead if present?
    NSv::NAppHost::DecodeJson(item, [&](const NJson::TJsonValue& p) {
        if (auto reqid = p["reqid"]; reqid.IsString()) {
            s.ReqId = reqid.GetString();
        }
        for (const auto& src : p["srcrwr"].GetMap()) {
            // TODO might be a backend's name; should somehow detect that a node's action
            //      is something like `proxy: *X` and have `X` as an srcrwr alias for it.
            if (auto id = g.Index.FindPtr(src.first)) {
                // TODO might be `srcrwr=X:Y` where `Y` is either another node or a backend;
                //      don't have any way to fetch config-defined backend lists here...
                StringSplitter(src.second.GetString()).Split(';').Collect(&s.Nodes[*id].Backends);
            }
        }
        for (const auto& name : p["srcskip"].GetArray()) {
            if (auto id = g.Index.FindPtr(name.GetString())) {
                // Marking inbound edges as "done" instead of outbound ones is better for two
                // reasons: 1. it requires less bookkeeping to remember which nodes we should start
                // after parsing `app_host_params` (the `Skip` flag is needed anyway for RESPONSE
                // reachability checks); 2. this way the values for the skipped node's flags are
                // set before all of its dependencies are done (but see a TODO in `OnOutputDone`).
                for (size_t edge : g.Nodes[*id].I) {
                    s.EdgeDone[edge] = true;
                }
                s.Nodes[*id].Wait = 0;
                s.Nodes[*id].Skip = true;
            }
        }
        if (auto dump = p["dump"].GetString(); dump == "eventlog") {
            s.DumpLogs = true;
        }
        // TODO "graphrwr": {"name": "another-name"}; keep track of action names when creating
        //      `TGraph`, then at param parsing time call `aux.Action("another-name")` and
        //      mark all nodes with action "name" overriden. Careful: `aux.Action` may return
        //      an empty function if that action does not exist or is not config-defined.
    });
    // These parameters are always sent to every node.
    *s.Params.AddAnswers() = std::move(item);
}

static TString EdgeName(const TGraph& g, const TEdge& e) {
    return e.Rename ? e.Rename : e.Source < g.Nodes.size() ? g.Nodes[e.Source].Name : "";
}

template <typename T>
static void CopyItem(const TGraph& g, const TEdge& e, const TAnswer& item, T& out, const TString& type = {}) {
    auto& to = *out.AddAnswers();
    if constexpr (std::is_same<std::decay_t<T>, TRequest>::value) {
        // This tagging is used by subgraphs to separate inputs into nodes.
        to.SetSourceName(EdgeName(g, e));
    }
    to.SetType(type ? type : item.GetType());
    to.SetData(item.GetData());
};

template <typename T>
static void CopyAll(const TGraph& g, TGraphState& s, const TEdge& e, T& out) {
    auto& storage = e.Source >= g.Nodes.size() ? g.Embeds[e.Source - g.Nodes.size()] : s.Nodes[e.Source].Data;
    auto& items = storage.GetAnswers();
    auto typeIt = e.Types.begin();
    for (auto itemIt = items.begin(), nextIt = itemIt; itemIt != items.end(); itemIt = nextIt) {
        nextIt = std::find_if(itemIt, items.end(), [&](auto& item) {
            return item.GetType() != itemIt->GetType();
        });
        typeIt = std::find_if(typeIt, e.Types.end(), [&](auto& type) {
            return type.first >= itemIt->GetType();
        });
        auto a = e.Filter == TEdge::LastOfType ? nextIt - 1 : itemIt;
        auto b = e.Filter == TEdge::FirstOfType ? itemIt + 1 : nextIt;
        for (; a != b; a++) {
            if (e.Types && !e.Negate) {
                // The type might be listed several times, e.g. `A@x->y,x->z`.
                for (auto it = typeIt; it != e.Types.end() && it->first == a->GetType(); it++) {
                    CopyItem(g, e, *a, out, it->second);
                }
            } else if (typeIt == e.Types.end() || typeIt->first != a->GetType()) {
                CopyItem(g, e, *a, out);
            }
        }
    }
    // XXX not sure what the real apphost does. Sends flags if there is at least one item?
    if (e.Types && !e.Negate) {
        return;
    }
    for (const auto& flag : storage.GetFlags()) {
        if constexpr (std::is_same<std::decay_t<T>, TRequest>::value) {
            auto meta = out.AddMetaFlags();
            meta->SetSourceName(EdgeName(g, e));
            meta->SetFlagName(flag);
        } else {
            out.AddFlags(flag);
        }
    }
};

template <typename T>
static void CopyAll(const TGraph& g, TGraphState& s, TArrayRef<const size_t> edges, T& out) {
    for (size_t edge : edges) {
        if (!g.Edges[edge].Expr(s.Flags, true).IsFalse()) {
            CopyAll(g, s, g.Edges[edge], out);
        }
        if constexpr (std::is_same<std::decay_t<T>, TRequest>::value) {
            // FIXME should only do this if there are no untransferred edges with the same name.
            //       See `StreamingRenamed` in tests/apphost.
            out.AddDone(EdgeName(g, g.Edges[edge]));
        }
    }

    // Avoid multiplying flags; the number of each doesn't matter.
    if constexpr (std::is_same<std::decay_t<T>, TRequest>::value) {
        auto flags = out.MutableMetaFlags();
        std::sort(flags->begin(), flags->end(), [](auto& a, auto& b) {
            return std::tie(a.GetSourceName(), a.GetFlagName())
                 < std::tie(b.GetSourceName(), b.GetFlagName());
        });
        flags->erase(std::unique(flags->begin(), flags->end(), [](auto& a, auto& b) {
            return std::tie(a.GetSourceName(), a.GetFlagName())
                == std::tie(b.GetSourceName(), b.GetFlagName());
        }), flags->end());
    } else {
        auto flags = out.MutableFlags();
        std::sort(flags->begin(), flags->end());
        flags->erase(std::unique(flags->begin(), flags->end()), flags->end());
    }
}

// Whether a node's response is/could be (transitively) used for RESPONSE or any async node.
static bool ShouldVisit(const TGraph& g, TGraphState& s, size_t i, bool root = true) {
    if (s.Nodes[i].Skip) {
        return false;
    }
    auto& node = g.Nodes[i];
    for (size_t edge : node.O) {
        if (!g.Edges[edge].Expr(s.Flags).IsFalse() && ShouldVisit(g, s, g.Edges[edge].Target, false)) {
            return true;
        }
    }
    // 1. If already started streaming, finish the request.
    // 2. For async nodes, coalesce unknown flag values if we're ready to begin
    //    evaluation right away. Otherwise, unknown-state nodes might still be needed.
    return s.Nodes[i].Stream || (node.IsTerminal && !node.Async(s.Flags, root).IsFalse());
}

static void OnOutputDone(const TGraph& g, TGraphState& s, size_t i, bool success);

template <typename T>
static void MoveMergeFrom(T& target, T&& source) {
    // Do protobuf messages *really* have no method to check emptiness?
    if (target.ByteSizeLong()) {
        // There's no `MergeFrom(T&&)` either, only `MergeFrom(const T&)` :(
        target.MergeFrom(std::move(source));
    } else {
        target = std::move(source);
    }
}

static bool OnInputReady(const TGraph& g, TGraphState& s, size_t i, TArrayRef<const size_t> edges) {
    auto& node = g.Nodes[i];
    if (!node.Action) {
        // TODO forward data to dependents?..
        if (node.NeedOutput) {
            CopyAll(g, s, edges, s.Nodes[i].Data);
        }
        if (i == g.ResponseId) {
            CopyAll(g, s, edges, s.Response), s.OnTerminal.wake();
        }
    } else if (auto& stream = s.Nodes[i].Stream) {
        auto& request = stream->GetBuffer();
        auto oldSize = request.ByteSizeLong();
        CopyAll(g, s, edges, request);
        if (auto newSize = request.ByteSizeLong(); newSize > oldSize) {
            // TODO count the complete request's size instead?
            node.RequestSize[newSize - oldSize]++;
        }
    } else {
        TRequest buffer;
        CopyAll(g, s, edges, buffer);
        // XXX kind-of apphost compatibility: do not send a request if there are no input *items*.
        //     There are still flags and notifications of completion, though, which is why
        //     we send them to streaming sources.
        if (g.Nodes[i].StreamIn ? !buffer.ByteSizeLong() : !buffer.AnswersSize()) {
            return false;
        }
        auto ruid = RandomNumber<ui32>();
        stream = NSv::NAppHost::Stream(node.Handler, node.Grpc, s.Nodes[i].Hint, s.Nodes[i].Backends,
            [&g, &s, i](TResponse&& chunk) {
                for (const auto& hint : chunk.GetHints()) {
                    if (auto* it = g.Index.FindPtr(hint.GetSourceName())) {
                        s.Nodes[*it].Hint = ToString(hint.GetValue());
                    }
                }
                for (const auto& line : chunk.GetLogLines()) {
                    s.Nodes[i].Stream->Log().Push<NSv::NEv::TAppHostNodeLog>(line);
                }
                // TODO split into streamed parts, like the input?
                g.Nodes[i].ResponseSize[chunk.ByteSizeLong()]++;
                if (g.Nodes[i].NeedOutput) {
                    if (i == g.ResponseId) {
                        s.Response.MergeFrom(chunk);
                        s.OnTerminal.wake();
                    }
                    MoveMergeFrom(s.Nodes[i].Data, std::move(chunk));
                } else if (i == g.ResponseId) {
                    MoveMergeFrom(s.Response, std::move(chunk));
                    s.OnTerminal.wake();
                }
                // And maybe set some flags here?
                return true;
            },
            [&g, &s, i]() {
                OnOutputDone(g, s, i, true);
                return true;
            },
            s.Log.Fork<NSv::NEv::TAppHostNodeVisit>(node.Name, ruid));
        auto& request = stream->GetBuffer() = std::move(buffer);
        // NOTE real apphost adds params to every chunk -- not sure if necessary.
        request.MergeFrom(s.Params);
        request.SetRuid(ruid);
        request.SetPath(node.Handler);
        node.Visits++;
        node.RequestSize[request.ByteSizeLong()]++;
        s.Tasks.add([&g, &s, i, &node, stream = NSv::IStreamPtr(stream)]() mutable {
            if ((node.Action(stream) || mun_errno != ECANCELED) && !s.Nodes[i].Done) {
                OnOutputDone(g, s, i, false);
            }
            return true;
        });
    }
    return true;
}

static void OnInputDone(const TGraph& g, TGraphState& s, size_t i) {
    auto& node = g.Nodes[i];
    if (!ShouldVisit(g, s, i)) {
        s.Log.Push<NSv::NEv::TAppHostNodeSkip>(node.Name);
        // For purposes of edge expressions, nodes skipped due to RESPONSE reachability
        // checks behave as if they failed. This includes transparent nodes, which
        // otherwise never fail.
        OnOutputDone(g, s, i, false);
    } else if (node.StreamIn ? g.Nodes[i].Action && !s.Nodes[i].Stream : !OnInputReady(g, s, i, node.I)) {
        s.Log.Push<NSv::NEv::TAppHostNodeEmptyRequest>(node.Name);
        OnOutputDone(g, s, i, false);
    } else if (!g.Nodes[i].Action) {
        // Kind of inconsistent with non-transparent nodes...I guess the point of the
        // "visits" signal for transparent nodes is how many times it was useful.
        node.Visits++;
        s.Log.Push<NSv::NEv::TAppHostTransparent>(node.Name);
        OnOutputDone(g, s, i, true);
    } else {
        s.Nodes[i].Stream->CloseBuffer();
    }
}

static void OnEdgeDone(const TGraph& g, TGraphState& s, size_t edge, size_t lastVisited) {
    size_t target = g.Edges[edge].Target;
    // Mark edge as done before evaluating any nodes so that their flags cannot
    // have a retroactive effect and evaluate the edge once more. (Though an edge
    // with a condition that depends on a node that uses the edge is weird.)
    s.EdgeDone[edge] = true;
    s.Nodes[target].Prev = lastVisited;
    if (g.Nodes[target].StreamIn && ShouldVisit(g, s, target)) {
        OnInputReady(g, s, target, {edge});
    }
    if (--s.Nodes[target].Wait == 0) {
        OnInputDone(g, s, target);
    }
}

static void OnOutputDone(const TGraph& g, TGraphState& s, size_t i, bool success) {
    if (s.Nodes[i].Stream) {
        s.Nodes[i].Stream->Log().Push<NSv::NEv::TAppHostNodeDone>(success);
    }
    s.Nodes[i].Done = true;
    s.Nodes[i].Stream.reset();
    if (g.Nodes[i].IsTerminal && --s.Remaining == 0) {
        s.OnTerminal.wake();
    }

    auto& flags = *s.Nodes[i].Data.MutableFlags();
    auto& items = *s.Nodes[i].Data.MutableAnswers();
    std::sort(flags.begin(), flags.end());
    std::stable_sort(items.begin(), items.end(), [](auto& a, auto& b) {
        return a.GetType() < b.GetType();
    });
    auto flagIt = flags.begin();
    auto itemIt = items.begin();
    TVector<bool> reeval(g.Edges.size());
    for (auto it = g.ItemIndex.lower_bound({i, ""}); it != g.ItemIndex.end() && it->first.first == i; it++) {
        const auto& name = it->first.second;
        flagIt = std::find_if(flagIt, flags.end(), [&](const auto& flag) {
            return flag >= name;
        });
        itemIt = std::find_if(itemIt, items.end(), [&](const auto& item) {
            return item.GetType() >= name;
        });
        // XXX maybe give that flag a better name?
        s.Flags[it->second] = name == "noans" ? !success
                            : (flagIt != flags.end() && *flagIt == name) ||
                              (itemIt != items.end() && itemIt->GetType() == name);
        for (size_t edge : g.Items[it->second]) {
            // Embeds are ready from the beginning, so attempting to early-evaluate
            // edge conditions attached to them is a waste of time.
            reeval[edge] = g.Edges[edge].Source < g.Nodes.size();
        }
    }
    // While normally it's not a good idea to iterate over all edges, the graphs
    // we have here are unlikely to be big, for a computer at least.
    for (size_t edge = 0; edge < reeval.size(); edge++) {
        // TODO recheck RESPONSE reachability for source nodes. Consider this subgraph:
        //          A -+---> B ---> C --- ... -+-> RESPONSE
        //              \--> D --- ...  ------/
        //      If the edge B->C is always false due to a flag which has just been set,
        //      then B is not needed for RESPONSE and will not be visited; all of its
        //      flags except `noans` will be false, but this will not be recorded
        //      in `Flags` until A completes.
        if (reeval[edge] && !s.EdgeDone[edge] && g.Edges[edge].Expr(s.Flags).IsFalse()) {
            OnEdgeDone(g, s, edge, i);
        }
    }
    for (size_t edge : g.Nodes[i].O) {
        if (!s.EdgeDone[edge]) {
            OnEdgeDone(g, s, edge, i);
        }
    }
}

static bool Respond(const TGraph& g, TGraphState& s, TInterface& ah) {
    for (bool first = true; auto chunk = ah.Read(); first = false) {
        for (auto& item : *chunk->MutableAnswers()) {
            if (item.GetType() == "app_host_params") {
                if (first) {
                    HandleParams(g, s, item);
                }
            } else if (auto id = g.Index.FindPtr(item.GetSourceName())) {
                // Reject attempts to add items to inputs that were already marked
                // as "done". Must be a bug in whatever created the request.
                if (g.Nodes[*id].IsInput && !s.Nodes[*id].Done) {
                    *s.Nodes[*id].Data.AddAnswers() = std::move(item);
                }
            }
        }
        for (auto& meta : *chunk->MutableMetaFlags()) {
            if (auto id = g.Index.FindPtr(meta.GetSourceName())) {
                if (g.Nodes[*id].IsInput && !s.Nodes[*id].Done) {
                    *s.Nodes[*id].Data.AddFlags() = std::move(meta.GetFlagName());
                }
            }
        }
        if (first) {
            s.Params.SetGuid(chunk->GetGuid());
            // When nested, the parent action will dump this log subtree as well.
            s.Log = s.DumpLogs && ah.IsRoot()
                ? ah.Log().ForkTracing<NSv::NEv::TAppHostGraph>(LogTracer(s), chunk->GetGuid(), chunk->GetRuid(), s.ReqId)
                : ah.Log().Fork<NSv::NEv::TAppHostGraph>(chunk->GetGuid(), chunk->GetRuid(), s.ReqId);
            // Now that we have app_host_params, we can finally start everything that
            // does not depend on inputs at all and send direct embeds to streaming nodes.
            for (size_t e = 0; e < g.Edges.size(); e++) {
                if (g.Edges[e].Source >= g.Nodes.size() && !s.EdgeDone[e]) {
                    OnEdgeDone(g, s, e, 0);
                }
            }
            for (size_t i = 0; i < g.Nodes.size(); i++) {
                if ((!g.Nodes[i].IsInput && !g.Nodes[i].I) || s.Nodes[i].Skip) {
                    OnInputDone(g, s, i);
                }
            }
        }
        // In total, all these loops start each node with `IsInput || !I || Skip` once.
        // Input nodes are always transparent, so calling `OnInputDone` for them sets `Done` right away.
        for (const auto& node : chunk->GetDone()) {
            if (auto id = g.Index.FindPtr(node)) {
                if (g.Nodes[*id].IsInput && !s.Nodes[*id].Done) {
                    OnInputDone(g, s, *id);
                }
            }
        }
        if (!chunk->ByteSizeLong()) {
            // Can now start all nodes that should be startable, but were not marked.
            // This is essentially compatibility with the default apphost protocol.
            for (size_t i = 0; i < g.Nodes.size(); i++) {
                if (g.Nodes[i].IsInput && !s.Nodes[i].Done) {
                    OnInputDone(g, s, i);
                }
            }
            while (!s.Nodes[g.ResponseId].Done) {
                if (ah.StreamOut() && s.Response.ByteSizeLong()) {
                    // FIXME this streaming is not symmetric with streaming *into* a subgraph.
                    //       It's fine for `http-to-apphost` (streaming payload to client),
                    //       but won't help with splitting a subgraph's output: need to attach
                    //       node names to output items & mark completed ones.
                    if (!ah.Write(std::move(s.Response))) {
                        return false;
                    }
                    s.Response.Clear();
                }
                if (!s.OnTerminal.wait()) {
                    return false;
                }
            }
            for (size_t j = g.ResponseId; j != 0; j = s.Nodes[j].Prev) {
                g.Nodes[j].CriticalPath++;
            }
            SaveLog(s);
            return ah.Write(std::move(s.Response));
        }
    }
    return false;
}

static NSv::TAction AppHost(const YAML::Node& args, NSv::TAuxData& aux) {
    CHECK_NODE(args, args.IsMap(), "an argument is required");
    return [g = TGraph(args.begin()->second, aux), d = MakeHolder<NSv::TThreadLocal<cone::mguard>>()](TInterface ah) mutable {
        auto s = MakeHolder<TGraphState>(g);
        if (!Respond(g, *s, ah) MUN_RETHROW) {
            return false; // Cancelling the request before RESPONSE is done cancels async nodes.
        }
        if (s->Remaining) {
            d->GetOrCreate().add([s = std::move(s)] {
                while (s->Remaining) {
                    if (!s->OnTerminal.wait()) {
                        return false;
                    }
                }
                return true;
            });
        };
        return ah.Close();
    };
}

SV_DEFINE_ACTION("apphost", AppHost);
