using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;

/// <summary>
/// Production <see cref="ISiteEnumerator"/> backing the central
/// <see cref="SiteAuditReconciliationActor"/>. Enumerates the configured sites
/// from the config DB via <see cref="ISiteRepository.GetAllSitesAsync"/> and
/// projects each site to a <see cref="SiteEntry"/> using the site's
/// <c>SiteIdentifier</c> as the cursor key and its <c>GrpcNodeAAddress</c> as
/// the dial target.
/// </summary>
/// <remarks>
/// <para>
/// <b>Scope-per-call.</b> <see cref="ISiteRepository"/> is a SCOPED EF Core
/// service (registered by <c>AddConfigurationDatabase</c>); resolving it from
/// the root provider would fail DI scope validation. The enumerator therefore
/// takes the root <see cref="IServiceProvider"/> and opens one
/// <c>CreateAsyncScope</c> per <see cref="EnumerateAsync"/> call — mirroring the
/// per-tick scope pattern in <see cref="SiteAuditReconciliationActor.OnTickAsync"/>.
/// </para>
/// <para>
/// <b>Blank-address skip.</b> A site is skipped only when BOTH
/// <c>GrpcNodeAAddress</c> and <c>GrpcNodeBAddress</c> are blank: the
/// reconciliation pull cannot dial it, but absence of any address is a
/// configuration decision, not a runtime error (per the
/// <see cref="ISiteEnumerator"/> contract).
/// </para>
/// <para>
/// <b>NodeA/NodeB failover.</b> The primary dial target is NodeA when
/// configured, otherwise NodeB (a site with only NodeB is no longer skipped).
/// When a distinct NodeB address also exists it rides along as
/// <see cref="SiteEntry.FallbackGrpcEndpoint"/> so the pull clients can fail over
/// to it once per call when the primary raises a transport fault.
/// </para>
/// </remarks>
public sealed class SiteEnumerator : ISiteEnumerator
{
    private readonly IServiceProvider _services;

    /// <summary>
    /// Initializes the enumerator with the root service provider used to open a
    /// fresh DI scope per enumeration call.
    /// </summary>
    /// <param name="services">Root service provider for resolving the scoped <see cref="ISiteRepository"/>.</param>
    public SiteEnumerator(IServiceProvider services)
    {
        ArgumentNullException.ThrowIfNull(services);
        _services = services;
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<SiteEntry>> EnumerateAsync(CancellationToken ct = default)
    {
        await using var scope = _services.CreateAsyncScope();
        var repository = scope.ServiceProvider.GetRequiredService<ISiteRepository>();

        var sites = await repository.GetAllSitesAsync(ct).ConfigureAwait(false);

        var entries = new List<SiteEntry>(sites.Count);
        foreach (var site in sites)
        {
            // Primary dial target: NodeA when configured, otherwise NodeB —
            // a site with only a NodeB address is no longer skipped.
            var nodeA = site.GrpcNodeAAddress;
            var nodeB = site.GrpcNodeBAddress;
            var primary = !string.IsNullOrWhiteSpace(nodeA) ? nodeA : nodeB;

            if (string.IsNullOrWhiteSpace(primary))
            {
                // Both addresses blank — nothing to dial (a configuration
                // decision, not a runtime error).
                continue;
            }

            // NodeB rides along as the failover endpoint only when it is a
            // distinct, non-blank address (i.e. NodeA is the primary and NodeB
            // differs); when NodeB IS the primary there is no separate fallback.
            var fallback =
                !string.IsNullOrWhiteSpace(nodeB) && !string.Equals(nodeB, primary, StringComparison.Ordinal)
                    ? nodeB
                    : null;

            entries.Add(new SiteEntry(site.SiteIdentifier, primary!, fallback));
        }

        return entries;
    }
}
