namespace DodoSSH.Client.Domain;

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

/// <summary>
/// Merges two divergent versions of an SSH key against the version they both started from.
/// </summary>
/// <remarks>
/// <para>
/// Every field is a scalar, so this is simpler than the host merge — there is no per-name map to reconcile
/// and no ordered list whose order is its meaning. It reuses <see cref="HostFieldConflict"/> rather than
/// declaring a parallel type: the conflict log, the storage that persists it and the interface that shows it
/// are all shared, and a second record with identical members would have to be mapped to the first at every
/// boundary.
/// </para>
/// <para>
/// <b>The conflict detail never carries key material.</b> A host conflict shows both values so the user can
/// put back the one that lost; doing that for a private key would write the discarded key into the conflict
/// log, which is a copy of a secret in a place designed to be read rather than used — and the log is
/// deliberately retained after acknowledgement. So the private key and its passphrase report only
/// <em>that</em> they differed. This is the one place where showing less is the safer answer, and it costs
/// the user nothing they can act on: two different private keys are not something anyone reconciles by
/// reading them side by side.
/// </para>
/// </remarks>
public static class SshKeySecretMerge
{
    /// <summary>Produces the merged key.</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 SshKeyMergeResult Merge(SshKeySecret ancestor, SshKeySecret local, SshKeySecret remote)
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);

        var conflicts = new List<HostFieldConflict>();

        var merged = new SshKeySecret
        {
            // Null-forgiving on the two required fields, as HostSecretMerge does for the same reason: the
            // merge returns one of the three inputs, and all three are non-null here by construction.
            Label = Shown(
                nameof(SshKeySecret.Label), ancestor.Label, local.Label, remote.Label, conflicts)!,
            PrivateKeyPem = Redacted(
                nameof(SshKeySecret.PrivateKeyPem),
                ancestor.PrivateKeyPem,
                local.PrivateKeyPem,
                remote.PrivateKeyPem,
                conflicts)!,
            Passphrase = Redacted(
                nameof(SshKeySecret.Passphrase),
                ancestor.Passphrase,
                local.Passphrase,
                remote.Passphrase,
                conflicts),
            PublicKey = Shown(
                nameof(SshKeySecret.PublicKey),
                ancestor.PublicKey,
                local.PublicKey,
                remote.PublicKey,
                conflicts),
            Notes = Shown(
                nameof(SshKeySecret.Notes), ancestor.Notes, local.Notes, remote.Notes, conflicts),
        };

        return new SshKeyMergeResult(merged, conflicts);
    }

    /// <summary>A field whose overridden value is safe to show.</summary>
    private static string? Shown(
        string name,
        string? ancestor,
        string? local,
        string? remote,
        List<HostFieldConflict> conflicts) =>
        Resolve(name, ancestor, local, remote, conflicts, redact: false);

    /// <summary>A field whose overridden value must not be written to the conflict log.</summary>
    private static string? Redacted(
        string name,
        string? ancestor,
        string? local,
        string? remote,
        List<HostFieldConflict> conflicts) =>
        Resolve(name, ancestor, local, remote, conflicts, redact: true);

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

        if (merge.IsConflicted)
        {
            conflicts.Add(new HostFieldConflict(
                name,
                MergeSide.Local,
                redact ? "(kept the server's value)" : merge.Value,
                redact ? "(a different value was discarded)" : merge.Discarded,
                DiscardedWasRemoval: false));
        }

        return merge.Value;
    }
}
