using System.Globalization;

namespace DodoSSH.Client.Domain;

/// <summary>The merged pin, and everything that had to be overridden to produce it.</summary>
/// <param name="Merged">The pin to store and push.</param>
/// <param name="Conflicts">Empty when the two sides were reconcilable field by field.</param>
public sealed record KnownHostMergeResult(
    KnownHostSecret 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 known host key against the version they both started from.
/// </summary>
/// <remarks>
/// <para>
/// Four scalars, so this is the same shape as <see cref="CredentialSecretMerge"/> and reuses
/// <see cref="HostFieldConflict"/> for the same reason: the conflict log, the storage behind it and the
/// interface that shows it are shared.
/// </para>
/// <para>
/// <b>Nothing here is redacted, and the fingerprint least of all.</b> The other two item types hide the
/// value that lost, because a discarded password or a discarded private key is still live somewhere. A
/// fingerprint is published by operators precisely so that it can be compared, and which of two fingerprints
/// the merge dropped is the entire content of the notice — a user told only that "the fingerprint differed"
/// has been told nothing they can check.
/// </para>
/// <para>
/// In practice only <see cref="KnownHostSecret.Fingerprint"/> can genuinely clash, and it is worth saying why
/// the other three are merged rather than asserted: they are what the pin is <em>about</em>, so two sides
/// holding different values for them are not two versions of one pin at all. Nothing in the client can
/// produce that — the store rewrites a fingerprint or creates a new item, never re-addresses an existing one
/// — but a payload arriving from elsewhere is untrusted input, and merging it field by field keeps the
/// outcome a recorded conflict rather than a silent adoption.
/// </para>
/// </remarks>
public static class KnownHostSecretMerge
{
    /// <summary>Produces the merged pin.</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 KnownHostMergeResult Merge(
        KnownHostSecret ancestor,
        KnownHostSecret local,
        KnownHostSecret remote)
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);

        var conflicts = new List<HostFieldConflict>();

        var merged = new KnownHostSecret
        {
            Host = Field(
                nameof(KnownHostSecret.Host),
                ancestor.Host,
                local.Host,
                remote.Host,
                conflicts,
                static host => host,
                StringComparer.Ordinal),
            Port = Field(
                nameof(KnownHostSecret.Port),
                ancestor.Port,
                local.Port,
                remote.Port,
                conflicts,
                static port => port.ToString(CultureInfo.InvariantCulture)),
            Algorithm = Field(
                nameof(KnownHostSecret.Algorithm),
                ancestor.Algorithm,
                local.Algorithm,
                remote.Algorithm,
                conflicts,
                static algorithm => algorithm,
                StringComparer.Ordinal),
            Fingerprint = Field(
                nameof(KnownHostSecret.Fingerprint),
                ancestor.Fingerprint,
                local.Fingerprint,
                remote.Fingerprint,
                conflicts,
                static fingerprint => fingerprint,
                StringComparer.Ordinal),
        };

        return new KnownHostMergeResult(merged, conflicts);
    }

    private static T Field<T>(
        string name,
        T ancestor,
        T local,
        T remote,
        List<HostFieldConflict> conflicts,
        Func<T, string?> format,
        IEqualityComparer<T>? comparer = null)
    {
        var merge = ThreeWayMerge.Scalar(ancestor, local, remote, comparer);

        if (merge.IsConflicted)
        {
            // The local side always loses a scalar clash — see ThreeWayMerge — so the discarded side is
            // fixed here rather than derived from the outcome.
            conflicts.Add(new HostFieldConflict(
                name,
                MergeSide.Local,
                format(merge.Value),
                format(merge.Discarded!),
                DiscardedWasRemoval: false));
        }

        return merge.Value;
    }
}
