using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;

/// <summary>
/// Base class for Blazor dashboard pages that watch gateway metrics snapshots.
/// Pages subscribe to the in-process <see cref="IDashboardSnapshotFeed"/>, which
/// multicasts a single <see cref="IDashboardSnapshotService.WatchSnapshotsAsync"/>
/// enumeration to every circuit. An earlier implementation had each page open its
/// own SignalR connection to <c>/hubs/snapshot</c> — a loopback WebSocket back into
/// this same process, per page. The snapshot hub and its publisher remain for
/// external (non-circuit) clients; server-rendered pages no longer use them.
/// </summary>
public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
{
    /// <summary>
    /// Upper bound on waiting for the watch loop while disposing. The loop marshals
    /// renders through the renderer's dispatcher and disposal can run on that same
    /// dispatcher, so the wait is bounded rather than unconditional.
    /// </summary>
    private static readonly TimeSpan WatchDrainTimeout = TimeSpan.FromSeconds(5);

    /// <summary>
    /// Delay between a feed subscription ending and the resubscribe that replaces it.
    /// Long enough that a feed failing on every attempt cannot spin, short enough that
    /// the page is stale for about a snapshot interval rather than until navigation.
    /// </summary>
    private static readonly TimeSpan ResubscribeDelay = TimeSpan.FromSeconds(1);

    private readonly CancellationTokenSource _watchCancellation = new();
    private Task? _watchTask;

    /// <summary>Snapshot service used to seed the initial render before the first feed update.</summary>
    [Inject]
    protected IDashboardSnapshotService SnapshotService { get; set; } = null!;

    /// <summary>Shared in-process snapshot feed this page renders from.</summary>
    [Inject]
    protected IDashboardSnapshotFeed SnapshotFeed { get; set; } = null!;

    /// <summary>Logger used to report a snapshot subscription that ended or would not drain.</summary>
    [Inject]
    protected ILogger<DashboardPageBase>? Logger { get; set; }

    /// <summary>
    /// The most recent gateway metric snapshot. Synchronously seeded from
    /// <see cref="IDashboardSnapshotService.GetSnapshot"/> for the very first
    /// render, then refreshed from the feed.
    /// </summary>
    protected DashboardSnapshot? Snapshot { get; private set; }

    /// <inheritdoc />
    protected override Task OnInitializedAsync()
    {
        Snapshot = SnapshotService.GetSnapshot();

        // Deliberately not awaited: the watch loop runs for the lifetime of the page
        // and is cancelled and drained by DisposeAsync.
        _watchTask = WatchSnapshotsAsync(_watchCancellation.Token);
        return Task.CompletedTask;
    }

    /// <summary>Cancels the snapshot subscription created for this page, tolerating disposal-time errors.</summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async ValueTask DisposeAsync()
    {
        try
        {
            await _watchCancellation.CancelAsync().ConfigureAwait(false);

            if (_watchTask is not null)
            {
                await _watchTask.WaitAsync(WatchDrainTimeout).ConfigureAwait(false);
            }
        }
        catch (TimeoutException)
        {
            // Accepted limitation: the abandoned loop still holds its feed subscription, so
            // the feed's idle gate stays open until it does unwind. There is no way to force
            // a detach — the loop is parked on a dispatcher that is not draining — so the
            // warning is the operator's only signal that a circuit teardown wedged.
            Logger?.LogWarning(
                "Dashboard page {Page} did not release its snapshot subscription within {Timeout}; "
                + "the shared snapshot feed stays active until it unwinds.",
                GetType().Name,
                WatchDrainTimeout);
        }
        catch
        {
            // Other disposal-time errors are best-effort.
        }

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

    /// <summary>
    /// Renders every snapshot the feed yields, resubscribing whenever the subscription ends
    /// for any reason other than this page going away.
    /// </summary>
    /// <remarks>
    /// The feed detaches a subscriber when its pump's source faults or completes, so a single
    /// enumeration is not a lifetime: without the outer loop the first fault froze the page on
    /// its last snapshot until the operator navigated. Resubscribing is also what restarts the
    /// feed — only a subscriber that finds no live generation starts a pump — so the page is
    /// the recovery path, not merely its beneficiary.
    /// </remarks>
    /// <param name="cancellationToken">Cancelled by <see cref="DisposeAsync"/> when the page goes away.</param>
    /// <returns>A task that completes when the page is disposed.</returns>
    private async Task WatchSnapshotsAsync(CancellationToken cancellationToken)
    {
        // One log line per fault *transition*, not per retry: a feed that is down stays down
        // for many iterations, and a warning per second per open page is noise, not signal.
        bool faultLogged = false;

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await foreach (DashboardSnapshot snapshot in SnapshotFeed
                    .WatchAsync(cancellationToken)
                    .ConfigureAwait(false))
                {
                    Snapshot = snapshot;
                    faultLogged = false;
                    await InvokeAsync(StateHasChanged).ConfigureAwait(false);
                }
            }
            catch (OperationCanceledException)
            {
                // The page is going away.
                return;
            }
            catch (Exception error) when (!faultLogged)
            {
                // The last rendered snapshot stays on screen while the retry runs, and the
                // snapshot service keeps serving GetSnapshot() for the next page load.
                faultLogged = true;
                Logger?.LogWarning(
                    error,
                    "Live snapshot updates failed for dashboard page {Page}; retrying every {Delay}. "
                    + "It keeps the last rendered snapshot until they resume.",
                    GetType().Name,
                    ResubscribeDelay);
            }
            catch (Exception)
            {
                // Same fault, already reported above; the retry below is unconditional.
            }

            // The enumeration's own disposal (run by the await foreach on every exit path)
            // is what releases the dead subscription, so the delay below is only paced —
            // there is nothing left of the old subscription to unwind here.
            try
            {
                await Task.Delay(ResubscribeDelay, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }
}
