using System.Collections.Concurrent;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

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

/// <summary>
/// Default <see cref="IAlarmSummaryService"/> implementation. Resolves
/// the site's Enabled instances, fans out one debug-snapshot fetch per instance
/// through the injected <see cref="IInstanceSnapshotClient"/> (capped at eight
/// concurrent fetches), and flattens every snapshot's alarm states into rows.
/// </summary>
/// <remarks>
/// Best-effort by design: a per-instance fetch that throws, is cancelled by its
/// own timeout, or reports <see cref="DebugViewSnapshot.InstanceNotFound"/> adds
/// the instance to <see cref="AlarmSummaryResult.NotReportingInstances"/> instead
/// of failing the whole call — an operator with one unreachable site still sees
/// every other instance's alarms. Caller cancellation
/// (<see cref="OperationCanceledException"/> on the supplied token) propagates.
/// </remarks>
public sealed class AlarmSummaryService : IAlarmSummaryService
{
    /// <summary>Max concurrent per-instance snapshot fetches.</summary>
    private const int MaxConcurrentFetches = 8;

    private readonly ITemplateEngineRepository _instanceRepo;
    private readonly ISiteRepository _siteRepo;
    private readonly IInstanceSnapshotClient _snapshotClient;

    /// <summary>
    /// Initializes a new instance of the <see cref="AlarmSummaryService"/> class.
    /// </summary>
    /// <param name="instanceRepo">Repository used to enumerate the site's instances.</param>
    /// <param name="siteRepo">Repository used to resolve the site identifier string.</param>
    /// <param name="snapshotClient">Single-shot per-instance snapshot client.</param>
    public AlarmSummaryService(
        ITemplateEngineRepository instanceRepo,
        ISiteRepository siteRepo,
        IInstanceSnapshotClient snapshotClient)
    {
        _instanceRepo = instanceRepo ?? throw new ArgumentNullException(nameof(instanceRepo));
        _siteRepo = siteRepo ?? throw new ArgumentNullException(nameof(siteRepo));
        _snapshotClient = snapshotClient ?? throw new ArgumentNullException(nameof(snapshotClient));
    }

    /// <inheritdoc/>
    public async Task<AlarmSummaryResult> GetSiteAlarmsAsync(
        int siteId, CancellationToken cancellationToken = default)
    {
        var site = await _siteRepo.GetSiteByIdAsync(siteId, cancellationToken);
        if (site is null)
        {
            return new AlarmSummaryResult(Array.Empty<AlarmSummaryRow>(), Array.Empty<string>());
        }

        var instances = await _instanceRepo.GetInstancesBySiteIdAsync(siteId, cancellationToken);
        var enabled = instances.Where(i => i.State == InstanceState.Enabled).ToList();
        if (enabled.Count == 0)
        {
            return new AlarmSummaryResult(Array.Empty<AlarmSummaryRow>(), Array.Empty<string>());
        }

        var rows = new ConcurrentBag<AlarmSummaryRow>();
        var notReporting = new ConcurrentBag<string>();

        using var gate = new SemaphoreSlim(MaxConcurrentFetches, MaxConcurrentFetches);

        var fetches = enabled.Select(instance => FetchInstanceAsync(
            site.SiteIdentifier, instance.UniqueName, gate, rows, notReporting, cancellationToken));
        await Task.WhenAll(fetches);

        // Deterministic ordering: instance name, then alarm name, so the page's
        // initial render and any test assertions are stable before client sorts.
        var orderedRows = rows
            .OrderBy(r => r.InstanceUniqueName, StringComparer.OrdinalIgnoreCase)
            .ThenBy(r => r.Alarm.AlarmName, StringComparer.OrdinalIgnoreCase)
            .ToList();
        var orderedNotReporting = notReporting
            .OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
            .ToList();

        return new AlarmSummaryResult(orderedRows, orderedNotReporting);
    }

