@page "/alarms"
@implements IAsyncDisposable
@using Microsoft.AspNetCore.SignalR.Client
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
@inject IDashboardLiveDataService LiveData
@inject IOptions<GatewayOptions> GatewayOptions
@inject DashboardHubConnectionFactory HubFactory

<PageTitle>Dashboard Alarms</PageTitle>

<div class="dashboard-page-header">
    <div>
        <h1>Alarms</h1>
        <div class="text-secondary">@HeaderLine()</div>
    </div>
    <div class="d-flex align-items-center gap-2">
        <span class="badge @_providerStatus.BadgeCssClass"
              title="@ProviderStatusTitle()">
            @_providerStatus.Label
        </span>
    </div>
</div>

@if (!GatewayOptions.Value.Alarms.Enabled)
{
    <div class="alert alert-danger">
        Alarm auto-subscribe is disabled (<code>MxGateway:Alarms:Enabled</code> is false). The
        dashboard session is not subscribed to any alarm provider, so this list will stay empty.
        Enable alarms in configuration and restart the gateway.
    </div>
}

@if (!string.IsNullOrWhiteSpace(_queryError))
{
    <div class="alert alert-danger">Alarm query failed: @_queryError</div>
}

<section class="metric-grid compact">
    <MetricCard Label="Active (unacked)" Value="@_unackedCount.ToString("N0")" />
    <MetricCard Label="Acknowledged" Value="@_ackedCount.ToString("N0")" />
    <MetricCard Label="Total Active" Value="@_alarms.Count.ToString("N0")" />
    <MetricCard Label="Showing" Value="@FilteredAlarms().Count.ToString("N0")" />
</section>

<section class="dashboard-section">
    <div class="section-heading">
        <h2>Filters</h2>
    </div>
    <div class="alarm-filters">
        <label class="alarm-filter-check">
            <input type="checkbox" @bind="_showActive" />
            <span>Active (unacked)</span>
        </label>
        <label class="alarm-filter-check">
            <input type="checkbox" @bind="_showAcked" />
            <span>Acknowledged</span>
        </label>
        <div class="alarm-filter-field">
            <label class="form-label" for="alarm-area">Area</label>
            <select id="alarm-area" class="form-select form-select-sm" @bind="_areaFilter">
                <option value="">All areas</option>
                @foreach (string area in Areas())
                {
                    <option value="@area">@area</option>
                }
            </select>
        </div>
        <div class="alarm-filter-field">
            <label class="form-label" for="alarm-sev-min">Min severity</label>
            <input id="alarm-sev-min" type="number" class="form-control form-control-sm" @bind="_minSeverity" />
        </div>
        <div class="alarm-filter-field">
            <label class="form-label" for="alarm-sev-max">Max severity</label>
            <input id="alarm-sev-max" type="number" class="form-control form-control-sm" @bind="_maxSeverity" />
        </div>
        <div class="alarm-filter-field alarm-filter-grow">
            <label class="form-label" for="alarm-search">Search</label>
            <input id="alarm-search" class="form-control form-control-sm"
                   placeholder="Reference, source or description…"
                   @bind="_search" @bind:event="oninput" />
        </div>
    </div>
</section>

