@page "/script-log"
@* Live script-log tail via SignalR. Subscribes to /hubs/script-log and shows entries from
   VirtualTagActor / ScriptedAlarmActor script execution. Engine emit lands with F8 + F9. *@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using ZB.MOM.WW.OtOpcUa.AdminUI.Hubs
@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging
@inject IInProcessBroadcaster<ScriptLogEntry> ScriptLogs
@implements IDisposable

<div class="d-flex justify-content-between align-items-center mb-3">
    <h4 class="mb-0">Script log</h4>
    <div class="d-flex align-items-center gap-2">
        <select class="form-select form-select-sm" style="width:auto" @bind="_levelFilter">
            <option value="">All levels</option>
            <option value="Trace">Trace+</option>
            <option value="Debug">Debug+</option>
            <option value="Information">Information+</option>
            <option value="Warning">Warning+</option>
            <option value="Error">Error+</option>
        </select>
        <input type="text" class="form-control form-control-sm" style="width:200px"
               placeholder="Filter script ID…" @bind="_scriptFilter" @bind:event="oninput" />
        <span class="conn-pill" data-state="@(_connected ? "connected" : "disconnected")">
            <span class="dot"></span><span>@(_connected ? "live" : "disconnected")</span>
        </span>
        <button class="btn btn-sm btn-outline-secondary" @onclick="ClearAsync">Clear</button>
    </div>
</div>

<section class="panel notice rise" style="animation-delay:.02s">
    Live tail of <span class="mono">script-logs</span> DPS topic, capped at @Capacity entries.
    Filter by minimum level + script ID. Sources: VirtualTagActor (F8), ScriptedAlarmActor (F9).
</section>

@if (VisibleRows.Count == 0)
{
    <section class="panel notice rise mt-3" style="animation-delay:.08s">
        @if (_rows.Count == 0)
        {
            <span>No script-log entries yet. Engine emit (F8/F9) is pending.</span>
        }
        else
        {
            <span>No entries match the current filter (@_rows.Count entries available).</span>
        }
    </section>
}
else
{
    <section class="panel rise mt-3" style="animation-delay:.08s">
        <div class="panel-head">Showing @VisibleRows.Count of @_rows.Count</div>
        <div class="table-wrap">
            <table class="data-table">
                <thead>
                    <tr>
                        <th>Time</th>
                        <th>Level</th>
                        <th>Script</th>
                        <th>Context</th>
                        <th>Message</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var e in VisibleRows)
                    {
                        <tr>
                            <td><span class="mono small">@e.TimestampUtc.ToString("HH:mm:ss.fff")</span></td>
                            <td><span class="chip @LevelChipClass(e.Level)">@e.Level</span></td>
                            <td><span class="mono small">@e.ScriptId</span></td>
                            <td class="text-muted small">
                                @if (!string.IsNullOrEmpty(e.VirtualTagId)) { <span>vtag=@e.VirtualTagId</span> }
                                @if (!string.IsNullOrEmpty(e.AlarmId)) { <span class="ms-1">alarm=@e.AlarmId</span> }
                                @if (!string.IsNullOrEmpty(e.EquipmentId)) { <span class="ms-1">eq=@e.EquipmentId</span> }
                            </td>
                            <td><span class="mono small">@e.Message</span></td>
                        </tr>
                    }
                </tbody>
            </table>
        </div>
    </section>
}

@code {
    private const int Capacity = 500;

    private readonly List<ScriptLogEntry> _rows = new();
    private bool _connected;
    private string _levelFilter = "";
    private string _scriptFilter = "";

    private static readonly Dictionary<string, int> LevelRank = new(StringComparer.OrdinalIgnoreCase)
    {
        ["Trace"] = 0, ["Debug"] = 1, ["Information"] = 2, ["Warning"] = 3, ["Error"] = 4, ["Critical"] = 5,
    };

    private List<ScriptLogEntry> VisibleRows
    {
        get
        {
            IEnumerable<ScriptLogEntry> q = _rows;
            if (!string.IsNullOrWhiteSpace(_levelFilter)
                && LevelRank.TryGetValue(_levelFilter, out var minRank))
            {
                q = q.Where(e => LevelRank.TryGetValue(e.Level, out var r) && r >= minRank);
            }
            if (!string.IsNullOrWhiteSpace(_scriptFilter))
            {
                q = q.Where(e => e.ScriptId.Contains(_scriptFilter, StringComparison.OrdinalIgnoreCase));
            }
            return q.ToList();
        }
    }

    protected override void OnInitialized()
    {
        // Live tail straight from the in-process broadcaster (fed by ScriptLogSignalRBridge off the
        // 'script-logs' DPS topic). Blazor Server can't self-connect a SignalR HubConnection behind
        // a reverse proxy — see IInProcessBroadcaster — so we subscribe in-process instead.
        ScriptLogs.Received += OnEntry;
        _connected = true;
    }

    private void OnEntry(ScriptLogEntry entry) =>
        // Marshal both the mutation and the re-render onto the circuit sync context so this can't
        // race ClearAsync (which runs there) over the shared _rows list.
        InvokeAsync(() =>
        {
            _rows.Insert(0, entry);
            if (_rows.Count > Capacity) _rows.RemoveAt(_rows.Count - 1);
            StateHasChanged();
        });

    private async Task ClearAsync()
    {
        _rows.Clear();
        await InvokeAsync(StateHasChanged);
    }

    private static string LevelChipClass(string level) => level switch
    {
        "Critical" or "Error" => "chip-alert",
        "Warning" => "chip-caution",
        "Information" => "chip-idle",
        _ => "chip-idle",
    };

    public void Dispose() => ScriptLogs.Received -= OnEntry;
}
