namespace DodoSSH.Client.Domain;

/// <summary>The merged credential, and everything that had to be overridden to produce it.</summary>
/// <param name="Merged">The credential to store and push.</param>
/// <param name="Conflicts">Empty when the two sides were reconcilable field by field.</param>
public sealed record CredentialMergeResult(
    CredentialSecret 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 credential against the version they both started from.
/// </summary>
/// <remarks>
/// <para>
/// Every field is a scalar, so this is the same shape as <see cref="SshKeySecretMerge"/> and reuses
/// <see cref="HostFieldConflict"/> for the same reason: the conflict log, the storage behind it and the
/// interface that shows it are shared, and a parallel record with identical members would have to be mapped
/// at every boundary.
/// </para>
/// <para>
/// <b>The password never reaches the conflict log.</b> Reported as having differed and nothing more, exactly
/// as <c>SshKeySecretMerge</c> does for key material — and here the case for it is if anything plainer, since
/// a discarded password is very often still the live password on some other system. The user loses nothing
/// they could act on: nobody reconciles two passwords by reading them side by side.
/// </para>
/// <para>
/// The username is shown, because it is not a secret and knowing which of two accounts the merge dropped is
/// exactly what makes the notice useful.
/// </para>
/// </remarks>
public static class CredentialSecretMerge
{
    /// <summary>Produces the merged credential.</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 CredentialMergeResult Merge(
        CredentialSecret ancestor,
        CredentialSecret local,
        CredentialSecret remote)
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);

        var conflicts = new List<HostFieldConflict>();

        var merged = new CredentialSecret
        {
            // Null-forgiving on the two required fields, as the host and key merges do for the same reason:
            // the merge returns one of its three inputs, and all three are non-null by construction.
            Label = Resolve(
                nameof(CredentialSecret.Label),
                ancestor.Label,
                local.Label,
                remote.Label,
                conflicts,
                redact: false)!,
            Password = Resolve(
                nameof(CredentialSecret.Password),
                ancestor.Password,
                local.Password,
                remote.Password,
                conflicts,
                redact: true)!,
            Username = Resolve(
                nameof(CredentialSecret.Username),
                ancestor.Username,
                local.Username,
                remote.Username,
                conflicts,
                redact: false),
            Notes = Resolve(
                nameof(CredentialSecret.Notes),
                ancestor.Notes,
                local.Notes,
                remote.Notes,
                conflicts,
                redact: false),
        };

        return new CredentialMergeResult(merged, conflicts);
    }

    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 ?? "(none)",
                redact ? "(a different value was discarded)" : merge.Discarded ?? "(none)",
                DiscardedWasRemoval: false));
        }

        return merge.Value;
    }
}
