using System.Runtime.InteropServices;

namespace DodoSSH.Client.Domain;

/// <summary>Which of the two diverging replicas a value came from.</summary>
public enum MergeSide
{
    /// <summary>The edit made on this machine.</summary>
    Local = 0,

    /// <summary>The edit that arrived from the server.</summary>
    Remote = 1,
}

/// <summary>How a single field was resolved.</summary>
public enum MergeDecision
{
    /// <summary>
    /// Both sides hold the same value — either neither changed it, or both made the identical
    /// change. Distinguishing those two is not useful: the outcome is the same and no one is
    /// surprised.
    /// </summary>
    Agreed = 0,

    /// <summary>Only this machine changed it.</summary>
    TookLocal = 1,

    /// <summary>Only the server side changed it.</summary>
    TookRemote = 2,

    /// <summary>Both changed it, differently. One value survives and the other is reported.</summary>
    Conflicted = 3,
}

/// <summary>The outcome of merging one field.</summary>
/// <typeparam name="T">The field's type.</typeparam>
/// <param name="Value">The value to keep.</param>
/// <param name="Decision">How it was resolved.</param>
/// <param name="Discarded">
/// The value that lost, meaningful only when <paramref name="Decision"/> is
/// <see cref="MergeDecision.Conflicted"/>. Never simply dropped: the caller is expected to record it.
/// </param>
[StructLayout(LayoutKind.Auto)]
public readonly record struct FieldMerge<T>(T Value, MergeDecision Decision, T? Discarded)
{
    /// <summary>Whether both sides changed this field to different values.</summary>
    public bool IsConflicted => Decision == MergeDecision.Conflicted;
}

/// <summary>A key whose value both sides changed, or which one side removed while the other edited.</summary>
/// <typeparam name="TKey">Key type.</typeparam>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="Key">The key in question.</param>
/// <param name="Kept">The value that survives, or <see langword="default"/> if the key is removed.</param>
/// <param name="DiscardedSide">Which replica's intent was overridden.</param>
/// <param name="Discarded">
/// The value that lost, or <see langword="default"/> when what lost was a removal.
/// </param>
/// <param name="DiscardedWasRemoval">
/// True when the overridden intent was to remove the key rather than to set it to a different value.
/// </param>
[StructLayout(LayoutKind.Auto)]
public readonly record struct MapConflict<TKey, TValue>(
    TKey Key,
    TValue? Kept,
    MergeSide DiscardedSide,
    TValue? Discarded,
    bool DiscardedWasRemoval);

/// <summary>The outcome of merging a keyed collection.</summary>
/// <typeparam name="TKey">Key type.</typeparam>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="Merged">The resulting collection.</param>
/// <param name="Conflicts">Every key where the two sides disagreed.</param>
[StructLayout(LayoutKind.Auto)]
public readonly record struct MapMerge<TKey, TValue>(
    IReadOnlyDictionary<TKey, TValue> Merged,
    IReadOnlyList<MapConflict<TKey, TValue>> Conflicts)
    where TKey : notnull;

/// <summary>
/// The merge primitives: resolve a field, or a keyed collection, from a common ancestor and two
/// divergent versions.
/// </summary>
/// <remarks>
/// <para>
/// The server cannot do any of this — it cannot read a payload, so it cannot merge one. That is why
/// a conflicting push comes back with the server's current row rather than being resolved for us,
/// and why this code is the last line of defence against losing a credential.
/// </para>
/// <para>
/// <b>Why the remote side wins a genuine clash.</b> It has to be one of them, and it has to be the
/// same one on every replica. If each client kept its own value, two clients would resolve the same
/// triple in opposite directions, each push would conflict with the other's, and they would ping-pong
/// forever without converging. Deferring to the value already on the server converges in one round.
/// </para>
/// <para>
/// The losing value is <em>never</em> discarded silently. Every primitive returns it, the item-level
/// merge collects them, and the sync engine writes them to a conflict log the user can act on. This
/// is the whole point: a merge that quietly drops the password someone just typed is worse than one
/// that refuses to merge at all.
/// </para>
/// </remarks>
public static class ThreeWayMerge
{
    /// <summary>
    /// Resolves one field.
    /// </summary>
    /// <param name="ancestor">The value both sides started from.</param>
    /// <param name="local">This machine's value.</param>
    /// <param name="remote">The server's value.</param>
    /// <param name="comparer">Value comparison; defaults to <see cref="EqualityComparer{T}.Default"/>.</param>
    public static FieldMerge<T> Scalar<T>(
        T ancestor,
        T local,
        T remote,
        IEqualityComparer<T>? comparer = null)
    {
        comparer ??= EqualityComparer<T>.Default;

        // Checked first, so two people making the identical edit is agreement rather than a
        // conflict they have to be bothered about.
        if (comparer.Equals(local, remote))
        {
            return new FieldMerge<T>(local, MergeDecision.Agreed, default);
        }

        if (comparer.Equals(local, ancestor))
        {
            return new FieldMerge<T>(remote, MergeDecision.TookRemote, default);
        }

        if (comparer.Equals(remote, ancestor))
        {
            return new FieldMerge<T>(local, MergeDecision.TookLocal, default);
        }

        return new FieldMerge<T>(remote, MergeDecision.Conflicted, local);
    }

