@page "/hosts"
@* Akka cluster topology: each member's NodeId (host:port), roles, leader status. v2 reshapes
   v1's "driver host" page — there are no per-driver host rows yet (driver-instance child actors
   land with F7). For now this is the cluster-membership view; expand to per-driver rows when
   DriverHostActor starts spawning DriverInstanceActor children. *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using Akka.Actor
@using Akka.Cluster
@inject ActorSystem ActorSystem
@implements IDisposable

<div class="d-flex justify-content-between align-items-center mb-3">
    <h4 class="mb-0">Cluster hosts</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>

<section class="panel notice rise" style="animation-delay:.02s">
    Each row is one Akka cluster member identified by <span class="mono">host:port</span>. Roles
    drive which actors run on which node — <span class="mono">admin</span> nodes host the
    control-plane singletons, <span class="mono">driver</span> nodes host the per-node runtime
    actors. The leader columns identify which member currently owns each role's singletons.
</section>

@if (_rows is null)
{
    <p>Loading…</p>
}
else if (_rows.Count == 0)
{
    <section class="panel notice rise" style="animation-delay:.08s">
        No cluster members visible. The local node may still be joining.
    </section>
}
else
{
    <section class="agg-grid rise" style="animation-delay:.08s">
        <div class="agg-card">
            <div class="agg-label">Members</div>
            <div class="agg-value numeric">@_rows.Count</div>
        </div>
        <div class="agg-card">
            <div class="agg-label">Up</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Status == "Up")</div>
        </div>
        <div class="agg-card caution">
            <div class="agg-label">Joining/Leaving</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Status is "Joining" or "Leaving" or "Exiting")</div>
        </div>
        <div class="agg-card alert">
            <div class="agg-label">Unreachable</div>
            <div class="agg-value numeric">@_rows.Count(r => r.Unreachable)</div>
        </div>
    </section>

    <section class="panel rise" style="animation-delay:.14s">
        <div class="panel-head">Members</div>
        <div class="table-wrap">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Address</th>
                        <th>Status</th>
                        <th>Roles</th>
                        <th>Leader for</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var r in _rows)
                    {
                        <tr>
                            <td>
                                <span class="mono">@r.Address</span>
                                @if (r.IsSelf) { <span class="chip chip-idle ms-2">self</span> }
                            </td>
                            <td>
                                <span class="chip @StatusChipClass(r.Status, r.Unreachable)">
                                    @(r.Unreachable ? $"{r.Status} (unreachable)" : r.Status)
                                </span>
                            </td>
                            <td>
                                @foreach (var role in r.Roles)
                                {
                                    <span class="chip chip-idle me-1">@role</span>
                                }
                            </td>
                            <td>
                                @if (r.LeaderRoles.Count == 0)
                                {
                                    <span class="text-muted">—</span>
                                }
                                else
                                {
                                    @foreach (var role in r.LeaderRoles)
                                    {
                                        <span class="chip chip-ok me-1">@role</span>
                                    }
                                }
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        </div>
    </section>
}

@code {
    private const int RefreshIntervalSeconds = 5;

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

    protected override void OnInitialized()
    {
        Refresh();
        _timer = new Timer(_ => InvokeAsync(() => { Refresh(); StateHasChanged(); }), null,
            TimeSpan.FromSeconds(RefreshIntervalSeconds),
            TimeSpan.FromSeconds(RefreshIntervalSeconds));
    }

    private async Task RefreshAsync()
    {
        _refreshing = true;
        StateHasChanged();
        try
        {
            await Task.Yield();
            Refresh();
        }
        finally
        {
            _refreshing = false;
            StateHasChanged();
        }
    }

    private void Refresh()
    {
        var cluster = Akka.Cluster.Cluster.Get(ActorSystem);
        var state = cluster.State;
        var unreachable = state.Unreachable
            .Select(m => m.Address.ToString()).ToHashSet();
        var selfAddress = cluster.SelfAddress.ToString();

        _rows = state.Members.Select(m =>
        {
            var address = m.Address.ToString();
            var hostPort = $"{m.Address.Host ?? "?"}:{m.Address.Port ?? 0}";
            var leaderRoles = m.Roles
                .Where(role => cluster.State.RoleLeader(role)?.ToString() == address)
                .OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
                .ToList();
            return new MemberRow(
                Address: hostPort,
                Status: m.Status.ToString(),
                Roles: m.Roles.OrderBy(s => s, StringComparer.OrdinalIgnoreCase).ToList(),
                LeaderRoles: leaderRoles,
                Unreachable: unreachable.Contains(address),
                IsSelf: address == selfAddress);
        })
        .OrderBy(r => r.Address, StringComparer.OrdinalIgnoreCase)
        .ToList();
        _lastRefreshUtc = DateTime.UtcNow;
    }

    private static string StatusChipClass(string status, bool unreachable) => (status, unreachable) switch
    {
        (_, true) => "chip-alert",
        ("Up", _) => "chip-ok",
        ("Joining", _) or ("Leaving", _) or ("Exiting", _) or ("WeaklyUp", _) => "chip-caution",
        ("Down", _) or ("Removed", _) => "chip-alert",
        _ => "chip-idle",
    };

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

    private sealed record MemberRow(
        string Address,
        string Status,
        IReadOnlyCollection<string> Roles,
        IReadOnlyCollection<string> LeaderRoles,
        bool Unreachable,
        bool IsSelf);
}
