using Mbproxy.Bcd;
using Mbproxy.Options;

namespace Mbproxy.Configuration;

/// <summary>
/// Immutable record describing what needs to change between two <see cref="MbproxyOptions"/>
/// snapshots. Computed by <see cref="Compute"/> — a pure function with no side effects.
///
/// <para><b>PLC identity is keyed on <c>Name</c>, not <c>ListenPort</c>.</b>
/// A PLC whose <c>ListenPort</c> changes is still the same PLC (treated as a restart).
/// A PLC whose <c>Name</c> changes is treated as remove-the-old + add-the-new.</para>
///
/// <para><b>Reseat vs. Restart</b>:
/// <list type="bullet">
///   <item><see cref="ToRestart"/> — PLC host, ListenPort, or backend Port changed.
///         The supervisor must stop and start (new TCP socket needed).</item>
///   <item><see cref="ToReseat"/> — Only the resolved <see cref="BcdTagMap"/> changed
///         (via global tag list or per-PLC overrides). The supervisor can keep its
///         listener socket; only the context needs a map swap.</item>
/// </list>
/// </para>
/// </summary>
public sealed record ReloadPlan(
    IReadOnlyList<PlcOptions> ToAdd,
    IReadOnlyList<string> ToRemove,                              // PLC names
    IReadOnlyList<(string Name, PlcOptions New)> ToRestart,     // network identity changed
    IReadOnlyList<(string Name, BcdTagMap NewMap)> ToReseat,    // only tag map changed
    ConnectionOptions Connection)
{
    /// <summary>
    /// Computes the reload plan that transforms <paramref name="current"/> into
    /// <paramref name="next"/>. Called after <see cref="ReloadValidator.Validate"/>
    /// has already confirmed <paramref name="next"/> is self-consistent.
    /// </summary>
    public static ReloadPlan Compute(MbproxyOptions current, MbproxyOptions next)
    {
        // Index current PLCs by name for O(1) lookup.
        var currentByName = current.Plcs.ToDictionary(p => p.Name, StringComparer.Ordinal);
        var nextByName    = next.Plcs.ToDictionary(p => p.Name, StringComparer.Ordinal);

        var toAdd     = new List<PlcOptions>();
        var toRemove  = new List<string>();
        var toRestart = new List<(string, PlcOptions)>();
        var toReseat  = new List<(string, BcdTagMap)>();

        // ── PLCs in next but not in current → Add ────────────────────────────
        foreach (var (name, plcNew) in nextByName)
        {
            if (!currentByName.ContainsKey(name))
                toAdd.Add(plcNew);
        }

        // ── PLCs in current but not in next → Remove ─────────────────────────
        foreach (var (name, _) in currentByName)
        {
            if (!nextByName.ContainsKey(name))
                toRemove.Add(name);
        }

        // ── PLCs in both → compare ────────────────────────────────────────────
        foreach (var (name, plcOld) in currentByName)
        {
            if (!nextByName.TryGetValue(name, out var plcNew))
                continue; // Already in ToRemove.

            // Network-identity change → restart (stop old TCP socket, start new one).
            bool networkChanged = plcOld.Host       != plcNew.Host
                               || plcOld.ListenPort != plcNew.ListenPort
                               || plcOld.Port       != plcNew.Port;

            if (networkChanged)
            {
                toRestart.Add((name, plcNew));
                continue;
            }

            // Tag-map change → reseat (swap context, keep socket).
            // We must build both maps to compare them structurally.
            // Compute happens after validation so Build should never return errors here.
            // Include DefaultCacheTtlMs in the build so a per-PLC default change is
            // detected by TagMapsEqual via the per-tag CacheTtlMs delta.
            var oldMap = BcdTagMapBuilder.Build(current.BcdTags, plcOld.BcdTags, plcOld.DefaultCacheTtlMs).Map;
            var newMap = BcdTagMapBuilder.Build(next.BcdTags,    plcNew.BcdTags, plcNew.DefaultCacheTtlMs).Map;

            if (!TagMapsEqual(oldMap, newMap))
                toReseat.Add((name, newMap));

            // Otherwise: PLC is unchanged — no action needed.
        }

        return new ReloadPlan(toAdd, toRemove, toRestart, toReseat, next.Connection);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────────────────

    /// <summary>
    /// Structural equality between two <see cref="BcdTagMap"/> instances: same set of
    /// (Address, Width, CacheTtlMs) triples. Order doesn't matter — we compare as sets.
    /// Includes <see cref="BcdTag.CacheTtlMs"/> in the comparison so a per-tag or per-PLC
    /// default TTL change reseats the context (which flushes the cache).
    /// </summary>
    private static bool TagMapsEqual(BcdTagMap a, BcdTagMap b)
    {
        if (a.Count != b.Count) return false;

        foreach (var tag in a.All)
        {
            if (!b.TryGet(tag.Address, out var bTag))
                return false;
            if (tag.Width != bTag.Width)
                return false;
            if (tag.CacheTtlMs != bTag.CacheTtlMs)
                return false;
        }

        return true;
    }
}
