using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Communication;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;

/// <summary>
/// Process-level memoizing façade over <see cref="AlarmSummaryService"/> (arch-review WP2.4).
/// <para>
/// The Alarm Summary page polls every 15s <em>per circuit</em>, and each poll fans one debug
/// snapshot Ask out to every Enabled instance on the site. Ten operators watching one site
/// meant ten independent fan-outs per 15s across the same instances — on top of the shared
/// live-alarm aggregator already seeding and reconciling from the identical fan-out. The
/// question is not user-specific, so this service collapses it: one memo slot per site,
/// single-flight, so the node costs ONE fan-out per site per window no matter how many
/// viewers are watching.
/// </para>
/// <para>
/// <b>While the live cache is serving the site there is NO fan-out at all.</b> The page already
/// ignores the poll's alarm rows in that state (the live deltas own them — arch-review R2 N5),
/// and the aggregator's own seed/reconcile fan-out now publishes the not-reporting set through
/// <see cref="ISiteAlarmLiveCache.GetNotReportingInstances"/> — the one thing the poll used to
/// still supply. So the live path is answered entirely from the cache: rows flattened from
/// <see cref="ISiteAlarmLiveCache.GetCurrentAlarms"/> through the same
/// <c>AlarmSummaryService.BuildFromLiveAlarmsCore</c> the page's live subscription uses, plus the
/// cached not-reporting names. One fan-out per site per reconcile window, run by the aggregator,
/// instead of two.
/// </para>
/// <para>
/// The rows are returned rather than left empty on purpose: liveness can flip to false in the
/// window between this call and the page's own <c>IsLive</c> re-check, and the page would then
/// adopt whatever came back. Handing it the live snapshot makes that race land on last-known
/// state instead of a blank grid.
/// </para>
/// <para>
/// While the cache is cold the poll is the page's full-rebuild safety net, so it still fans out,
/// memoized with a window just under the page's 15s tick so every tick gets fresh data.
/// </para>
/// </summary>
public sealed class SharedAlarmSummaryService : IAlarmSummaryService
{
    /// <summary>
    /// Freshness window while the live alarm cache is NOT serving this site: the poll is the
    /// page's authoritative rebuild path, so it must stay under the 15s page tick.
    /// </summary>
    internal static readonly TimeSpan ColdCacheTtl = TimeSpan.FromSeconds(12);

    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ISiteAlarmLiveCache _liveCache;
    private readonly Func<DateTimeOffset>? _clock;

    private readonly ConcurrentDictionary<int, SingleFlightMemo<AlarmSummaryResult>> _bySite = new();

    /// <summary>
    /// Initializes the shared alarm summary façade.
    /// </summary>
    /// <remarks>
    /// There is no live-cache freshness window any more: the live path reads the aggregator's
    /// cache directly and never fans out, so the only window left is <see cref="ColdCacheTtl"/>.
    /// The reconcile interval this ctor used to read from <c>CommunicationOptions</c> is now
    /// implicit — it IS the cadence at which the aggregator refreshes what the live path returns.
    /// </remarks>
    /// <param name="scopeFactory">Opens a fresh DI scope per fan-out (fresh repositories, off any circuit scope).</param>
    /// <param name="liveCache">The shared live alarm cache: liveness, alarm rows, and not-reporting names.</param>
    public SharedAlarmSummaryService(
        IServiceScopeFactory scopeFactory,
        ISiteAlarmLiveCache liveCache)
        : this(scopeFactory, liveCache, clock: null)
    {
    }

    /// <summary>
    /// Test seam: same façade with an explicit clock.
    /// </summary>
    /// <param name="scopeFactory">Opens a fresh DI scope per fan-out.</param>
    /// <param name="liveCache">The shared live alarm cache.</param>
    /// <param name="clock">Clock used for freshness.</param>
    internal SharedAlarmSummaryService(
        IServiceScopeFactory scopeFactory,
        ISiteAlarmLiveCache liveCache,
        Func<DateTimeOffset>? clock)
    {
        _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
        _liveCache = liveCache ?? throw new ArgumentNullException(nameof(liveCache));
        _clock = clock;
    }

    /// <inheritdoc/>
    public Task<AlarmSummaryResult> GetSiteAlarmsAsync(
        int siteId, CancellationToken cancellationToken = default)
    {
        // Live: answer from the shared aggregator's cache and skip the fan-out entirely. Not
        // memoized — there is nothing to amortize, and reading the cache directly keeps the
        // answer as fresh as the aggregator itself.
        if (_liveCache.IsLive(siteId))
        {
            var live = AlarmSummaryService.BuildFromLiveAlarmsCore(_liveCache.GetCurrentAlarms(siteId));
            return Task.FromResult(new AlarmSummaryResult(
                live.Alarms, _liveCache.GetNotReportingInstances(siteId)));
        }

        var memo = _bySite.GetOrAdd(
            siteId,
            _ => new SingleFlightMemo<AlarmSummaryResult>(ColdCacheTtl, _clock));

        return memo.GetAsync(
            () => FanOutAsync(siteId),
            forceRefresh: false,
            cancellationToken,
            ttlOverride: ColdCacheTtl);
    }

    /// <inheritdoc/>
    public AlarmSummaryResult BuildFromLiveAlarms(IReadOnlyList<AlarmStateChanged> alarms) =>
        AlarmSummaryService.BuildFromLiveAlarmsCore(alarms);

    /// <inheritdoc/>
    public AlarmRollup ComputeRollup(IReadOnlyList<AlarmSummaryRow> rows) =>
        AlarmSummaryService.ComputeRollupCore(rows);

    /// <summary>
    /// One shared fan-out. Deliberately runs with no caller cancellation token: the flight is
    /// shared, so one circuit navigating away must not cancel the round the others are awaiting.
    /// </summary>
    private async Task<AlarmSummaryResult> FanOutAsync(int siteId)
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var inner = scope.ServiceProvider.GetRequiredService<AlarmSummaryService>();
        return await inner.GetSiteAlarmsAsync(siteId, CancellationToken.None);
    }
}
