@page "/fleet"
@* Per-node deployment status. v2 reads NodeDeploymentState (the per-(node, deployment) apply
   progress row owned by each DriverHostActor) and projects the most-recent row per node. The
   Akka cluster topology comes from IClusterRoleInfo so we can show nodes that haven't applied
   anything yet alongside nodes that have. *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.Commons.Interfaces
@using ZB.MOM.WW.OtOpcUa.Configuration
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@inject IClusterRoleInfo Cluster
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
@implements IDisposable

<div class="d-flex justify-content-between align-items-center mb-3">
    <h4 class="mb-0">Fleet status</h4>
</div>

<div class="d-flex align-items-center mb-3 gap-2">
    <button class="btn btn-sm btn-outline-primary" @onclick="RefreshAsync" disabled="@_refreshing">
        @if (_refreshing) { <span class="spinner-border spinner-border-sm me-1" /> }
        Refresh
    </button>
    <span class="text-muted small">
        Auto-refresh every @RefreshIntervalSeconds s. Last updated: @(_lastRefreshUtc?.ToString("HH:mm:ss 'UTC'") ?? "—")
    </span>
</div>

@if (_rows is null)
{
    <p>Loading…</p>
}
else if (_rows.Count == 0)
{
    <section class="panel notice rise" style="animation-delay:.02s">
        No driver-role nodes are currently Up in the Akka cluster, and no NodeDeploymentState
        rows have been recorded yet. Either no driver nodes have joined or the cluster is still
        forming.
    </section>
}
else
{
    <section class="agg-grid rise" style="animation-delay:.02s">
        <div class="agg-card">
            <div class="agg-label">Nodes</div>
            <div class="agg-value numeric">@_rows.Count</div>
        </div>
        <div class="agg-card">
            <div class="agg-label">Applied</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Status == NodeDeploymentStatus.Applied)</div>
        </div>
        <div class="agg-card caution">
            <div class="agg-label">Applying</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Status == NodeDeploymentStatus.Applying)</div>
        </div>
        <div class="agg-card alert">
            <div class="agg-label">Failed</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Status == NodeDeploymentStatus.Failed)</div>
        </div>
    </section>

    <section class="panel rise" style="animation-delay:.08s">
        <div class="panel-head">Nodes</div>
        <div class="table-wrap">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Node</th>
                        <th>Roles</th>
                        <th>Status</th>
                        <th>Applied at</th>
                        <th>Started at</th>
                        <th>Failure reason</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var r in _rows)
                    {
                        <tr>
                            <td><span class="mono">@r.NodeId</span></td>
                            <td>
                                @foreach (var role in r.Roles)
                                {
                                    <span class="chip chip-idle me-1">@role</span>
                                }
                            </td>
                            <td><span class="chip @StatusChipClass(r.Status)">@StatusLabel(r.Status)</span></td>
                            <td>@(r.AppliedAtUtc?.ToString("u") ?? "—")</td>
                            <td>@(r.StartedAtUtc?.ToString("u") ?? "—")</td>
                            <td>@(r.FailureReason ?? "")</td>
                        </tr>
                    }
                </tbody>
            </table>
        </div>
    </section>
}

@code {
    private const int RefreshIntervalSeconds = 10;

    private List<NodeRow>? _rows;
    private bool _refreshing;
    private DateTime? _lastRefreshUtc;
    private Timer? _timer;

    protected override async Task OnInitializedAsync()
    {
        await LoadAsync();
        _timer = new Timer(_ => _ = InvokeAsync(LoadAsync), null,
            TimeSpan.FromSeconds(RefreshIntervalSeconds),
            TimeSpan.FromSeconds(RefreshIntervalSeconds));
    }

    private async Task RefreshAsync() => await LoadAsync();

    private async Task LoadAsync()
    {
        _refreshing = true;
        StateHasChanged();
        try
        {
            await using var db = await DbFactory.CreateDbContextAsync();
            // Project the most-recent NodeDeploymentState per node — that's the row the
            // DriverHostActor most recently touched, regardless of which deployment it was for.
            var states = await db.NodeDeploymentStates.AsNoTracking()
                .GroupBy(s => s.NodeId)
                .Select(g => g.OrderByDescending(s => s.StartedAtUtc).First())
                .ToListAsync();
            var byNode = states.ToDictionary(s => s.NodeId);

            // Union with current Akka driver members so a freshly-joined node that has no
            // NodeDeploymentState row yet still appears as "waiting".
            var akkaDrivers = Cluster.MembersWithRole("driver")
                .Select(n => n.Value).ToHashSet(StringComparer.OrdinalIgnoreCase);
            var allNodes = byNode.Keys.Union(akkaDrivers, StringComparer.OrdinalIgnoreCase)
                .OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList();

            _rows = allNodes.Select(nodeId =>
            {
                byNode.TryGetValue(nodeId, out var state);
                return new NodeRow(
                    NodeId: nodeId,
                    Roles: akkaDrivers.Contains(nodeId) ? new[] { "driver" } : Array.Empty<string>(),
                    Status: state?.Status,
                    StartedAtUtc: state?.StartedAtUtc,
                    AppliedAtUtc: state?.AppliedAtUtc,
                    FailureReason: state?.FailureReason);
            }).ToList();
            _lastRefreshUtc = DateTime.UtcNow;
        }
        finally
        {
            _refreshing = false;
            StateHasChanged();
        }
    }

    private static string StatusChipClass(NodeDeploymentStatus? status) => status switch
    {
        NodeDeploymentStatus.Applied => "chip-ok",
        NodeDeploymentStatus.Applying => "chip-caution",
        NodeDeploymentStatus.Failed => "chip-alert",
        _ => "chip-idle",
    };

    private static string StatusLabel(NodeDeploymentStatus? status) => status?.ToString() ?? "waiting";

    public void Dispose() => _timer?.Dispose();

    private sealed record NodeRow(
        string NodeId,
        IReadOnlyCollection<string> Roles,
        NodeDeploymentStatus? Status,
        DateTime? StartedAtUtc,
        DateTime? AppliedAtUtc,
        string? FailureReason);
}
