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;
public NodeGraphNode? Find(string nodeId)
{
_byId ??= Nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
return _byId.TryGetValue(nodeId, out var node) ? node : 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)
=> Edges.Where(e => e.FromNodeId == nodeId && e.FromOutput == output);
/// Edges arriving at a node, across all inputs.
public IEnumerable EdgesTo(string nodeId)
=> Edges.Where(e => e.ToNodeId == nodeId);
}