<section class="dashboard-section">
    <div class="section-heading">
        <h2>Active Alarms</h2>
    </div>
    @{
        IReadOnlyList<DashboardActiveAlarm> rows = FilteredAlarms();
    }
    @if (rows.Count == 0)
    {
        <div class="empty-state">
            @if (_alarms.Count == 0)
            {
                <span>No alarms are currently Active or ActiveAcked.</span>
            }
            else
            {
                <span>No alarms match the current filters.</span>
            }
        </div>
    }
    else
    {
        <div class="table-responsive">
            <table class="table table-sm dashboard-table">
                <thead>
                    <tr>
                        <th scope="col">State</th>
                        <th scope="col">Severity</th>
                        <th scope="col">Alarm Reference</th>
                        <th scope="col">Source</th>
                        <th scope="col">Type</th>
                        <th scope="col">Area</th>
                        <th scope="col">Last Transition</th>
                        <th scope="col">Operator</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (DashboardActiveAlarm alarm in rows)
                    {
                        <tr>
                            <td><span class="alarm-state @StateClass(alarm.State)">@StateText(alarm.State)</span></td>
                            <td class="alarm-severity">@alarm.Severity</td>
                            <td>
                                <code>@alarm.Reference</code>
                                @if (!string.IsNullOrWhiteSpace(alarm.Description))
                                {
                                    <div class="alarm-desc">@alarm.Description</div>
                                }
                            </td>
                            <td>@DashboardDisplay.Text(alarm.Source)</td>
                            <td>@DashboardDisplay.Text(alarm.AlarmType)</td>
                            <td>@DashboardDisplay.Text(alarm.Area)</td>
                            <td>@(alarm.LastTransition is { } ts ? DashboardDisplay.DateTime(ts) : "-")</td>
                            <td>
                                @DashboardDisplay.Text(alarm.OperatorUser)
                                @if (!string.IsNullOrWhiteSpace(alarm.OperatorComment))
                                {
                                    <div class="alarm-desc">@alarm.OperatorComment</div>
                                }
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        </div>
    }
    <div class="browse-search-note">
        Cleared alarms are not retained — this list reflects only alarms currently Active or
        ActiveAcked, refreshed every 3 seconds.
    </div>
</section>

@code {
    private readonly List<DashboardActiveAlarm> _alarms = [];
    private string? _queryError;
    private int? _workerPid;
    private DateTimeOffset? _lastRefresh;
    private int _unackedCount;
    private int _ackedCount;

    private bool _showActive = true;
    private bool _showAcked;
    private string _areaFilter = string.Empty;
    private int _minSeverity;
    private int _maxSeverity = 1000;
    private string _search = string.Empty;

    private readonly CancellationTokenSource _cts = new();
    private Task? _pollTask;

    private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
    private HubConnection? _alarmsHub;

    /// <inheritdoc />
    protected override void OnInitialized()
    {
        _pollTask = PollLoopAsync();
        _ = AttachAlarmsHubAsync();
    }

    private string? ProviderStatusTitle()
    {
        return _providerStatus.IsDegraded && !string.IsNullOrWhiteSpace(_providerStatus.Reason)
            ? _providerStatus.Reason
            : null;
    }

    private async Task AttachAlarmsHubAsync()
    {
        _alarmsHub = HubFactory.Create("/hubs/alarms");
        _alarmsHub.On<AlarmFeedMessage>(AlarmsHub.AlarmMessage, async message =>
        {
            if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
            {
                _providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
                await InvokeAsync(StateHasChanged).ConfigureAwait(false);
            }
        });

        try
        {
            await _alarmsHub.StartAsync(_cts.Token).ConfigureAwait(false);
        }
        catch
        {
            // The badge is best-effort; it stays at the healthy default until
            // the hub reconnects and delivers a fresh provider-status message.
        }
    }

    private string HeaderLine()
    {
        string refreshed = _lastRefresh is { } at
            ? $"refreshed {DashboardDisplay.DateTime(at)}"
            : "awaiting first refresh";
        return _workerPid is int pid ? $"{refreshed} · worker pid {pid}" : refreshed;
    }

    private IReadOnlyList<string> Areas()
    {
        return _alarms
            .Select(alarm => alarm.Area)
            .Where(area => !string.IsNullOrWhiteSpace(area))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .OrderBy(area => area, StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    private IReadOnlyList<DashboardActiveAlarm> FilteredAlarms()
    {
        string query = _search.Trim();
        return _alarms
            .Where(MatchesState)
            .Where(alarm => _areaFilter.Length == 0
                || string.Equals(alarm.Area, _areaFilter, StringComparison.OrdinalIgnoreCase))
            .Where(alarm => alarm.Severity >= _minSeverity && alarm.Severity <= _maxSeverity)
            .Where(alarm => query.Length == 0
                || alarm.Reference.Contains(query, StringComparison.OrdinalIgnoreCase)
                || alarm.Source.Contains(query, StringComparison.OrdinalIgnoreCase)
                || alarm.Description.Contains(query, StringComparison.OrdinalIgnoreCase))
            .OrderByDescending(alarm => alarm.Severity)
            .ThenByDescending(alarm => alarm.LastTransition ?? DateTimeOffset.MinValue)
            .ToArray();
    }

    private bool MatchesState(DashboardActiveAlarm alarm)
    {
        return alarm.State switch
        {
            AlarmConditionState.Active => _showActive,
            AlarmConditionState.ActiveAcked => _showAcked,
            _ => true,
        };
    }

    private static string StateText(AlarmConditionState state)
    {
        return state switch
        {
            AlarmConditionState.Active => "Active",
            AlarmConditionState.ActiveAcked => "Acked",
            AlarmConditionState.Inactive => "Inactive",
            _ => "Unknown",
        };
    }

    private static string StateClass(AlarmConditionState state)
    {
        return state switch
        {
            AlarmConditionState.Active => "alarm-state-active",
            AlarmConditionState.ActiveAcked => "alarm-state-acked",
            _ => "alarm-state-other",
        };
    }

    private async Task PollLoopAsync()
    {
        try
        {
            await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
            using PeriodicTimer timer = new(TimeSpan.FromSeconds(3));
            while (await timer.WaitForNextTickAsync(_cts.Token).ConfigureAwait(false))
            {
                await InvokeAsync(RefreshAlarmsAsync).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException)
        {
        }
    }

    private async Task RefreshAlarmsAsync()
    {
        DashboardAlarmQueryResult result = await LiveData.QueryAlarmsAsync(_cts.Token);
        _queryError = result.Error;
        _workerPid = result.WorkerProcessId;
        _lastRefresh = DateTimeOffset.UtcNow;
        _alarms.Clear();
        _alarms.AddRange(result.Alarms);
        _unackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.Active);
        _ackedCount = _alarms.Count(alarm => alarm.State == AlarmConditionState.ActiveAcked);
        StateHasChanged();
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        await _cts.CancelAsync();

        if (_alarmsHub is not null)
        {
            try
            {
                await _alarmsHub.DisposeAsync();
            }
            catch
            {
                // Disposal-time errors are best-effort.
            }
        }

        if (_pollTask is not null)
        {
            try
            {
                await _pollTask;
            }
            catch (OperationCanceledException)
            {
            }
        }

        _cts.Dispose();
        GC.SuppressFinalize(this);
    }
}
