using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
using StellaOps.Authority.Storage.Mongo.Documents;

namespace StellaOps.Authority.Storage.Mongo.Stores;

internal sealed class AuthorityRevocationStore : IAuthorityRevocationStore
{
    private readonly IMongoCollection<AuthorityRevocationDocument> collection;
    private readonly ILogger<AuthorityRevocationStore> logger;

    public AuthorityRevocationStore(
        IMongoCollection<AuthorityRevocationDocument> collection,
        ILogger<AuthorityRevocationStore> logger)
    {
        this.collection = collection ?? throw new ArgumentNullException(nameof(collection));
        this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async ValueTask UpsertAsync(AuthorityRevocationDocument document, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(document);

        if (string.IsNullOrWhiteSpace(document.Category))
        {
            throw new ArgumentException("Revocation category is required.", nameof(document));
        }

        if (string.IsNullOrWhiteSpace(document.RevocationId))
        {
            throw new ArgumentException("Revocation identifier is required.", nameof(document));
        }

        document.Category = document.Category.Trim();
        document.RevocationId = document.RevocationId.Trim();
        document.Scopes = NormalizeScopes(document.Scopes);
        document.Metadata = NormalizeMetadata(document.Metadata);

        var filter = Builders<AuthorityRevocationDocument>.Filter.And(
            Builders<AuthorityRevocationDocument>.Filter.Eq(d => d.Category, document.Category),
            Builders<AuthorityRevocationDocument>.Filter.Eq(d => d.RevocationId, document.RevocationId));

        var now = DateTimeOffset.UtcNow;
        document.UpdatedAt = now;

        var existing = await collection
            .Find(filter)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        if (existing is null)
        {
            document.CreatedAt = now;
        }
        else
        {
            document.Id = existing.Id;
            document.CreatedAt = existing.CreatedAt;
        }

        await collection.ReplaceOneAsync(filter, document, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
        logger.LogDebug("Upserted Authority revocation entry {Category}:{RevocationId}.", document.Category, document.RevocationId);
    }

    public async ValueTask<bool> RemoveAsync(string category, string revocationId, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(revocationId))
        {
            return false;
        }

        var filter = Builders<AuthorityRevocationDocument>.Filter.And(
            Builders<AuthorityRevocationDocument>.Filter.Eq(d => d.Category, category.Trim()),
            Builders<AuthorityRevocationDocument>.Filter.Eq(d => d.RevocationId, revocationId.Trim()));

        var result = await collection.DeleteOneAsync(filter, cancellationToken).ConfigureAwait(false);
        if (result.DeletedCount > 0)
        {
            logger.LogInformation("Removed Authority revocation entry {Category}:{RevocationId}.", category, revocationId);
            return true;
        }

        return false;
    }

    public async ValueTask<IReadOnlyList<AuthorityRevocationDocument>> GetActiveAsync(DateTimeOffset asOf, CancellationToken cancellationToken)
    {
        var filter = Builders<AuthorityRevocationDocument>.Filter.Or(
            Builders<AuthorityRevocationDocument>.Filter.Eq(d => d.ExpiresAt, null),
            Builders<AuthorityRevocationDocument>.Filter.Gt(d => d.ExpiresAt, asOf));

        var documents = await collection
            .Find(filter)
            .Sort(Builders<AuthorityRevocationDocument>.Sort.Ascending(d => d.Category).Ascending(d => d.RevocationId))
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

        return documents;
    }

    private static List<string>? NormalizeScopes(List<string>? scopes)
    {
        if (scopes is null || scopes.Count == 0)
        {
            return null;
        }

        var distinct = scopes
            .Where(scope => !string.IsNullOrWhiteSpace(scope))
            .Select(scope => scope.Trim())
            .Distinct(StringComparer.Ordinal)
            .OrderBy(scope => scope, StringComparer.Ordinal)
            .ToList();

        return distinct.Count == 0 ? null : distinct;
    }

    private static Dictionary<string, string?>? NormalizeMetadata(Dictionary<string, string?>? metadata)
    {
        if (metadata is null || metadata.Count == 0)
        {
            return null;
        }

        var result = new SortedDictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
        foreach (var pair in metadata)
        {
            if (string.IsNullOrWhiteSpace(pair.Key))
            {
                continue;
            }

            result[pair.Key.Trim()] = pair.Value;
        }

        return result.Count == 0 ? null : new Dictionary<string, string?>(result, StringComparer.OrdinalIgnoreCase);
    }
}
