namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

/// <summary>
///     Pure diff between the currently-running driver children (keyed by
///     <c>DriverInstance.DriverInstanceId</c>) and the target spec list from a freshly-applied
///     deployment artifact. The DriverHostActor consumes the three lists and calls
///     spawn / ApplyDelta / stop on its child actors accordingly.
/// </summary>
/// <param name="ToSpawn">Specs with no current child — create a new actor.</param>
/// <param name="ToApplyDelta">
///     In-place config deltas. <b>Always empty since #516</b> — a DriverConfig change is now a
///     stop + respawn so the factory is the single parse authority. Retained because
///     <c>DriverInstanceActor</c> still handles <c>ApplyDelta</c> on its own config-adoption path
///     (a config arriving mid-connect), and removing the list would hide that seam.
/// </param>
/// <param name="ToStop">DriverInstanceIds currently running but missing from the new artifact, or now disabled.</param>
public sealed record DriverSpawnPlan(
    IReadOnlyList<DriverInstanceSpec> ToSpawn,
    IReadOnlyList<DriverInstanceSpec> ToApplyDelta,
    IReadOnlyList<string> ToStop);

public static class DriverSpawnPlanner
{
    /// <summary>
    ///     Compute the spawn/delta/stop sets. Disabled entries in <paramref name="target"/> are
    ///     treated as "not desired here": if a child exists for the id it goes into ToStop,
    ///     otherwise the row is dropped entirely (no spawn for a disabled driver).
    /// </summary>
    /// <param name="current">The currently running driver children keyed by ID.</param>
    /// <param name="target">The target driver instances from the deployment artifact.</param>
    /// <returns>The computed <see cref="DriverSpawnPlan"/> with spawn, apply-delta, and stop sets.</returns>
    public static DriverSpawnPlan Compute(
        IReadOnlyDictionary<string, DriverChildSnapshot> current,
        IReadOnlyList<DriverInstanceSpec> target)
    {
        var toSpawn = new List<DriverInstanceSpec>();
        var toDelta = new List<DriverInstanceSpec>();
        var toStop = new List<string>();

        var targetById = new Dictionary<string, DriverInstanceSpec>(StringComparer.Ordinal);
        foreach (var spec in target) targetById[spec.DriverInstanceId] = spec;

        foreach (var (id, snap) in current)
        {
            if (!targetById.TryGetValue(id, out var spec) || !spec.Enabled)
            {
                toStop.Add(id);
                continue;
            }
            // ANY of the three config surfaces changing forces a stop + respawn, so the FACTORY is the
            // single parse authority for everything a driver is built from.
            //
            //   • DriverType — factory-bound, can't be reinitialized in place.
            //   • ResilienceConfig — the CapabilityInvoker (and its resolved options) binds to the child
            //     actor at spawn time; the factory invalidates the stale cached pipelines on respawn.
            //   • DriverConfig — was an in-place ApplyDelta until #516. It is now a respawn.
            //
            // #516: the in-place delta silently DISCARDED the edit on five drivers (Modbus, FOCAS,
            // OpcUaClient, AbLegacy, Sql), whose InitializeAsync served the options captured by their
            // constructor and never looked at the driverConfigJson they were handed. The deployment still
            // sealed green. Those five now re-parse (belt), and this respawn is the braces: several
            // drivers build MORE than options from config — Sql derives its dialect + connection string
            // and FOCAS its client-factory backend, both injected at construction — so an in-place
            // re-parse of options ALONE would apply a new tag set against an old connection. Only a
            // respawn rebuilds all of it.
            //
            // The accepted cost is a reconnect on every DriverConfig edit, replacing the prior
            // no-reconnect in-place path. That is deliberate: a correct reconnect beats a silent no-op.
            if (!string.Equals(snap.DriverType, spec.DriverType, StringComparison.Ordinal)
                || !string.Equals(snap.ResilienceConfig, spec.ResilienceConfig, StringComparison.Ordinal)
                || !string.Equals(snap.LastConfigJson, spec.DriverConfig, StringComparison.Ordinal))
            {
                toStop.Add(id);
                toSpawn.Add(spec);
                continue;
            }
        }

        foreach (var (id, spec) in targetById)
        {
            if (!spec.Enabled) continue;
            if (current.ContainsKey(id)) continue;
            toSpawn.Add(spec);
        }

        return new DriverSpawnPlan(toSpawn, toDelta, toStop);
    }
}

/// <summary>Snapshot of one running driver child as the host sees it. Used as the diff input.</summary>
/// <param name="DriverType">The driver type name the child was spawned for.</param>
/// <param name="LastConfigJson">The <c>DriverConfig</c> JSON the child currently serves.</param>
/// <param name="ResilienceConfig">The per-instance <c>ResilienceConfig</c> JSON the child's invoker was built with (null = tier defaults).</param>
public sealed record DriverChildSnapshot(string DriverType, string LastConfigJson, string? ResilienceConfig = null);