    private async Task FetchInstanceAsync(
        string siteIdentifier,
        string instanceUniqueName,
        SemaphoreSlim gate,
        ConcurrentBag<AlarmSummaryRow> rows,
        ConcurrentBag<string> notReporting,
        CancellationToken cancellationToken)
    {
        await gate.WaitAsync(cancellationToken);
        try
        {
            var snapshot = await _snapshotClient.GetSnapshotAsync(
                siteIdentifier, instanceUniqueName, cancellationToken);

            if (snapshot.InstanceNotFound)
            {
                notReporting.Add(instanceUniqueName);
                return;
            }

            foreach (var alarm in snapshot.AlarmStates)
            {
                rows.Add(new AlarmSummaryRow(instanceUniqueName, alarm));
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // Caller-initiated cancel — propagate so the page can drop the response.
            throw;
        }
        catch
        {
            // Any other fault (per-instance timeout, transport error, the snapshot
            // Ask throwing) degrades this one instance to "not reporting" rather
            // than failing the whole summary.
            notReporting.Add(instanceUniqueName);
        }
        finally
        {
            gate.Release();
        }
    }

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

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

    /// <summary>
    /// Pure flattening of a live-cache snapshot into summary rows. Shared with
    /// <see cref="SharedAlarmSummaryService"/>, which memoizes the fan-out but must produce
    /// byte-identical rows on the live path — one implementation, no drift.
    /// </summary>
    /// <param name="alarms">The live-cache alarm snapshot.</param>
    /// <returns>The flattened, deterministically ordered rows (never any not-reporting names).</returns>
    internal static AlarmSummaryResult BuildFromLiveAlarmsCore(IReadOnlyList<AlarmStateChanged> alarms)
    {
        ArgumentNullException.ThrowIfNull(alarms);

        // Flatten one row per alarm (instance from the alarm itself, mirroring
        // FetchInstanceAsync) and apply the SAME deterministic instance-then-name
        // sort as GetSiteAlarmsAsync so the live and poll paths render identically.
        var orderedRows = alarms
            .Select(alarm => new AlarmSummaryRow(alarm.InstanceUniqueName, alarm))
            .OrderBy(r => r.InstanceUniqueName, StringComparer.OrdinalIgnoreCase)
            .ThenBy(r => r.Alarm.AlarmName, StringComparer.OrdinalIgnoreCase)
            .ToList();

        // Alarm rows alone cannot name the instances that failed to answer, so this pure
        // flattening leaves the list empty. The names come from the fan-out that produced the
        // snapshot: while the live cache is serving a site, SharedAlarmSummaryService pairs
        // these rows with ISiteAlarmLiveCache.GetNotReportingInstances (the aggregator's own
        // seed/reconcile result); while it is cold, GetSiteAlarmsAsync's poll supplies both.
        return new AlarmSummaryResult(orderedRows, Array.Empty<string>());
    }

    /// <summary>
    /// Pure roll-up over already-flattened rows. Shared with
    /// <see cref="SharedAlarmSummaryService"/> for the same one-implementation reason as
    /// <see cref="BuildFromLiveAlarmsCore"/>.
    /// </summary>
    /// <param name="rows">The rows to roll up.</param>
    /// <returns>The active/worst-severity/unacked/by-kind roll-up.</returns>
    internal static AlarmRollup ComputeRollupCore(IReadOnlyList<AlarmSummaryRow> rows)
    {
        ArgumentNullException.ThrowIfNull(rows);

        var totalActive = 0;
        var worstSeverity = 0;
        var unackedCount = 0;
        var countsByKind = new Dictionary<AlarmKind, int>();

        foreach (var row in rows)
        {
            var alarm = row.Alarm;
            countsByKind[alarm.Kind] = countsByKind.GetValueOrDefault(alarm.Kind) + 1;

            if (alarm.State == AlarmState.Active)
            {
                totalActive++;
                if (alarm.Condition.Severity > worstSeverity)
                {
                    worstSeverity = alarm.Condition.Severity;
                }
            }

            if (alarm.Condition.Active
                && !alarm.Condition.Acknowledged
                && alarm.Kind != AlarmKind.Computed)
            {
                unackedCount++;
            }
        }

        return new AlarmRollup(totalActive, worstSeverity, unackedCount, countsByKind);
    }
}
