using System.Collections.Concurrent;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;

namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;

/// <summary>
/// <see cref="IStaleInstanceProbe"/> backed by the deployment flattening pipeline.
/// Re-flattens an instance through <see cref="IFlatteningPipeline"/> and returns
/// the computed revision hash — the exact same value
/// <c>DeploymentService.GetDeploymentComparisonAsync</c> compares against the
/// deployed snapshot to decide staleness. Hosted in DeploymentManager so the
/// Transport component (which references only Commons + TemplateEngine) can probe
/// staleness without taking a DeploymentManager project reference.
///
/// <para>
/// <b>Watermark fast path.</b> A bundle import probes EVERY instance derived from
/// an overwritten template, and the Deployments page sweeps the fleet — both pay a
/// full flatten per instance for an answer that is usually "unchanged". A probe
/// result is therefore memoised against the
/// <see cref="ITemplateGraphWatermark"/> reading that produced it: the instance's
/// own version, the graph's structure version (which covers chain MEMBERSHIP), and
/// the version of every template the flatten actually walked. When all of those are
/// unchanged the recorded hash is still current by construction and the flatten is
/// skipped.
/// </para>
///
/// <para>
/// The fast path can only ever cause EXTRA work, never stale work: the watermark is
/// in-memory and process-local, so a restart, a failover to the other central node,
/// or any mutation at all simply misses and falls back to the authoritative
/// flatten.
/// </para>
/// </summary>
public sealed class StaleInstanceProbe : IStaleInstanceProbe
{
    /// <summary>
    /// Ceiling on memoised probe results. Bounded because the key space is the
    /// instance table and this cache is process-wide and static; on overflow it is
    /// dropped wholesale, which is safe here (unlike the script-compile verdict
    /// cache) because a miss costs one flatten and leaks nothing.
    /// </summary>
    private const int MaxCachedProbes = 4096;

    private static readonly ConcurrentDictionary<int, ProbeMemo> Memos = new();

    private readonly IFlatteningPipeline _flatteningPipeline;
    private readonly ITemplateGraphWatermark _watermark;

    /// <summary>Initializes a new <see cref="StaleInstanceProbe"/>.</summary>
    /// <param name="flatteningPipeline">The deployment flattening pipeline used to recompute the current revision hash.</param>
    /// <param name="watermark">Graph version watermark backing the fast path.</param>
    public StaleInstanceProbe(IFlatteningPipeline flatteningPipeline, ITemplateGraphWatermark watermark)
    {
        _flatteningPipeline = flatteningPipeline ?? throw new ArgumentNullException(nameof(flatteningPipeline));
        _watermark = watermark ?? throw new ArgumentNullException(nameof(watermark));
    }

    /// <summary>
    /// A previously computed revision hash together with the exact watermark
    /// readings it was computed under.
    /// </summary>
    /// <param name="Structure">Graph structure version at flatten time.</param>
    /// <param name="InstanceVersion">The instance's own version at flatten time.</param>
    /// <param name="TemplateVersions">Version of every template the flatten walked, by template id.</param>
    /// <param name="RevisionHash">The hash that reading produced.</param>
    private sealed record ProbeMemo(
        long Structure,
        long InstanceVersion,
        IReadOnlyDictionary<int, long> TemplateVersions,
        string RevisionHash);

    /// <inheritdoc />
    public async Task<string?> GetCurrentRevisionHashAsync(int instanceId, CancellationToken cancellationToken = default)
    {
        if (TryGetMemoisedHash(instanceId, out var memoised))
            return memoised;

        // The pipeline returns a Result; a flatten failure (e.g. unresolvable
        // template chain mid-import) yields null so the caller treats the
        // instance as "staleness indeterminate" and skips it. Flattening reuses
        // the scoped ITemplateEngineRepository, so it observes template rows the
        // in-flight import has staged on the shared change tracker.
        // Staleness only needs the revision hash — skip the expensive Roslyn
        // script-compilation stage (this probe is called per-instance across a whole
        // bundle import in BundleImporter.ComputeStaleInstanceIdsAsync).
        //
        // Watermark readings are taken BEFORE the flatten. A mutation that lands
        // during the flatten therefore bumps a version past the recorded one and
        // invalidates the memo on the next read, rather than being captured as if
        // it had already been included.
        var structure = _watermark.StructureVersion;
        var instanceVersion = _watermark.GetInstanceVersion(instanceId);

        var session = _flatteningPipeline.CreateSession();
        var result = await _flatteningPipeline
            .FlattenAndValidateAsync(instanceId, cancellationToken, validateScripts: false, session)
            .ConfigureAwait(false);

        if (result.IsFailure)
            return null;

        if (Memos.Count >= MaxCachedProbes)
            Memos.Clear();

        var visited = session.VisitedTemplateIds;
        var versions = new Dictionary<int, long>(visited.Count);
        foreach (var templateId in visited)
            versions[templateId] = _watermark.GetTemplateVersion(templateId);

        Memos[instanceId] = new ProbeMemo(structure, instanceVersion, versions, result.Value.RevisionHash);
        return result.Value.RevisionHash;
    }

    /// <summary>
    /// Returns the memoised revision hash when every watermark reading it was
    /// computed under is still current.
    /// </summary>
    private bool TryGetMemoisedHash(int instanceId, out string? hash)
    {
        hash = null;
        if (!Memos.TryGetValue(instanceId, out var memo))
            return false;

        if (memo.Structure != _watermark.StructureVersion)
            return false;

        if (memo.InstanceVersion != _watermark.GetInstanceVersion(instanceId))
            return false;

        foreach (var (templateId, version) in memo.TemplateVersions)
        {
            if (_watermark.GetTemplateVersion(templateId) != version)
                return false;
        }

        hash = memo.RevisionHash;
        return true;
    }

    /// <summary>
    /// Drops every memoised probe result. Test hook — production invalidation is
    /// driven entirely by the watermark.
    /// </summary>
    public static void ClearMemos() => Memos.Clear();
}
