namespace DodoSSH.Client.Domain;

/// <summary>The merged snippet, and everything that had to be overridden to produce it.</summary>
/// <param name="Merged">The snippet to store and push.</param>
/// <param name="Conflicts">Empty when the two sides were reconcilable field by field.</param>
public sealed record SnippetMergeResult(
    SnippetSecret Merged,
    IReadOnlyList<HostFieldConflict> Conflicts)
{
    /// <summary>Whether anything had to be overridden.</summary>
    public bool HasConflicts => Conflicts.Count > 0;
}

/// <summary>
/// Merges two divergent versions of a snippet against the version they both started from.
/// </summary>
/// <remarks>
/// <para>
/// Three strings and a flag, so the shape is <see cref="CredentialSecretMerge"/>'s and it reuses
/// <see cref="HostFieldConflict"/> for the same reason. Nothing is redacted: a snippet is a command somebody
/// wrote down on purpose, and a notice that hid the discarded version would leave the user unable to tell
/// whether the one that survived is the one they wanted to keep.
/// </para>
/// <para>
/// <b><see cref="SnippetSecret.RunsOnInsert"/> cannot conflict, and it is worth knowing why rather than
/// assuming it.</b> A three-way clash needs local and remote each to differ from the ancestor <em>and</em>
/// from one another; with only two possible values, the first two conditions force the third to fail. So this
/// field always resolves to whichever side actually changed it, and a merge can never turn a snippet into one
/// that runs on its own — the outcome the ordinary rule would have made possible if the field had a third
/// state. An earlier draft special-cased it to resolve to <see langword="false"/> on a clash; the branch was
/// unreachable, and unreachable safety code is worse than none, because it reads as protection.
/// </para>
/// </remarks>
public static class SnippetSecretMerge
{
    /// <summary>Produces the merged snippet.</summary>
    /// <param name="ancestor">The version both sides branched from.</param>
    /// <param name="local">The pending local version.</param>
    /// <param name="remote">The server's current version.</param>
    public static SnippetMergeResult Merge(
        SnippetSecret ancestor,
        SnippetSecret local,
        SnippetSecret remote)
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);

        var conflicts = new List<HostFieldConflict>();

        var merged = new SnippetSecret
        {
            // Null-forgiving on the two required fields, as the neighbouring merges do for the same reason:
            // the merge returns one of its three inputs, and all three are non-null by construction.
            Label = Text(
                nameof(SnippetSecret.Label), ancestor.Label, local.Label, remote.Label, conflicts)!,
            Command = Text(
                nameof(SnippetSecret.Command),
                ancestor.Command,
                local.Command,
                remote.Command,
                conflicts)!,
            Notes = Text(
                nameof(SnippetSecret.Notes), ancestor.Notes, local.Notes, remote.Notes, conflicts),
            RunsOnInsert = ThreeWayMerge
                .Scalar(ancestor.RunsOnInsert, local.RunsOnInsert, remote.RunsOnInsert)
                .Value,
        };

        return new SnippetMergeResult(merged, conflicts);
    }

    private static string? Text(
        string name,
        string? ancestor,
        string? local,
        string? remote,
        List<HostFieldConflict> conflicts)
    {
        var merge = ThreeWayMerge.Scalar(ancestor, local, remote, StringComparer.Ordinal);

        if (merge.IsConflicted)
        {
            conflicts.Add(new HostFieldConflict(
                name,
                MergeSide.Local,
                merge.Value ?? "(none)",
                merge.Discarded ?? "(none)",
                DiscardedWasRemoval: false));
        }

        return merge.Value;
    }
}
