using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories;

/// <summary>
/// WP2.6a (arch-review misc — site external-system resolution): the shared,
/// redeploy-invalidated snapshot backing <see cref="SiteExternalSystemRepository"/>'s
/// reads.
/// <para>
/// Registered as a DI <c>singleton</c> in production (<c>AddSiteRuntime</c>) so every
/// scoped <see cref="SiteExternalSystemRepository"/> instance — one per script
/// execution's DI scope — shares ONE snapshot instead of re-querying and
/// re-parsing the <c>external_systems</c> table (including its JSON
/// <c>method_definitions</c> column) on every call. The repository's default
/// constructor also accepts a private, unshared instance of this class so tests that
/// construct the repository directly keep seeing freshly-written rows without any
/// caching surprise.
/// </para>
/// <para>
/// Loaded lazily and wholesale on first read (one query covering every system's
/// definition AND its parsed method list, indexed by name and by
/// <see cref="SyntheticId"/> for O(1) by-ID lookups) and dropped wholesale by
/// <see cref="InvalidateAll"/> — called by <c>DeploymentManagerActor</c> after an
/// artifact deploy applies new/changed/removed external systems, so the cache never
/// serves a stale method list or a deleted system past the next read.
/// </para>
/// </summary>
public sealed class ExternalSystemDefinitionCache
{
    /// <summary>
    /// One fully-loaded, immutable view of the site's external-system catalogue.
    /// </summary>
    /// <param name="ByName">System definitions keyed by name (ordinal).</param>
    /// <param name="IdToName">Synthetic system ID → name, for O(1) by-ID resolution.</param>
    /// <param name="MethodsByName">Parsed method list per system name.</param>
    /// <param name="MethodIdIndex">Synthetic method ID → (system name, method name), for O(1) by-ID resolution.</param>
    internal sealed record Snapshot(
        IReadOnlyDictionary<string, ExternalSystemDefinition> ByName,
        IReadOnlyDictionary<int, string> IdToName,
        IReadOnlyDictionary<string, IReadOnlyList<ExternalSystemMethod>> MethodsByName,
        IReadOnlyDictionary<int, (string SystemName, string MethodName)> MethodIdIndex);

    // Guards concurrent cache-miss loads so a burst of first-callers after a redeploy
    // collapses onto a single SQLite read rather than each racing its own full scan.
    private readonly SemaphoreSlim _loadGate = new(1, 1);

    // Reference reads/writes of a class type are already atomic in .NET; volatile only
    // adds the memory-visibility guarantee across threads, which is all the
    // check-then-load double-check pattern below needs.
    private volatile Snapshot? _snapshot;

    /// <summary>
    /// Drops the cached snapshot. The next read reloads a fresh one from storage.
    /// Safe to call at any time, including with no snapshot yet loaded.
    /// </summary>
    public void InvalidateAll() => _snapshot = null;

    /// <summary>
    /// Returns the cached snapshot, loading it via <paramref name="loader"/> on a
    /// cache miss (first read, or the first read after <see cref="InvalidateAll"/>).
    /// Concurrent misses collapse onto a single load.
    /// </summary>
    /// <param name="loader">Loads a fresh snapshot from storage; invoked at most once per miss.</param>
    /// <param name="cancellationToken">Cancellation token for the load.</param>
    internal async Task<Snapshot> GetOrLoadAsync(
        Func<CancellationToken, Task<Snapshot>> loader, CancellationToken cancellationToken)
    {
        var snapshot = _snapshot;
        if (snapshot != null)
            return snapshot;

        await _loadGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            // Re-check: another caller may have already loaded while we waited.
            snapshot = _snapshot;
            if (snapshot != null)
                return snapshot;

            snapshot = await loader(cancellationToken).ConfigureAwait(false);
            _snapshot = snapshot;
            return snapshot;
        }
        finally
        {
            _loadGate.Release();
        }
    }
}
