using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;

namespace ZB.MOM.WW.ScadaBridge.Communication;

/// <summary>
/// Central, transient, in-memory per-site live alarm cache (plan #10, Task 4).
/// A DI singleton shared by every Alarm Summary Blazor circuit on the active central
/// node. For each site with at least one active viewer it runs ONE shared per-site
/// aggregator that seeds from the existing debug-snapshot fan-out and stays warm on a
/// single site-wide, alarm-only gRPC stream (<c>SubscribeSite</c>), so the page
/// reflects alarm transitions in near-real-time instead of re-polling every 15s.
/// <para>
/// <b>Hard constraint (locked <c>[PERM]</c>):</b> there is NO persisted central alarm
/// store. This cache is purely in-memory, per-node — no EF entity/table/migration backs
/// it. In routine operation only the active node (behind Traefik) hosts viewers, so only
/// it runs aggregators; browsing the standby node directly (diagnostic ports) starts an
/// independent read-only aggregator there — accepted (arch review 02 round 2, N8):
/// bounded (one stream/site, viewer-capped), read-only, and torn down by the viewer
/// linger. On a NodeA↔NodeB failover the new active node re-seeds from scratch.
/// </para>
/// <para>
/// <b>Reference-counted lifecycle:</b> the first <see cref="Subscribe"/> for a site
/// starts its aggregator (open stream + snapshot seed); the last subscriber leaving
/// stops it after a short linger (see
/// <see cref="CommunicationOptions.LiveAlarmCacheLinger"/>) to avoid re-seed thrash.
/// One gRPC stream per <em>site</em>, not per browser circuit.
/// </para>
/// </summary>
public interface ISiteAlarmLiveCache
{
    /// <summary>
    /// Registers a viewer for a site's live alarm feed and returns an
    /// <see cref="IDisposable"/> that unregisters it. The first subscriber for a site
    /// starts the shared aggregator; the last subscriber's <see cref="IDisposable.Dispose"/>
    /// schedules a linger-delayed stop. <paramref name="onChanged"/> is raised (on the
    /// aggregator's thread) after each applied change — the handler must not block and
    /// should marshal any UI work onto its own dispatcher (mirrors
    /// <c>IDeploymentStatusNotifier</c>). Disposing the returned handle is idempotent.
    /// </summary>
    /// <param name="siteId">The numeric site id whose alarms the viewer wants.</param>
    /// <param name="onChanged">Callback invoked whenever the site's cached alarm set changes.</param>
    /// <returns>A disposable that unregisters this viewer (idempotent).</returns>
    IDisposable Subscribe(int siteId, Action onChanged);

    /// <summary>
    /// Returns the current in-memory alarm snapshot for a site — exactly what the page
    /// should render. Empty when the site has no aggregator or has not yet completed its
    /// first seed. Safe to call from Blazor render threads: it returns an immutable
    /// point-in-time list (the aggregator publishes fresh immutable lists; readers never
    /// see a partially-mutated collection).
    /// </summary>
    /// <param name="siteId">The numeric site id.</param>
    /// <returns>The current cached alarms (possibly empty), never <c>null</c>.</returns>
    IReadOnlyList<AlarmStateChanged> GetCurrentAlarms(int siteId);

    /// <summary>
    /// Whether the site's aggregator has completed its initial seed and is serving live
    /// state. The page can use this to decide whether to keep its 15s poll fallback
    /// (Task 5): <c>false</c> means "not live yet — keep polling"; <c>true</c> means the
    /// cache is authoritative.
    /// </summary>
    /// <param name="siteId">The numeric site id.</param>
    /// <returns><c>true</c> while a <b>living</b> aggregator has seeded and published; reverts to false if the aggregator terminates (R2 N6) — a frozen snapshot is never reported as live.</returns>
    bool IsLive(int siteId);

    /// <summary>
    /// The Enabled instances on the site that failed to answer the aggregator's most recent
    /// snapshot fan-out (seed or reconcile) — i.e. that reported <c>InstanceNotFound</c>, timed
    /// out, or faulted. Deterministically ordered by instance name, and empty when the site has
    /// no aggregator or has not yet completed a fan-out.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Published alongside the alarm snapshot on every fan-out, because the aggregator already
    /// knows it: the seed/reconcile is the SAME per-instance debug-snapshot fan-out the Alarm
    /// Summary poll used to run a second time purely to compute this list. Exposing it here lets
    /// the UI-side poll be skipped entirely while <see cref="IsLive"/> is true, which is the
    /// whole point — one fan-out per site per reconcile window, not two.
    /// </para>
    /// <para>
    /// It is refreshed at the aggregator's fan-out cadence (seed, then each reconcile), NOT on
    /// every stream delta: an instance going silent is precisely what the stream cannot tell you.
    /// Callers must therefore treat it as reconcile-fresh, not delta-fresh — which matches the
    /// poll's own 15 s freshness, so nothing on screen changes.
    /// </para>
    /// </remarks>
    /// <param name="siteId">The numeric site id.</param>
    /// <returns>The not-reporting instance unique names (possibly empty), never <c>null</c>.</returns>
    IReadOnlyList<string> GetNotReportingInstances(int siteId);
}
