using ScadaLink.Commons.Types.Flattening;

namespace ScadaLink.TemplateEngine.Flattening;

/// <summary>
/// Compares two FlattenedConfigurations (deployed vs current) and produces a ConfigurationDiff
/// showing Added, Removed, and Changed entries for attributes, alarms, and scripts.
/// </summary>
public class DiffService
{
    /// <summary>
    /// Computes the diff between an old (deployed) and new (current) flattened configuration.
    /// </summary>
    /// <param name="oldConfig">The previously deployed configuration. Null for first-time deployment.</param>
    /// <param name="newConfig">The current flattened configuration.</param>
    /// <param name="oldRevisionHash">The revision hash of the old config, if any.</param>
    /// <param name="newRevisionHash">The revision hash of the new config.</param>
    /// <returns>A ConfigurationDiff with all changes.</returns>
    public ConfigurationDiff ComputeDiff(
        FlattenedConfiguration? oldConfig,
        FlattenedConfiguration newConfig,
        string? oldRevisionHash = null,
        string? newRevisionHash = null)
    {
        ArgumentNullException.ThrowIfNull(newConfig);

        var attributeChanges = ComputeEntityDiff(
            oldConfig?.Attributes ?? [],
            newConfig.Attributes,
            a => a.CanonicalName,
            AttributesEqual);

        var alarmChanges = ComputeEntityDiff(
            oldConfig?.Alarms ?? [],
            newConfig.Alarms,
            a => a.CanonicalName,
            AlarmsEqual);

        var scriptChanges = ComputeEntityDiff(
            oldConfig?.Scripts ?? [],
            newConfig.Scripts,
            s => s.CanonicalName,
            ScriptsEqual);

        return new ConfigurationDiff
        {
            InstanceUniqueName = newConfig.InstanceUniqueName,
            OldRevisionHash = oldRevisionHash,
            NewRevisionHash = newRevisionHash,
            AttributeChanges = attributeChanges,
            AlarmChanges = alarmChanges,
            ScriptChanges = scriptChanges
        };
    }

    private static List<DiffEntry<T>> ComputeEntityDiff<T>(
        IReadOnlyList<T> oldItems,
        IReadOnlyList<T> newItems,
        Func<T, string> getCanonicalName,
        Func<T, T, bool> areEqual)
    {
        var result = new List<DiffEntry<T>>();

        var oldMap = oldItems.ToDictionary(getCanonicalName, x => x, StringComparer.Ordinal);
        var newMap = newItems.ToDictionary(getCanonicalName, x => x, StringComparer.Ordinal);

        // Find removed and changed
        foreach (var (name, oldItem) in oldMap)
        {
            if (!newMap.TryGetValue(name, out var newItem))
            {
                result.Add(new DiffEntry<T>
                {
                    CanonicalName = name,
                    ChangeType = DiffChangeType.Removed,
                    OldValue = oldItem,
                    NewValue = default
                });
            }
            else if (!areEqual(oldItem, newItem))
            {
                result.Add(new DiffEntry<T>
                {
                    CanonicalName = name,
                    ChangeType = DiffChangeType.Changed,
                    OldValue = oldItem,
                    NewValue = newItem
                });
            }
        }

        // Find added
        foreach (var (name, newItem) in newMap)
        {
            if (!oldMap.ContainsKey(name))
            {
                result.Add(new DiffEntry<T>
                {
                    CanonicalName = name,
                    ChangeType = DiffChangeType.Added,
                    OldValue = default,
                    NewValue = newItem
                });
            }
        }

        return result.OrderBy(d => d.CanonicalName, StringComparer.Ordinal).ToList();
    }

    private static bool AttributesEqual(ResolvedAttribute a, ResolvedAttribute b) =>
        a.CanonicalName == b.CanonicalName &&
        a.Value == b.Value &&
        a.DataType == b.DataType &&
        a.IsLocked == b.IsLocked &&
        a.DataSourceReference == b.DataSourceReference &&
        a.BoundDataConnectionId == b.BoundDataConnectionId;

    private static bool AlarmsEqual(ResolvedAlarm a, ResolvedAlarm b) =>
        a.CanonicalName == b.CanonicalName &&
        a.PriorityLevel == b.PriorityLevel &&
        a.IsLocked == b.IsLocked &&
        a.TriggerType == b.TriggerType &&
        a.TriggerConfiguration == b.TriggerConfiguration &&
        a.OnTriggerScriptCanonicalName == b.OnTriggerScriptCanonicalName;

    private static bool ScriptsEqual(ResolvedScript a, ResolvedScript b) =>
        a.CanonicalName == b.CanonicalName &&
        a.Code == b.Code &&
        a.IsLocked == b.IsLocked &&
        a.TriggerType == b.TriggerType &&
        a.TriggerConfiguration == b.TriggerConfiguration &&
        a.ParameterDefinitions == b.ParameterDefinitions &&
        a.ReturnDefinition == b.ReturnDefinition &&
        a.MinTimeBetweenRuns == b.MinTimeBetweenRuns;
}
