using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Notification;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Communication;

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

/// <summary>
/// Default <see cref="IKpiSnapshotCache"/> — a DI <b>singleton</b> holding one
/// <see cref="SingleFlightMemo{T}"/> per KPI query.
/// <para>
/// Registered as a singleton on purpose: the whole point is that the memo outlives any one
/// circuit. It depends only on <see cref="CommunicationService"/> (itself a singleton) and
/// <see cref="IServiceScopeFactory"/> — the audit KPI resolves its scoped query service from
/// a fresh scope per flight, the same shape <c>AuditLogQueryService</c> uses internally so a
/// KPI round never shares a circuit-scoped <c>DbContext</c>.
/// </para>
/// </summary>
public sealed class KpiSnapshotCache : IKpiSnapshotCache
{
    /// <summary>
    /// Freshness window. Sized just under the Health dashboard's 10s tick so each tick still
    /// yields ONE fresh query for the whole node (rather than one per circuit), while a
    /// circuit whose tick lands mid-window is served the memoized value instead of duplicating it.
    /// </summary>
    internal static readonly TimeSpan Ttl = TimeSpan.FromSeconds(8);

    private readonly CommunicationService _communication;
    private readonly IServiceScopeFactory _scopeFactory;

    private readonly SingleFlightMemo<NotificationKpiResponse> _notificationKpis;
    private readonly SingleFlightMemo<PerSiteNotificationKpiResponse> _perSiteNotificationKpis;
    private readonly SingleFlightMemo<PerNodeNotificationKpiResponse> _perNodeNotificationKpis;
    private readonly SingleFlightMemo<SiteCallKpiResponse> _siteCallKpis;
    private readonly SingleFlightMemo<PerNodeSiteCallKpiResponse> _perNodeSiteCallKpis;
    private readonly SingleFlightMemo<AuditLogKpiSnapshot> _auditKpis;

    /// <summary>
    /// Initializes the shared KPI snapshot cache.
    /// </summary>
    /// <param name="communication">Central-side comms used to Ask the outbox / site-call singletons.</param>
    /// <param name="scopeFactory">Opens a fresh DI scope per audit-KPI flight.</param>
    public KpiSnapshotCache(CommunicationService communication, IServiceScopeFactory scopeFactory)
        : this(communication, scopeFactory, Ttl, clock: null)
    {
    }

    /// <summary>
    /// Test seam: same cache with an explicit TTL and clock so freshness can be asserted
    /// without real delays.
    /// </summary>
    /// <param name="communication">Central-side comms used to Ask the outbox / site-call singletons.</param>
    /// <param name="scopeFactory">Opens a fresh DI scope per audit-KPI flight.</param>
    /// <param name="ttl">Freshness window.</param>
    /// <param name="clock">Clock used for freshness.</param>
    internal KpiSnapshotCache(
        CommunicationService communication,
        IServiceScopeFactory scopeFactory,
        TimeSpan ttl,
        Func<DateTimeOffset>? clock)
    {
        _communication = communication ?? throw new ArgumentNullException(nameof(communication));
        _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));

        _notificationKpis = new SingleFlightMemo<NotificationKpiResponse>(ttl, clock);
        _perSiteNotificationKpis = new SingleFlightMemo<PerSiteNotificationKpiResponse>(ttl, clock);
        _perNodeNotificationKpis = new SingleFlightMemo<PerNodeNotificationKpiResponse>(ttl, clock);
        _siteCallKpis = new SingleFlightMemo<SiteCallKpiResponse>(ttl, clock);
        _perNodeSiteCallKpis = new SingleFlightMemo<PerNodeSiteCallKpiResponse>(ttl, clock);
        _auditKpis = new SingleFlightMemo<AuditLogKpiSnapshot>(ttl, clock);
    }

    /// <inheritdoc/>
    public Task<NotificationKpiResponse> GetNotificationKpisAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _notificationKpis.GetAsync(
            () => _communication.GetNotificationKpisAsync(new NotificationKpiRequest(NewCorrelationId())),
            forceRefresh, cancellationToken);

    /// <inheritdoc/>
    public Task<PerSiteNotificationKpiResponse> GetPerSiteNotificationKpisAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _perSiteNotificationKpis.GetAsync(
            () => _communication.GetPerSiteNotificationKpisAsync(new PerSiteNotificationKpiRequest(NewCorrelationId())),
            forceRefresh, cancellationToken);

    /// <inheritdoc/>
    public Task<PerNodeNotificationKpiResponse> GetPerNodeNotificationKpisAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _perNodeNotificationKpis.GetAsync(
            () => _communication.GetPerNodeNotificationKpisAsync(new PerNodeNotificationKpiRequest(NewCorrelationId())),
            forceRefresh, cancellationToken);

    /// <inheritdoc/>
    public Task<SiteCallKpiResponse> GetSiteCallKpisAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _siteCallKpis.GetAsync(
            () => _communication.GetSiteCallKpisAsync(new SiteCallKpiRequest(NewCorrelationId())),
            forceRefresh, cancellationToken);

    /// <inheritdoc/>
    public Task<PerNodeSiteCallKpiResponse> GetPerNodeSiteCallKpisAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _perNodeSiteCallKpis.GetAsync(
            () => _communication.GetPerNodeSiteCallKpisAsync(new PerNodeSiteCallKpiRequest(NewCorrelationId())),
            forceRefresh, cancellationToken);

    /// <inheritdoc/>
    public Task<AuditLogKpiSnapshot> GetAuditKpiSnapshotAsync(
        bool forceRefresh = false, CancellationToken cancellationToken = default) =>
        _auditKpis.GetAsync(LoadAuditKpisAsync, forceRefresh, cancellationToken);

    private async Task<AuditLogKpiSnapshot> LoadAuditKpisAsync()
    {
        await using var scope = _scopeFactory.CreateAsyncScope();
        var queryService = scope.ServiceProvider.GetRequiredService<IAuditLogQueryService>();
        return await queryService.GetKpiSnapshotAsync();
    }

    private static string NewCorrelationId() => Guid.NewGuid().ToString("N");
}
