@implements IDisposable
@inject NavigationManager Navigation
@inject IJSRuntime JS

@code {
    // CentralUI-005 / CentralUI-020: session expiry is a sliding window owned by
    // the cookie authentication middleware (ZB.MOM.WW.ScadaBridge.Security AddCookie:
    // ExpireTimeSpan = idle timeout, SlidingExpiration = true). An active user's
    // cookie is continually renewed; an idle user's cookie lapses after the idle
    // timeout. There is no fixed login-time deadline to redirect at.
    //
    // This component must NOT poll the Blazor AuthenticationStateProvider:
    // CookieAuthenticationStateProvider serves a frozen constructor-time
    // principal for the whole circuit (CentralUI-004), so the polled auth state
    // can never transition to "expired" and the redirect would never fire
    // (CentralUI-020).
    //
    // Instead it polls the server endpoint GET /auth/ping via fetch(). Being a
    // normal HTTP request, the cookie middleware re-validates — and slides — the
    // cookie on every hit, and answers 200 while the session is live or 401 once
    // it has lapsed. A genuine idle user's circuit produces no other HTTP
    // traffic, so once the cookie lapses the next ping returns 401 and this
    // component redirects to /login. (The ping itself slides the cookie, but the
    // poll interval is well under the idle timeout, so an idle session still
    // lapses on schedule once the poll catches the lapsed state — the ping only
    // ever observes expiry, it does not keep a dead session alive.)

    /// <summary>Server endpoint that reports live session validity.</summary>
    internal const string PingUrl = "/auth/ping";

    /// <summary>HTTP status returned by <see cref="PingUrl"/> once the cookie has lapsed.</summary>
    private const int Unauthorized = 401;

    private const string ModulePath = "./_content/ZB.MOM.WW.ScadaBridge.CentralUI/js/session-expiry.js";

    /// <summary>How often the session validity is re-checked.</summary>
    internal static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(1);

    private CancellationTokenSource? _cts;
    private IJSObjectReference? _module;

    protected override void OnInitialized()
    {
        // The login page uses the same layout, so this component renders there
        // too. Polling/redirecting on /login → /login would loop.
        if (IsOnLoginPage) return;

        _cts = new CancellationTokenSource();
        _ = PollSessionAsync(_cts.Token);
    }

    private bool IsOnLoginPage =>
        Navigation.ToBaseRelativePath(Navigation.Uri)
            .StartsWith("login", StringComparison.OrdinalIgnoreCase);

    private async Task PollSessionAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            try { await Task.Delay(PollInterval, token); }
            catch (TaskCanceledException) { return; }

            if (token.IsCancellationRequested) return;
            await CheckSessionAsync();
        }
    }

    /// <summary>
    /// Runs one liveness check: pings the server and, if the session has lapsed
    /// server-side (HTTP 401), redirects to the login page. Exposed for tests
    /// (CentralUI-025) so the redirect path can be exercised without waiting on
    /// the poll interval.
    /// </summary>
    internal async Task CheckSessionAsync()
    {
        if (IsOnLoginPage) return;

        int status;
        try
        {
            _module ??= await JS.InvokeAsync<IJSObjectReference>("import", ModulePath);
            status = await _module.InvokeAsync<int>("ping", PingUrl);
        }
        catch (JSDisconnectedException)
        {
            // Circuit gone — nothing to redirect.
            return;
        }
        catch (JSException)
        {
            // Network blip or fetch failure: treat as inconclusive and retry on
            // the next poll rather than logging an authenticated user out on a
            // transient error.
            return;
        }

        if (status == Unauthorized)
        {
            await InvokeAsync(() => Navigation.NavigateTo("/login", forceLoad: true));
        }
    }

    public void Dispose()
    {
        _cts?.Cancel();
        _cts?.Dispose();
        // The module reference is owned by the circuit's JS runtime; once the
        // circuit is disposed disposing it would throw — fire-and-forget and
        // swallow the expected disconnect.
        if (_module is not null)
        {
            _ = DisposeModuleAsync(_module);
        }
    }

    private static async Task DisposeModuleAsync(IJSObjectReference module)
    {
        try { await module.DisposeAsync(); }
        catch (JSDisconnectedException) { /* circuit already gone */ }
    }
}
