namespace DodoSSH.Client.Domain;

/// <summary>The merged bucket, and everything that had to be overridden to produce it.</summary>
/// <param name="Merged">The bucket to store and push.</param>
/// <param name="Conflicts">Empty when the two sides were reconcilable field by field.</param>
public sealed record ObjectStoreMergeResult(
    ObjectStoreSecret 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 bucket against the version they both started from.
/// </summary>
/// <remarks>
/// <para>
/// Every field is a scalar, so this is <see cref="CredentialSecretMerge"/>'s shape and it reuses
/// <see cref="HostFieldConflict"/> for the same reason.
/// </para>
/// <para>
/// <b>The secret access key never reaches the conflict log</b>, exactly as a password does not: a discarded
/// one is very often still live on the service it belongs to. The access key <em>id</em> is shown, because it
/// is an identifier rather than a secret and knowing which of two key pairs the merge dropped is the whole
/// content of the notice.
/// </para>
/// </remarks>
public static class ObjectStoreSecretMerge
{
    /// <summary>Produces the merged bucket.</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 ObjectStoreMergeResult Merge(
        ObjectStoreSecret ancestor,
        ObjectStoreSecret local,
        ObjectStoreSecret remote)
    {
        ArgumentNullException.ThrowIfNull(ancestor);
        ArgumentNullException.ThrowIfNull(local);
        ArgumentNullException.ThrowIfNull(remote);

        var conflicts = new List<HostFieldConflict>();

        var merged = new ObjectStoreSecret
        {
            // Null-forgiving on the required fields, as the neighbouring merges do: the merge returns one of
            // its three inputs, and all three are non-null by construction.
            Label = Resolve(
                nameof(ObjectStoreSecret.Label), ancestor.Label, local.Label, remote.Label, conflicts)!,
            Bucket = Resolve(
                nameof(ObjectStoreSecret.Bucket), ancestor.Bucket, local.Bucket, remote.Bucket, conflicts)!,
            AccessKeyId = Resolve(
                nameof(ObjectStoreSecret.AccessKeyId),
                ancestor.AccessKeyId,
                local.AccessKeyId,
                remote.AccessKeyId,
                conflicts)!,
            SecretAccessKey = Resolve(
                nameof(ObjectStoreSecret.SecretAccessKey),
                ancestor.SecretAccessKey,
                local.SecretAccessKey,
                remote.SecretAccessKey,
                conflicts,
                redact: true)!,
            Region = Resolve(
                nameof(ObjectStoreSecret.Region), ancestor.Region, local.Region, remote.Region, conflicts),
            Endpoint = Resolve(
                nameof(ObjectStoreSecret.Endpoint),
                ancestor.Endpoint,
                local.Endpoint,
                remote.Endpoint,
                conflicts),
            UsePathStyle = ThreeWayMerge
                .Scalar(ancestor.UsePathStyle, local.UsePathStyle, remote.UsePathStyle)
                .Value,
            Notes = Resolve(
                nameof(ObjectStoreSecret.Notes), ancestor.Notes, local.Notes, remote.Notes, conflicts),
        };

        return new ObjectStoreMergeResult(merged, conflicts);
    }

    private static string? Resolve(
        string name,
        string? ancestor,
        string? local,
        string? remote,
        List<HostFieldConflict> conflicts,
        bool redact = false)
    {
        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;
    }
}
