namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;

/// <summary>
///     Pure diff between two <see cref="Phase7CompositionResult"/> snapshots — the
///     <c>previous</c> currently-applied composition and the <c>next</c> from a freshly-applied
///     deployment. Three lists per entity class (Equipment / DriverInstance / ScriptedAlarm)
///     captured by stable identity: added items are new, removed items have to be torn down,
///     changed items have the same identity but at least one field differs.
///
///     OpcUaPublishActor's <c>RebuildAddressSpace</c> consumes this against a real
///     <see cref="Commons.OpcUa.IOpcUaAddressSpaceSink"/> binding so re-applies only mutate the
///     nodes that actually changed — full tear-down + rebuild is reserved for first-boot or
///     drastic schema flips.
/// </summary>
public sealed record Phase7Plan(
    IReadOnlyList<EquipmentNode> AddedEquipment,
    IReadOnlyList<EquipmentNode> RemovedEquipment,
    IReadOnlyList<Phase7Plan.EquipmentDelta> ChangedEquipment,
    IReadOnlyList<DriverInstancePlan> AddedDrivers,
    IReadOnlyList<DriverInstancePlan> RemovedDrivers,
    IReadOnlyList<Phase7Plan.DriverDelta> ChangedDrivers,
    IReadOnlyList<ScriptedAlarmPlan> AddedAlarms,
    IReadOnlyList<ScriptedAlarmPlan> RemovedAlarms,
    IReadOnlyList<Phase7Plan.AlarmDelta> ChangedAlarms,
    IReadOnlyList<GalaxyTagPlan> AddedGalaxyTags,
    IReadOnlyList<GalaxyTagPlan> RemovedGalaxyTags,
    IReadOnlyList<Phase7Plan.GalaxyTagDelta> ChangedGalaxyTags)
{
    /// <summary>
    ///     Equipment-namespace tag diff sets, keyed by <see cref="EquipmentTagPlan.TagId"/>. Added as
    ///     init-only members (defaulting empty) rather than positional parameters so existing
    ///     <c>Phase7Plan</c> construction sites compile unchanged — consistent with how
    ///     <see cref="Phase7CompositionResult.EquipmentTags"/> was added. Without these, an
    ///     incremental deploy that changes ONLY equipment tags produced an empty plan and
    ///     <c>OpcUaPublishActor.HandleRebuild</c> short-circuited before materialising them.
    /// </summary>
    public IReadOnlyList<EquipmentTagPlan> AddedEquipmentTags { get; init; } = Array.Empty<EquipmentTagPlan>();
    /// <inheritdoc cref="AddedEquipmentTags"/>
    public IReadOnlyList<EquipmentTagPlan> RemovedEquipmentTags { get; init; } = Array.Empty<EquipmentTagPlan>();
    /// <inheritdoc cref="AddedEquipmentTags"/>
    public IReadOnlyList<EquipmentTagDelta> ChangedEquipmentTags { get; init; } = Array.Empty<EquipmentTagDelta>();

    /// <summary>
    ///     Equipment-namespace VirtualTag diff sets, keyed by <see cref="EquipmentVirtualTagPlan.VirtualTagId"/>.
    ///     The value-side analogue of <see cref="AddedEquipmentTags"/>: a VirtualTag carries an
    ///     <c>Expression</c> evaluated over <c>DependencyRefs</c>, so a deploy that changes ONLY
    ///     VirtualTags (e.g. a new computed signal or an edited formula) must still produce a
    ///     non-empty plan and drive a rebuild — without these the diff was blind to VirtualTags and
    ///     such a deploy silently no-op'd. Added as init-only members (defaulting empty) for the same
    ///     compile-compatibility reason as <see cref="AddedEquipmentTags"/>.
    /// </summary>
    public IReadOnlyList<EquipmentVirtualTagPlan> AddedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
    /// <inheritdoc cref="AddedEquipmentVirtualTags"/>
    public IReadOnlyList<EquipmentVirtualTagPlan> RemovedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagPlan>();
    /// <inheritdoc cref="AddedEquipmentVirtualTags"/>
    public IReadOnlyList<EquipmentVirtualTagDelta> ChangedEquipmentVirtualTags { get; init; } = Array.Empty<EquipmentVirtualTagDelta>();

    /// <summary>Gets a value indicating whether the composition plan contains no changes.</summary>
    public bool IsEmpty =>
        AddedEquipment.Count == 0 && RemovedEquipment.Count == 0 && ChangedEquipment.Count == 0 &&
        AddedDrivers.Count == 0 && RemovedDrivers.Count == 0 && ChangedDrivers.Count == 0 &&
        AddedAlarms.Count == 0 && RemovedAlarms.Count == 0 && ChangedAlarms.Count == 0 &&
        AddedGalaxyTags.Count == 0 && RemovedGalaxyTags.Count == 0 && ChangedGalaxyTags.Count == 0 &&
        AddedEquipmentTags.Count == 0 && RemovedEquipmentTags.Count == 0 && ChangedEquipmentTags.Count == 0 &&
        AddedEquipmentVirtualTags.Count == 0 && RemovedEquipmentVirtualTags.Count == 0 && ChangedEquipmentVirtualTags.Count == 0;

    public sealed record EquipmentDelta(EquipmentNode Previous, EquipmentNode Current);
    public sealed record DriverDelta(DriverInstancePlan Previous, DriverInstancePlan Current);
    public sealed record AlarmDelta(ScriptedAlarmPlan Previous, ScriptedAlarmPlan Current);
    public sealed record GalaxyTagDelta(GalaxyTagPlan Previous, GalaxyTagPlan Current);
    public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current);
    public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current);
}

