using System.Security.Cryptography;
using DodoSSH.Api.Setup;
using DodoSSH.Domain.Sync;
using Microsoft.Extensions.Options;

namespace DodoSSH.Api.Features.Sync;

/// <summary>Supplies the key that tags sync cursors.</summary>
public interface ICursorKeyProvider
{
    /// <summary>The signing key.</summary>
    ReadOnlySpan<byte> Key { get; }
}

/// <summary>
/// Resolves the cursor signing key from configuration, generating an ephemeral one if unset.
/// </summary>
/// <remarks>
/// <para>
/// Losing this key is harmless: it only invalidates in-flight cursors, and a client that gets a
/// rejected cursor resyncs from the beginning. That is why an unconfigured deployment gets a random
/// per-process key rather than a startup failure — it works, and the only cost is that cursors do
/// not survive a restart or span multiple nodes.
/// </para>
/// <para>
/// Multi-node deployments must configure it explicitly, or a cursor issued by one node will be
/// rejected by another and clients will resync constantly.
/// </para>
/// </remarks>
internal sealed class CursorKeyProvider : ICursorKeyProvider
{
    private readonly byte[] key;

    public CursorKeyProvider(IOptions<SyncOptions> options, ILogger<CursorKeyProvider> logger)
    {
        ArgumentNullException.ThrowIfNull(options);
        ArgumentNullException.ThrowIfNull(logger);

        var configured = options.Value.CursorSigningKey;

        if (string.IsNullOrWhiteSpace(configured))
        {
            key = RandomNumberGenerator.GetBytes(SyncCursor.MinimumKeyLength);
            CursorKeyLog.EphemeralKeyGenerated(logger);
            return;
        }

        if (!TryDecodeKey(configured, out key))
        {
            throw new InvalidOperationException(
                "Sync:CursorSigningKey must be base64 and decode to at least "
                + $"{SyncCursor.MinimumKeyLength} bytes.");
        }
    }

    /// <inheritdoc />
    public ReadOnlySpan<byte> Key => key;

    private static bool TryDecodeKey(string configured, out byte[] key)
    {
        key = [];

        Span<byte> buffer = new byte[configured.Length];
        if (!Convert.TryFromBase64String(configured, buffer, out var written)
            || written < SyncCursor.MinimumKeyLength)
        {
            return false;
        }

        key = buffer[..written].ToArray();
        return true;
    }
}

internal static partial class CursorKeyLog
{
    [LoggerMessage(
        EventId = 2010,
        Level = LogLevel.Warning,
        Message = "Sync:CursorSigningKey is not configured, so an ephemeral per-process key was "
            + "generated. Cursors will not survive a restart and will be rejected across nodes, "
            + "causing clients to resync from the beginning. Configure it for any multi-node or "
            + "production deployment.")]
    internal static partial void EphemeralKeyGenerated(ILogger logger);
}
