using System;
using System.Collections.Generic;
using MongoDB.Driver;
using StellaOps.Authority.Storage.Mongo.Documents;

namespace StellaOps.Authority.Storage.Mongo.Stores;

internal sealed class AuthorityBootstrapInviteStore : IAuthorityBootstrapInviteStore
{
    private readonly IMongoCollection<AuthorityBootstrapInviteDocument> collection;

    public AuthorityBootstrapInviteStore(IMongoCollection<AuthorityBootstrapInviteDocument> collection)
        => this.collection = collection ?? throw new ArgumentNullException(nameof(collection));

    public async ValueTask<AuthorityBootstrapInviteDocument> CreateAsync(AuthorityBootstrapInviteDocument document, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(document);

        await collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
        return document;
    }

    public async ValueTask<BootstrapInviteReservationResult> TryReserveAsync(
        string token,
        string expectedType,
        DateTimeOffset now,
        string? reservedBy,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.NotFound, null);
        }

        var normalizedToken = token.Trim();
        var filter = Builders<AuthorityBootstrapInviteDocument>.Filter.And(
            Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Token, normalizedToken),
            Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Status, AuthorityBootstrapInviteStatuses.Pending));

        var update = Builders<AuthorityBootstrapInviteDocument>.Update
            .Set(i => i.Status, AuthorityBootstrapInviteStatuses.Reserved)
            .Set(i => i.ReservedAt, now)
            .Set(i => i.ReservedBy, reservedBy);

        var options = new FindOneAndUpdateOptions<AuthorityBootstrapInviteDocument>
        {
            ReturnDocument = ReturnDocument.After
        };

        var invite = await collection.FindOneAndUpdateAsync(filter, update, options, cancellationToken).ConfigureAwait(false);

        if (invite is null)
        {
            var existing = await collection
                .Find(i => i.Token == normalizedToken)
                .FirstOrDefaultAsync(cancellationToken)
                .ConfigureAwait(false);

            if (existing is null)
            {
                return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.NotFound, null);
            }

            if (existing.Status is AuthorityBootstrapInviteStatuses.Consumed or AuthorityBootstrapInviteStatuses.Reserved)
            {
                return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.AlreadyUsed, existing);
            }

            if (existing.Status == AuthorityBootstrapInviteStatuses.Expired || existing.ExpiresAt <= now)
            {
                return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.Expired, existing);
            }

            return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.NotFound, existing);
        }

        if (!string.Equals(invite.Type, expectedType, StringComparison.OrdinalIgnoreCase))
        {
            await ReleaseAsync(normalizedToken, cancellationToken).ConfigureAwait(false);
            return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.NotFound, invite);
        }

        if (invite.ExpiresAt <= now)
        {
            await MarkExpiredAsync(normalizedToken, cancellationToken).ConfigureAwait(false);
            return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.Expired, invite);
        }

        return new BootstrapInviteReservationResult(BootstrapInviteReservationStatus.Reserved, invite);
    }

    public async ValueTask<bool> ReleaseAsync(string token, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return false;
        }

        var result = await collection.UpdateOneAsync(
            Builders<AuthorityBootstrapInviteDocument>.Filter.And(
                Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Token, token.Trim()),
                Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Status, AuthorityBootstrapInviteStatuses.Reserved)),
            Builders<AuthorityBootstrapInviteDocument>.Update
                .Set(i => i.Status, AuthorityBootstrapInviteStatuses.Pending)
                .Set(i => i.ReservedAt, null)
                .Set(i => i.ReservedBy, null),
            cancellationToken: cancellationToken).ConfigureAwait(false);

        return result.ModifiedCount > 0;
    }

    public async ValueTask<bool> MarkConsumedAsync(string token, string? consumedBy, DateTimeOffset consumedAt, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return false;
        }

        var result = await collection.UpdateOneAsync(
            Builders<AuthorityBootstrapInviteDocument>.Filter.And(
                Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Token, token.Trim()),
                Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Status, AuthorityBootstrapInviteStatuses.Reserved)),
            Builders<AuthorityBootstrapInviteDocument>.Update
                .Set(i => i.Status, AuthorityBootstrapInviteStatuses.Consumed)
                .Set(i => i.ConsumedAt, consumedAt)
                .Set(i => i.ConsumedBy, consumedBy),
            cancellationToken: cancellationToken).ConfigureAwait(false);

        return result.ModifiedCount > 0;
    }

    public async ValueTask<IReadOnlyList<AuthorityBootstrapInviteDocument>> ExpireAsync(DateTimeOffset now, CancellationToken cancellationToken)
    {
        var filter = Builders<AuthorityBootstrapInviteDocument>.Filter.And(
            Builders<AuthorityBootstrapInviteDocument>.Filter.Lte(i => i.ExpiresAt, now),
            Builders<AuthorityBootstrapInviteDocument>.Filter.In(
                i => i.Status,
                new[] { AuthorityBootstrapInviteStatuses.Pending, AuthorityBootstrapInviteStatuses.Reserved }));

        var update = Builders<AuthorityBootstrapInviteDocument>.Update
            .Set(i => i.Status, AuthorityBootstrapInviteStatuses.Expired)
            .Set(i => i.ReservedAt, null)
            .Set(i => i.ReservedBy, null);

        var expired = await collection.Find(filter)
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

        if (expired.Count == 0)
        {
            return Array.Empty<AuthorityBootstrapInviteDocument>();
        }

        await collection.UpdateManyAsync(filter, update, cancellationToken: cancellationToken).ConfigureAwait(false);

        return expired;
    }

    private async Task MarkExpiredAsync(string token, CancellationToken cancellationToken)
    {
        await collection.UpdateOneAsync(
            Builders<AuthorityBootstrapInviteDocument>.Filter.Eq(i => i.Token, token),
            Builders<AuthorityBootstrapInviteDocument>.Update.Set(i => i.Status, AuthorityBootstrapInviteStatuses.Expired),
            cancellationToken: cancellationToken).ConfigureAwait(false);
    }
}
