using RackPeek.Domain.Persistence;
using RackPeek.Domain.Resources;
using RackPeek.Domain.Resources.Hardware;
using RackPeek.Domain.Resources.Services;
using RackPeek.Domain.Resources.Services.Networking;
using RackPeek.Domain.Resources.SystemResources;
namespace RackPeek.Domain.Graph.UseCases;
///
/// Logical / service-oriented view. Each system (hypervisor, VM, LXC,
/// container) becomes a single "host card" whose body lists every
/// service running on it. Cards are grouped subnet → hardware. No edges
/// are emitted — containment alone conveys "runs on", and the
/// serialiser stacks siblings vertically via invisible links.
///
public class BuildLogicalGraphUseCase(IResourceCollection repo) : IUseCase {
private const int _defaultPrefix = 24;
public async Task ExecuteAsync() {
IReadOnlyList services = await repo.GetAllOfTypeAsync();
IReadOnlyList systems = await repo.GetAllOfTypeAsync();
IReadOnlyList hardware = repo.HardwareResources;
var byName = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (Hardware hw in hardware) byName[hw.Name] = hw;
foreach (SystemResource s in systems) byName[s.Name] = s;
foreach (Service svc in services) byName[svc.Name] = svc;
// Group services by the system they ultimately run on. We resolve
// the immediate runsOn first — that's the host the service was
// declared against. Services whose immediate runsOn isn't a known
// system (e.g. it points at hardware or is missing) are dropped from
// the compact view since they have no host card to live inside.
var servicesByHost = new Dictionary>(StringComparer.OrdinalIgnoreCase);
foreach (Service service in services) {
var parent = service.RunsOn.FirstOrDefault();
if (parent is null) continue;
if (!byName.TryGetValue(parent, out Resource? parentResource)) continue;
if (parentResource is not SystemResource) continue;
if (!servicesByHost.TryGetValue(parent, out List? list))
servicesByHost[parent] = list = new List();
list.Add(service);
}
// Each system becomes a host card. Hosts without services still
// appear as a labelled card (e.g. a hypervisor that only contains
// VMs has no services running directly on it, but is still a
// meaningful logical entity).
var hostEntries = new List();
foreach (SystemResource sys in systems) {
var ip = FindIp(sys, byName);
var subnet = SubnetCidr(ip, _defaultPrefix);
if (subnet is null) continue;
Hardware? parentHw = FindParentHardware(sys, byName);
servicesByHost.TryGetValue(sys.Name, out List? hostServices);
var rows = (hostServices ?? new List())
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.Select(s => new GraphNodeRow(s.Name, ServiceDetail(s)))
.ToList();
hostEntries.Add(new HostEntry(sys, subnet, parentHw?.Name, ip, rows));
}
var nodes = hostEntries
.OrderBy(e => e.Subnet, StringComparer.Ordinal)
.ThenBy(e => e.HardwareName ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ThenByDescending(e => e.Rows.Count) // big cards first within a hardware bucket
.ThenBy(e => e.System.Name, StringComparer.OrdinalIgnoreCase)
.Select(e => new GraphNode(
e.System.Name,
e.System.Name,
NodeKind(e.System),
e.Ip,
Rows: e.Rows.Count > 0 ? e.Rows : null))
.ToList();
List groups = BuildGroups(hostEntries);
return new Graph(nodes, [], groups, GraphRenderHint.Compact);
}
private static List BuildGroups(IReadOnlyList entries) {
var groups = new List();
IOrderedEnumerable> bySubnet = entries
.GroupBy(e => e.Subnet, StringComparer.Ordinal)
.OrderBy(g => g.Key, StringComparer.Ordinal);
foreach (IGrouping subnetGroup in bySubnet) {
var subnetId = "g_" + Slug(subnetGroup.Key);
var directNodes = new List();
IOrderedEnumerable> byHardware = subnetGroup
.GroupBy(e => e.HardwareName)
.OrderBy(g => g.Key ?? string.Empty, StringComparer.OrdinalIgnoreCase);
foreach (IGrouping hwGroup in byHardware) {
if (hwGroup.Key is null) {
directNodes.AddRange(hwGroup.Select(e => e.System.Name));
continue;
}
var hwGroupId = subnetId + "__" + Slug(hwGroup.Key);
groups.Add(new GraphGroup(
hwGroupId,
hwGroup.Key,
hwGroup.Select(e => e.System.Name).ToList(),
subnetId));
}
groups.Add(new GraphGroup(subnetId, subnetGroup.Key, directNodes, null));
}
return groups;
}
private static string NodeKind(SystemResource sys) {
if (string.IsNullOrWhiteSpace(sys.Type)) return "System";
var t = sys.Type.Trim().ToLowerInvariant();
return t switch {
"hypervisor" => "Hypervisor",
"vm" => "Vm",
"container" => "Container",
_ => "System"
};
}
private static string? ServiceDetail(Service service) {
var port = service.Network?.Port;
return port.HasValue ? ":" + port.Value : null;
}
private static string? FindIp(Resource resource, Dictionary byName) {
var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
Resource? current = resource;
while (current is not null && visited.Add(current.Name)) {
switch (current) {
case Service { Network.Ip: { Length: > 0 } svcIp }:
return svcIp;
case SystemResource { Ip: { Length: > 0 } sysIp }:
return sysIp;
}
var parent = current.RunsOn.FirstOrDefault();
if (parent is null) return null;
current = byName.GetValueOrDefault(parent);
}
return null;
}
private static Hardware? FindParentHardware(Resource resource, Dictionary byName) {
var visited = new HashSet(StringComparer.OrdinalIgnoreCase);
Resource? current = resource;
while (current is not null && visited.Add(current.Name)) {
if (current is Hardware hw) return hw;
var parent = current.RunsOn.FirstOrDefault();
if (parent is null) return null;
current = byName.GetValueOrDefault(parent);
}
return null;
}
private static string? SubnetCidr(string? ip, int prefix) {
if (string.IsNullOrWhiteSpace(ip)) return null;
try {
var u = IpHelper.ToUInt32(ip);
var mask = IpHelper.MaskFromPrefix(prefix);
return $"{IpHelper.ToIp(u & mask)}/{prefix}";
}
catch {
return null;
}
}
private static string Slug(string value) {
var chars = value.Select(c => char.IsLetterOrDigit(c) ? char.ToLowerInvariant(c) : '_').ToArray();
return new string(chars);
}
private readonly record struct HostEntry(
SystemResource System,
string Subnet,
string? HardwareName,
string? Ip,
IReadOnlyList Rows);
}