using System.Text.Json.Nodes;
namespace w4c_workflows.Models.Nodes;
// ---------------------------------------------------------------------------
// A compiled node graph: the executable form of the node-mode half of a
// workflow definition. It is deliberately free of EF/DB types so it can be
// built, validated and run in isolation (unit tests, previews, dry runs).
//
// `next`/`onError` from the YAML are lowered to edges here, so the run kernel
// only ever follows edges — never a single "next" pointer.
// ---------------------------------------------------------------------------
/// One step in a node graph: a resolved blueprint plus its parameters.
public sealed record NodeGraphNode
{
/// The YAML task id; stable and used in expressions such as $("stepId").
public required string Id { get; init; }
/// Blueprint resolved from the catalog (validated at compile time).
public required NodeBlueprint Blueprint { get; init; }
/// Raw (unresolved) parameters; interpolation happens per item at run time.
public required JsonObject Parameters { get; init; }
///
/// Credentials this step needs, keyed by the blueprint credential alias;
/// the value is a vault credential id or unique name, resolved at run time.
///
public IReadOnlyDictionary CredentialRefs { get; init; } =
new Dictionary();
/// Run-mode override; null means "use the blueprint default".
public string? RunMode { get; init; }
/// Effective run mode (override, else blueprint).
public string EffectiveRunMode =>
string.IsNullOrWhiteSpace(RunMode) ? Blueprint.RunMode : RunMode!;
public bool ContinueOnFail { get; init; }
public TaskRetryDefinition? Retry { get; init; }
public NodePort Output(int index)
=> index >= 0 && index < Blueprint.Outputs.Count ? Blueprint.Outputs[index] : NodePort.Main;
public NodePort Input(int index)
=> index >= 0 && index < Blueprint.Inputs.Count ? Blueprint.Inputs[index] : NodePort.Main;
/// Index of the blueprint's error output, or -1 when it has none.
public int ErrorOutputIndex
=> Blueprint.Outputs.FindIndex(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal));
}
/// A directed connection between two steps on specific ports.
public sealed record NodeGraphEdge
{
public required string FromNodeId { get; init; }
public int FromOutput { get; init; }
public required string ToNodeId { get; init; }
public int ToInput { get; init; }
///
/// True when this edge closes a loop (a back edge into a loop node). It does
/// not count toward the target's readiness; instead it re-triggers the loop
/// node for the next iteration.
///
public bool IsLoopBack { get; init; }
}
///
/// A validated, runnable node graph. is the single
/// node with no incoming non-loop edge. Cycles are rejected at compile time
/// unless they close through a loop-capable node (a loop-back edge).
///
public sealed record NodeGraph
{
public required IReadOnlyList Nodes { get; init; }
public required IReadOnlyList Edges { get; init; }
public required string EntryNodeId { get; init; }
private Dictionary? _byId;
private Dictionary? _byDisplayName;
private Dictionary<(string NodeId, int Output), List>? _from;
private Dictionary>? _to;
public NodeGraphNode? Find(string nodeId)
{
_byId ??= Nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
return _byId.TryGetValue(nodeId, out var node) ? node : null;
}
///
/// First node whose blueprint display name matches, or null. Lets
/// $("Send message") resolve without scanning per
/// item; built once and reused.
///
public NodeGraphNode? FindByDisplayName(string displayName)
{
if (_byDisplayName == null)
{
// First-wins: duplicate display names are ambiguous but legal.
var index = new Dictionary(StringComparer.Ordinal);
foreach (var node in Nodes)
index.TryAdd(node.Blueprint.DisplayName, node);
_byDisplayName = index;
}
return _byDisplayName.TryGetValue(displayName, out var match) ? match : null;
}
public NodeGraphNode Require(string nodeId)
=> Find(nodeId) ?? throw new InvalidOperationException($"node '{nodeId}' is not in the graph");
/// Edges leaving a specific output port.
public IEnumerable EdgesFrom(string nodeId, int output)
{
if (_from == null)
{
var index = new Dictionary<(string, int), List>();
foreach (var edge in Edges)
{
if (!index.TryGetValue((edge.FromNodeId, edge.FromOutput), out var outgoing))
index[(edge.FromNodeId, edge.FromOutput)] = outgoing = new List();
outgoing.Add(edge);
}
_from = index;
}
return _from.TryGetValue((nodeId, output), out var edges) ? edges : Array.Empty();
}
/// Edges arriving at a node, across all inputs.
public IEnumerable EdgesTo(string nodeId)
{
if (_to == null)
{
var index = new Dictionary>(StringComparer.Ordinal);
foreach (var edge in Edges)
{
if (!index.TryGetValue(edge.ToNodeId, out var incoming))
index[edge.ToNodeId] = incoming = new List();
incoming.Add(edge);
}
_to = index;
}
return _to.TryGetValue(nodeId, out var edges) ? edges : Array.Empty();
}
}