public static class Phase7Planner
{
    /// <summary>
    ///     Diff two compositions, emitting Added/Removed/Changed sets per entity class.
    ///     Identity is the entity's stable id (EquipmentId, DriverInstanceId, ScriptedAlarmId).
    ///     Element equality on the projection records doubles as the "did this change" check,
    ///     so any field difference moves an item from "stable" to ChangedX.
    /// </summary>
    /// <param name="previous">The previous composition result.</param>
    /// <param name="next">The new composition result.</param>
    public static Phase7Plan Compute(Phase7CompositionResult previous, Phase7CompositionResult next)
    {
        ArgumentNullException.ThrowIfNull(previous);
        ArgumentNullException.ThrowIfNull(next);

        var (addedEq, removedEq, changedEq) = DiffById(
            previous.EquipmentNodes, next.EquipmentNodes,
            n => n.EquipmentId,
            (a, b) => new Phase7Plan.EquipmentDelta(a, b));

        var (addedDrv, removedDrv, changedDrv) = DiffById(
            previous.DriverInstancePlans, next.DriverInstancePlans,
            d => d.DriverInstanceId,
            (a, b) => new Phase7Plan.DriverDelta(a, b));

        var (addedAlarm, removedAlarm, changedAlarm) = DiffById(
            previous.ScriptedAlarmPlans, next.ScriptedAlarmPlans,
            a => a.ScriptedAlarmId,
            (a, b) => new Phase7Plan.AlarmDelta(a, b));

        var (addedGalaxy, removedGalaxy, changedGalaxy) = DiffById(
            previous.GalaxyTags, next.GalaxyTags,
            t => t.TagId,
            (a, b) => new Phase7Plan.GalaxyTagDelta(a, b));

        var (addedEqTags, removedEqTags, changedEqTags) = DiffById(
            previous.EquipmentTags, next.EquipmentTags,
            t => t.TagId,
            (a, b) => new Phase7Plan.EquipmentTagDelta(a, b));

        // VirtualTags diff by VirtualTagId, mirroring the EquipmentTags pass. EquipmentVirtualTagPlan
        // overrides record equality to compare ALL fields by value — scalars (Expression/DataType/
        // Name/FolderPath) plus DependencyRefs element-wise (SequenceEqual). So a no-op redeploy (fresh
        // list instances, identical contents) correctly diffs to empty; only a real content change is
        // flagged as changed.
        var (addedVTags, removedVTags, changedVTags) = DiffById(
            previous.EquipmentVirtualTags, next.EquipmentVirtualTags,
            t => t.VirtualTagId,
            (a, b) => new Phase7Plan.EquipmentVirtualTagDelta(a, b));

        return new Phase7Plan(
            addedEq, removedEq, changedEq,
            addedDrv, removedDrv, changedDrv,
            addedAlarm, removedAlarm, changedAlarm,
            addedGalaxy, removedGalaxy, changedGalaxy)
        {
            AddedEquipmentTags = addedEqTags,
            RemovedEquipmentTags = removedEqTags,
            ChangedEquipmentTags = changedEqTags,
            AddedEquipmentVirtualTags = addedVTags,
            RemovedEquipmentVirtualTags = removedVTags,
            ChangedEquipmentVirtualTags = changedVTags,
        };
    }

    private static (IReadOnlyList<T> Added, IReadOnlyList<T> Removed, IReadOnlyList<TDelta> Changed)
        DiffById<T, TDelta>(
            IReadOnlyList<T> previous,
            IReadOnlyList<T> next,
            Func<T, string> identity,
            Func<T, T, TDelta> deltaFactory) where T : class
    {
        var prevById = previous.ToDictionary(identity, StringComparer.Ordinal);
        var nextById = next.ToDictionary(identity, StringComparer.Ordinal);

        var added = new List<T>();
        var removed = new List<T>();
        var changed = new List<TDelta>();

        foreach (var (id, p) in prevById)
        {
            if (!nextById.TryGetValue(id, out var n)) { removed.Add(p); continue; }
            if (!EqualityComparer<T>.Default.Equals(p, n)) changed.Add(deltaFactory(p, n));
        }
        foreach (var (id, n) in nextById)
        {
            if (!prevById.ContainsKey(id)) added.Add(n);
        }

        added.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
        removed.Sort((a, b) => string.CompareOrdinal(identity(a), identity(b)));
        return (added, removed, changed);
    }
}
