@using Microsoft.AspNetCore.SignalR.Client
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
@using ZB.MOM.WW.OtOpcUa.Admin.Services
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@inject ClusterNodeService NodeSvc
@inject NavigationManager Nav
@implements IAsyncDisposable

<h4>Redundancy topology</h4>
@if (_roleChangedBanner is not null)
{
    <div class="alert alert-info small mb-2">@_roleChangedBanner</div>
}
<p class="text-muted small">
    One row per <code>ClusterNode</code> in this cluster. Role, <code>ApplicationUri</code>,
    and <code>ServiceLevelBase</code> are authored separately; the Admin UI shows them read-only
    here so operators can confirm the published topology without touching it. LastSeen older than
    @((int)ClusterNodeService.StaleThreshold.TotalSeconds)s is flagged Stale — the node has
    stopped heart-beating and is likely down. Role swap goes through the server-side
    <code>RedundancyCoordinator</code> apply-lease flow, not direct DB edits.
</p>

@if (_nodes is null)
{
    <p>Loading…</p>
}
else if (_nodes.Count == 0)
{
    <div class="alert alert-warning">
        No ClusterNode rows for this cluster. The server process needs at least one entry
        (with a non-blank <code>ApplicationUri</code>) before it can start up per OPC UA spec.
    </div>
}
else
{
    var primaries = _nodes.Count(n => n.RedundancyRole == RedundancyRole.Primary);
    var secondaries = _nodes.Count(n => n.RedundancyRole == RedundancyRole.Secondary);
    var standalone = _nodes.Count(n => n.RedundancyRole == RedundancyRole.Standalone);
    var staleCount = _nodes.Count(ClusterNodeService.IsStale);

    <div class="row g-3 mb-4">
        <div class="col-md-3"><div class="card"><div class="card-body">
            <h6 class="text-muted mb-1">Nodes</h6>
            <div class="fs-3">@_nodes.Count</div>
        </div></div></div>
        <div class="col-md-3"><div class="card border-success"><div class="card-body">
            <h6 class="text-muted mb-1">Primary</h6>
            <div class="fs-3 text-success">@primaries</div>
        </div></div></div>
        <div class="col-md-3"><div class="card border-info"><div class="card-body">
            <h6 class="text-muted mb-1">Secondary</h6>
            <div class="fs-3 text-info">@secondaries</div>
        </div></div></div>
        <div class="col-md-3"><div class="card @(staleCount > 0 ? "border-warning" : "")"><div class="card-body">
            <h6 class="text-muted mb-1">Stale</h6>
            <div class="fs-3 @(staleCount > 0 ? "text-warning" : "")">@staleCount</div>
        </div></div></div>
    </div>

    @if (primaries == 0 && standalone == 0)
    {
        <div class="alert alert-danger small mb-3">
            No Primary or Standalone node — the cluster has no authoritative write target. Secondaries
            stay read-only until one of them gets promoted via <code>RedundancyCoordinator</code>.
        </div>
    }
    else if (primaries > 1)
    {
        <div class="alert alert-danger small mb-3">
            <strong>Split-brain:</strong> @primaries nodes claim the Primary role. Apply-lease
            enforcement should have made this impossible at the coordinator level. Investigate
            immediately — one of the rows was likely hand-edited.
        </div>
    }

    <table class="table table-sm table-hover align-middle">
        <thead>
            <tr>
                <th>Node</th>
                <th>Role</th>
                <th>Host</th>
                <th class="text-end">OPC UA port</th>
                <th class="text-end">ServiceLevel base</th>
                <th>ApplicationUri</th>
                <th>Enabled</th>
                <th>Last seen</th>
            </tr>
        </thead>
        <tbody>
        @foreach (var n in _nodes)
        {
            <tr class="@RowClass(n)">
                <td><code>@n.NodeId</code></td>
                <td><span class="badge @RoleBadge(n.RedundancyRole)">@n.RedundancyRole</span></td>
                <td>@n.Host</td>
                <td class="text-end"><code>@n.OpcUaPort</code></td>
                <td class="text-end">@n.ServiceLevelBase</td>
                <td class="small text-break"><code>@n.ApplicationUri</code></td>
                <td>
                    @if (n.Enabled) { <span class="badge bg-success">Enabled</span> }
                    else { <span class="badge bg-secondary">Disabled</span> }
                </td>
                <td class="small @(ClusterNodeService.IsStale(n) ? "text-warning fw-bold" : "")">
                    @(n.LastSeenAt is null ? "never" : FormatAge(n.LastSeenAt.Value))
                    @if (ClusterNodeService.IsStale(n)) { <span class="badge bg-warning text-dark ms-1">Stale</span> }
                </td>
            </tr>
        }
        </tbody>
    </table>
}

@code {
    [Parameter] public string ClusterId { get; set; } = string.Empty;

    private List<ClusterNode>? _nodes;
    private HubConnection? _hub;
    private string? _roleChangedBanner;

    protected override async Task OnParametersSetAsync()
    {
        _nodes = await NodeSvc.ListByClusterAsync(ClusterId, CancellationToken.None);
        if (_hub is null) await ConnectHubAsync();
    }

    private async Task ConnectHubAsync()
    {
        _hub = new HubConnectionBuilder()
            .WithUrl(Nav.ToAbsoluteUri("/hubs/fleet-status"))
            .WithAutomaticReconnect()
            .Build();

        _hub.On<RoleChangedMessage>("RoleChanged", async msg =>
        {
            if (msg.ClusterId != ClusterId) return;
            _roleChangedBanner = $"Role changed on {msg.NodeId}: {msg.FromRole} → {msg.ToRole} at {msg.ObservedAtUtc:HH:mm:ss 'UTC'}";
            _nodes = await NodeSvc.ListByClusterAsync(ClusterId, CancellationToken.None);
            await InvokeAsync(StateHasChanged);
        });

        await _hub.StartAsync();
        await _hub.SendAsync("SubscribeCluster", ClusterId);
    }

    public async ValueTask DisposeAsync()
    {
        if (_hub is not null)
        {
            await _hub.DisposeAsync();
            _hub = null;
        }
    }

    private static string RowClass(ClusterNode n) =>
        ClusterNodeService.IsStale(n) ? "table-warning" :
        !n.Enabled ? "table-secondary" : "";

    private static string RoleBadge(RedundancyRole r) => r switch
    {
        RedundancyRole.Primary    => "bg-success",
        RedundancyRole.Secondary  => "bg-info",
        RedundancyRole.Standalone => "bg-primary",
        _                         => "bg-secondary",
    };

    private static string FormatAge(DateTime t)
    {
        var age = DateTime.UtcNow - t;
        if (age.TotalSeconds < 60) return $"{(int)age.TotalSeconds}s ago";
        if (age.TotalMinutes < 60) return $"{(int)age.TotalMinutes}m ago";
        if (age.TotalHours < 24) return $"{(int)age.TotalHours}h ago";
        return t.ToString("yyyy-MM-dd HH:mm 'UTC'");
    }
}