    /// <summary>
    /// Resolves a keyed collection key by key.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Per-key rather than whole-collection, which is the difference between two people each adding
    /// a directive and both keeping it, versus one of them losing theirs to a conflict. That is the
    /// single most visible benefit of a field-level merge over last-writer-wins.
    /// </para>
    /// <para>
    /// <b>An edit beats a removal.</b> Where one side deleted a key and the other changed its value,
    /// the value survives and the removal is reported. The asymmetry is deliberate and it is not a
    /// preference: re-applying a removal costs one click, while a discarded value may be the only
    /// copy of something the user cannot reconstruct.
    /// </para>
    /// </remarks>
    /// <param name="ancestor">The state both sides started from.</param>
    /// <param name="local">This machine's state.</param>
    /// <param name="remote">The server's state.</param>
    /// <param name="keyComparer">Defines key identity.</param>
    /// <param name="valueComparer">Value comparison; defaults to <see cref="EqualityComparer{T}.Default"/>.</param>
    public static MapMerge<TKey, TValue> Map<TKey, TValue>(
        IReadOnlyDictionary<TKey, TValue> ancestor,
        IReadOnlyDictionary<TKey, TValue> local,
        IReadOnlyDictionary<TKey, TValue> remote,
        IEqualityComparer<TKey> keyComparer,
        IEqualityComparer<TValue>? valueComparer = null)
        where TKey : notnull
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);
        ArgumentNullException.ThrowIfNull(keyComparer);

        valueComparer ??= EqualityComparer<TValue>.Default;

        var merged = new Dictionary<TKey, TValue>(keyComparer);
        var conflicts = new List<MapConflict<TKey, TValue>>();

        foreach (var key in UnionOfKeys(ancestor, local, remote, keyComparer))
        {
            var a = Slot.For(ancestor, key);
            var l = Slot.For(local, key);
            var r = Slot.For(remote, key);

            var resolved = ResolveKey(key, a, l, r, valueComparer, conflicts);

            if (resolved.Present)
            {
                merged[key] = resolved.Value!;
            }
        }

        return new MapMerge<TKey, TValue>(merged, conflicts);
    }

    /// <summary>One key's state on one replica: present with a value, or absent.</summary>
    [StructLayout(LayoutKind.Auto)]
    private readonly record struct Slot<TValue>(bool Present, TValue? Value)
    {
        internal bool Matches(in Slot<TValue> other, IEqualityComparer<TValue> comparer) =>
            Present == other.Present
            && (!Present || comparer.Equals(Value!, other.Value!));
    }

    private static class Slot
    {
        internal static Slot<TValue> For<TKey, TValue>(
            IReadOnlyDictionary<TKey, TValue> source,
            TKey key) =>
            source.TryGetValue(key, out var value)
                ? new Slot<TValue>(true, value)
                : new Slot<TValue>(false, default);
    }

    private static Slot<TValue> ResolveKey<TKey, TValue>(
        TKey key,
        in Slot<TValue> ancestor,
        in Slot<TValue> local,
        in Slot<TValue> remote,
        IEqualityComparer<TValue> valueComparer,
        List<MapConflict<TKey, TValue>> conflicts)
    {
        if (local.Matches(remote, valueComparer))
        {
            return local;
        }

        if (local.Matches(ancestor, valueComparer))
        {
            return remote;
        }

        if (remote.Matches(ancestor, valueComparer))
        {
            return local;
        }

        // Both sides moved. Prefer whichever still holds a value, so an edit outlives a removal;
        // where both hold one, defer to the server so every replica converges the same way.
        var winner = remote.Present ? remote : local;
        var loserSide = remote.Present ? MergeSide.Local : MergeSide.Remote;
        var loser = remote.Present ? local : remote;

        conflicts.Add(new MapConflict<TKey, TValue>(
            key,
            winner.Value,
            loserSide,
            loser.Present ? loser.Value : default,
            DiscardedWasRemoval: !loser.Present));

        return winner;
    }

    private static IEnumerable<TKey> UnionOfKeys<TKey, TValue>(
        IReadOnlyDictionary<TKey, TValue> ancestor,
        IReadOnlyDictionary<TKey, TValue> local,
        IReadOnlyDictionary<TKey, TValue> remote,
        IEqualityComparer<TKey> keyComparer)
        where TKey : notnull
    {
        var seen = new HashSet<TKey>(keyComparer);

        foreach (var key in ancestor.Keys.Concat(local.Keys).Concat(remote.Keys))
        {
            if (seen.Add(key))
            {
                yield return key;
            }
        }
    }
}
