using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
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.Authority.Storage.Mongo.Documents;
using StellaOps.Authority.Storage.Mongo.Stores;
using StellaOps.Configuration;

namespace StellaOps.Authority.Revocation;

internal sealed class RevocationBundleBuilder
{
    private const string SchemaVersion = "1.0.0";
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.General)
    {
        PropertyNamingPolicy = null,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = true
    };

    private readonly IAuthorityTokenStore tokenStore;
    private readonly IAuthorityRevocationStore revocationStore;
    private readonly IAuthorityRevocationExportStateStore stateStore;
    private readonly StellaOpsAuthorityOptions authorityOptions;
    private readonly TimeProvider clock;
    private readonly ILogger<RevocationBundleBuilder> logger;

    public RevocationBundleBuilder(
        IAuthorityTokenStore tokenStore,
        IAuthorityRevocationStore revocationStore,
        IAuthorityRevocationExportStateStore stateStore,
        IOptions<StellaOpsAuthorityOptions> authorityOptions,
        TimeProvider clock,
        ILogger<RevocationBundleBuilder> logger)
    {
        this.tokenStore = tokenStore ?? throw new ArgumentNullException(nameof(tokenStore));
        this.revocationStore = revocationStore ?? throw new ArgumentNullException(nameof(revocationStore));
        this.stateStore = stateStore ?? throw new ArgumentNullException(nameof(stateStore));
        this.authorityOptions = authorityOptions?.Value ?? throw new ArgumentNullException(nameof(authorityOptions));
        this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<RevocationBundleBuildResult> BuildAsync(CancellationToken cancellationToken)
    {
        var issuer = authorityOptions.Issuer?.ToString()?.TrimEnd('/')
            ?? throw new InvalidOperationException("Authority issuer configuration is required before exporting revocations.");

        var state = await stateStore.GetAsync(cancellationToken).ConfigureAwait(false);
        var previousSequence = state?.Sequence ?? 0;
        var sequence = previousSequence + 1;
        var issuedAt = clock.GetUtcNow();

        var tokenDocuments = await tokenStore.ListRevokedAsync(null, cancellationToken).ConfigureAwait(false);
        var manualDocuments = await revocationStore.GetActiveAsync(issuedAt, cancellationToken).ConfigureAwait(false);

        var entries = new List<RevocationEntryModel>();
        entries.AddRange(BuildTokenEntries(tokenDocuments, issuedAt));
        entries.AddRange(BuildManualEntries(manualDocuments));

        entries.Sort(static (left, right) =>
        {
            var categoryCompare = string.CompareOrdinal(left.Category, right.Category);
            if (categoryCompare != 0)
            {
                return categoryCompare;
            }

            var idCompare = string.CompareOrdinal(left.Id, right.Id);
            if (idCompare != 0)
            {
                return idCompare;
            }

            return DateTimeOffset.Compare(left.RevokedAt, right.RevokedAt);
        });

        var metadata = new SortedDictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
        {
            ["entryCount"] = entries.Count.ToString(CultureInfo.InvariantCulture)
        };

        var bundle = new RevocationBundleModel
        {
            SchemaVersion = SchemaVersion,
            Issuer = issuer,
            IssuedAt = issuedAt,
            ValidFrom = issuedAt,
            Sequence = sequence,
            SigningKeyId = authorityOptions.Signing?.ActiveKeyId,
            Revocations = entries,
            Metadata = metadata
        };

        var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(bundle, SerializerOptions);
        var sha256 = Convert.ToHexString(SHA256.HashData(jsonBytes)).ToLowerInvariant();
        bundle.BundleId = sha256;

        await PersistStateAsync(previousSequence, sequence, sha256, issuedAt, cancellationToken).ConfigureAwait(false);

        return new RevocationBundleBuildResult(bundle, jsonBytes, sha256, sequence, issuedAt);
    }

    private IEnumerable<RevocationEntryModel> BuildTokenEntries(IReadOnlyCollection<AuthorityTokenDocument> documents, DateTimeOffset issuedAt)
    {
        foreach (var document in documents)
        {
            if (!string.Equals(document.Status, "revoked", StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            if (document.ExpiresAt is { } expires && expires <= issuedAt)
            {
                continue;
            }

            var revocationId = document.TokenId;
            if (string.IsNullOrWhiteSpace(revocationId))
            {
                continue;
            }

            var scopes = document.Scope.Count > 0
                ? document.Scope
                    .Where(scope => !string.IsNullOrWhiteSpace(scope))
                    .Select(scope => scope.Trim())
                    .Distinct(StringComparer.Ordinal)
                    .OrderBy(scope => scope, StringComparer.Ordinal)
                    .ToList()
                : null;

            var metadata = document.RevokedMetadata is null
                ? null
                : new SortedDictionary<string, string?>(document.RevokedMetadata, StringComparer.OrdinalIgnoreCase);

            yield return new RevocationEntryModel
            {
                Id = revocationId,
                Category = "token",
                TokenType = document.Type,
                SubjectId = Normalize(document.SubjectId),
                ClientId = Normalize(document.ClientId),
                Reason = NormalizeReason(document.RevokedReason) ?? "unspecified",
                ReasonDescription = Normalize(document.RevokedReasonDescription),
                RevokedAt = document.RevokedAt ?? document.CreatedAt,
                EffectiveAt = document.RevokedAt ?? document.CreatedAt,
                ExpiresAt = document.ExpiresAt,
                Scopes = scopes,
                Metadata = metadata
            };
        }
    }

    private static IEnumerable<RevocationEntryModel> BuildManualEntries(IReadOnlyCollection<AuthorityRevocationDocument> documents)
    {
        foreach (var document in documents)
        {
            var metadata = document.Metadata is null
                ? null
                : new SortedDictionary<string, string?>(document.Metadata, StringComparer.OrdinalIgnoreCase);

            var scopes = document.Scopes is null
                ? null
                : document.Scopes
                    .Where(scope => !string.IsNullOrWhiteSpace(scope))
                    .Select(scope => scope.Trim())
                    .Distinct(StringComparer.Ordinal)
                    .OrderBy(scope => scope, StringComparer.Ordinal)
                    .ToList();

            yield return new RevocationEntryModel
            {
                Id = document.RevocationId,
                Category = document.Category,
                TokenType = Normalize(document.TokenType),
                SubjectId = Normalize(document.SubjectId),
                ClientId = Normalize(document.ClientId),
                Reason = NormalizeReason(document.Reason) ?? "unspecified",
                ReasonDescription = Normalize(document.ReasonDescription),
                RevokedAt = document.RevokedAt,
                EffectiveAt = document.EffectiveAt ?? document.RevokedAt,
                ExpiresAt = document.ExpiresAt,
                Scopes = scopes,
                Fingerprint = Normalize(document.Fingerprint),
                Metadata = metadata
            };
        }
    }

    private async Task PersistStateAsync(long previousSequence, long newSequence, string bundleId, DateTimeOffset issuedAt, CancellationToken cancellationToken)
    {
        try
        {
            await stateStore.UpdateAsync(previousSequence, newSequence, bundleId, issuedAt, cancellationToken).ConfigureAwait(false);
        }
        catch (InvalidOperationException ex)
        {
            logger.LogError(ex, "Failed to update revocation export state (expected sequence {Expected}, new sequence {Sequence}).", previousSequence, newSequence);
            throw;
        }
    }

    private static string? Normalize(string? value)
        => string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static string? NormalizeReason(string? reason)
    {
        var normalized = Normalize(reason);
        return normalized?.ToLowerInvariant();
    }
}
