using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Configuration;
using StellaOps.Cryptography;

namespace StellaOps.Authority.Revocation;

internal sealed class RevocationBundleSigner
{
    private static readonly JsonSerializerOptions HeaderSerializerOptions = new(JsonSerializerDefaults.General)
    {
        PropertyNamingPolicy = null,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false
    };

    private readonly ICryptoProviderRegistry providerRegistry;
    private readonly StellaOpsAuthorityOptions authorityOptions;
    private readonly ILogger<RevocationBundleSigner> logger;

    public RevocationBundleSigner(
        ICryptoProviderRegistry providerRegistry,
        IOptions<StellaOpsAuthorityOptions> authorityOptions,
        ILogger<RevocationBundleSigner> logger)
    {
        this.providerRegistry = providerRegistry ?? throw new ArgumentNullException(nameof(providerRegistry));
        this.authorityOptions = authorityOptions?.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<RevocationBundleSignature> SignAsync(byte[] payload, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(payload);

        var signing = authorityOptions.Signing ?? throw new InvalidOperationException("Authority signing configuration is required to export revocations.");
        if (string.IsNullOrWhiteSpace(signing.ActiveKeyId))
        {
            throw new InvalidOperationException("Authority signing configuration requires an active key identifier.");
        }

        var algorithm = string.IsNullOrWhiteSpace(signing.Algorithm)
            ? SignatureAlgorithms.Es256
            : signing.Algorithm.Trim();

        var keyReference = new CryptoKeyReference(signing.ActiveKeyId, signing.Provider);
        var resolved = providerRegistry.ResolveSigner(
            CryptoCapability.Signing,
            algorithm,
            keyReference,
            signing.Provider);
        var signer = resolved.Signer;

        var header = new Dictionary<string, object>
        {
            ["alg"] = algorithm,
            ["kid"] = signing.ActiveKeyId,
            ["provider"] = resolved.ProviderName,
            ["typ"] = "application/vnd.stellaops.revocation-bundle+jws",
            ["b64"] = false,
            ["crit"] = new[] { "b64" }
        };

        var headerJson = JsonSerializer.Serialize(header, HeaderSerializerOptions);
        var protectedHeader = Base64UrlEncode(Encoding.UTF8.GetBytes(headerJson));

        var signingInputLength = protectedHeader.Length + 1 + payload.Length;
        var buffer = ArrayPool<byte>.Shared.Rent(signingInputLength);
        try
        {
            var headerBytes = Encoding.ASCII.GetBytes(protectedHeader);
            Buffer.BlockCopy(headerBytes, 0, buffer, 0, headerBytes.Length);
            buffer[headerBytes.Length] = (byte)'.';
            Buffer.BlockCopy(payload, 0, buffer, headerBytes.Length + 1, payload.Length);

            var signingInput = new ReadOnlyMemory<byte>(buffer, 0, signingInputLength);
            var signatureBytes = await signer.SignAsync(signingInput, cancellationToken).ConfigureAwait(false);
            var encodedSignature = Base64UrlEncode(signatureBytes);
            return new RevocationBundleSignature(
                algorithm,
                signing.ActiveKeyId,
                resolved.ProviderName,
                string.Concat(protectedHeader, "..", encodedSignature));
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }

    private static string Base64UrlEncode(ReadOnlySpan<byte> value)
    {
        var encoded = Convert.ToBase64String(value);
        var builder = new StringBuilder(encoded.Length);
        foreach (var ch in encoded)
        {
            switch (ch)
            {
                case '+':
                    builder.Append('-');
                    break;
                case '/':
                    builder.Append('_');
                    break;
                case '=':
                    break;
                default:
                    builder.Append(ch);
                    break;
            }
        }

        return builder.ToString();
    }
}
