using Microsoft.Extensions.Logging;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.Secrets.Replication;

/// <summary>
/// Decorates a local <see cref="ISecretStore"/> so every successful local write is published to
/// peers through <see cref="ISecretReplicator"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is what turns the <see cref="ISecretReplicator"/> seam from a declaration into behaviour.
/// Decorating the store — rather than calling the replicator from each write site — means the CLI,
/// the Blazor UI, and any future caller all replicate automatically, and no new write path can
/// forget to.
/// </para>
/// <para>
/// <b>Publishing is best-effort.</b> A transport failure is logged and swallowed: the local write is
/// already durable, and refusing it because a peer was unreachable would make every node a single
/// point of failure for every other. Propagation is then guaranteed by the transport's periodic
/// anti-entropy sweep instead, which is why every transport in this library reconciles
/// <em>bidirectionally</em> — a dropped publish is repaired by the next sweep rather than lost.
/// </para>
/// <para>
/// <see cref="ApplyReplicatedAsync"/> deliberately does not publish (that would echo a peer's row
/// straight back and, across three or more nodes, never stop). Neither does
/// <see cref="ApplyRewrapAsync"/>: a re-wrap does not bump the revision, so it is invisible to
/// last-writer-wins by design and a peer would discard it — KEK rotation is run once per independent
/// store instead.
/// </para>
/// </remarks>
/// <param name="inner">The local store doing the actual persistence.</param>
/// <param name="replicator">The transport that publishes rows to peers.</param>
/// <param name="logger">Logger for publish failures.</param>
public sealed class ReplicatingSecretStore(
    ISecretStore inner,
    ISecretReplicator replicator,
    ILogger<ReplicatingSecretStore> logger) : ISecretStore
{
    /// <inheritdoc />
    public Task<StoredSecret?> GetAsync(SecretName name, CancellationToken ct) =>
        inner.GetAsync(name, ct);

    /// <inheritdoc />
    public Task<IReadOnlyList<SecretMetadata>> ListAsync(bool includeDeleted, CancellationToken ct) =>
        inner.ListAsync(includeDeleted, ct);

    /// <inheritdoc />
    public Task<IReadOnlyList<SecretManifestEntry>> GetManifestAsync(CancellationToken ct) =>
        inner.GetManifestAsync(ct);

    /// <inheritdoc />
    public Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct) =>
        inner.ApplyReplicatedAsync(row, ct);

    /// <inheritdoc />
    public Task<bool> ApplyRewrapAsync(
        StoredSecret rewrappedRow, byte[] expectedCurrentWrappedDek, CancellationToken ct) =>
        inner.ApplyRewrapAsync(rewrappedRow, expectedCurrentWrappedDek, ct);

    /// <inheritdoc />
    public async Task UpsertAsync(StoredSecret row, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(row);

        await inner.UpsertAsync(row, ct).ConfigureAwait(false);
        await PublishCurrentAsync(row.Name, ct).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task<bool> DeleteAsync(SecretName name, string? actor, CancellationToken ct)
    {
        bool deleted = await inner.DeleteAsync(name, actor, ct).ConfigureAwait(false);

        if (deleted)
        {
            // The tombstone is the thing being replicated — a delete propagates as a newer row, not
            // as an absence, so peers converge on "deleted" instead of resurrecting it.
            await PublishCurrentAsync(name, ct).ConfigureAwait(false);
        }

        return deleted;
    }

    // Re-reads the row the store actually persisted and publishes THAT, never the caller's input:
    // the store assigns the revision and updated_utc, and those two fields are exactly what peers
    // order by. Publishing the caller's copy would broadcast a row whose ordering keys are wrong.
    private async Task PublishCurrentAsync(SecretName name, CancellationToken ct)
    {
        StoredSecret? persisted = await inner.GetAsync(name, ct).ConfigureAwait(false);

        if (persisted is null)
        {
            return;
        }

        try
        {
            await replicator.PublishAsync(persisted, ct).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception ex)
        {
            // Deliberately swallowed — see the class remarks. Anti-entropy repairs it.
            logger.LogWarning(
                ex,
                "Failed to publish secret {SecretName} to peers; the local write succeeded and " +
                "anti-entropy will propagate it on the next sweep.",
                name.Value);
        }
    }
}
