using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;

namespace StellaOps.Feedser.Storage.Mongo.Migrations;

internal sealed class EnsureGridFsExpiryIndexesMigration : IMongoMigration
{
    private readonly MongoStorageOptions _options;

    public EnsureGridFsExpiryIndexesMigration(IOptions<MongoStorageOptions> options)
    {
        ArgumentNullException.ThrowIfNull(options);
        _options = options.Value;
    }

    public string Id => "20241005_gridfs_expiry_indexes";

    public string Description => "Ensure GridFS metadata.expiresAt TTL index reflects retention settings";

    public async Task ApplyAsync(IMongoDatabase database, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(database);

        var needsTtl = _options.RawDocumentRetention > TimeSpan.Zero;
        var collection = database.GetCollection<BsonDocument>("documents.files");

        using var cursor = await collection.Indexes.ListAsync(cancellationToken).ConfigureAwait(false);
        var indexes = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);

        var ttlIndex = indexes.FirstOrDefault(x => TryGetName(x, out var name) && string.Equals(name, "gridfs_files_expiresAt_ttl", StringComparison.Ordinal));

        if (needsTtl)
        {
            var shouldRebuild = ttlIndex is null || !IndexMatchesTtlExpectations(ttlIndex);
            if (shouldRebuild)
            {
                if (ttlIndex is not null)
                {
                    await collection.Indexes.DropOneAsync("gridfs_files_expiresAt_ttl", cancellationToken).ConfigureAwait(false);
                }

                var keys = Builders<BsonDocument>.IndexKeys.Ascending("metadata.expiresAt");
                var options = new CreateIndexOptions<BsonDocument>
                {
                    Name = "gridfs_files_expiresAt_ttl",
                    ExpireAfter = TimeSpan.Zero,
                    PartialFilterExpression = Builders<BsonDocument>.Filter.Exists("metadata.expiresAt", true),
                };

                await collection.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(keys, options), cancellationToken: cancellationToken).ConfigureAwait(false);
            }
        }
        else if (ttlIndex is not null)
        {
            await collection.Indexes.DropOneAsync("gridfs_files_expiresAt_ttl", cancellationToken).ConfigureAwait(false);
        }
    }

    private static bool IndexMatchesTtlExpectations(BsonDocument index)
    {
        if (!index.TryGetValue("expireAfterSeconds", out var expireAfter) || expireAfter.ToDouble() != 0)
        {
            return false;
        }

        if (!index.TryGetValue("partialFilterExpression", out var partialFilter) || partialFilter is not BsonDocument partialDoc)
        {
            return false;
        }

        if (!partialDoc.TryGetValue("metadata.expiresAt", out var expiresAtRule) || expiresAtRule is not BsonDocument expiresAtDoc)
        {
            return false;
        }

        return expiresAtDoc.Contains("$exists") && expiresAtDoc["$exists"].ToBoolean();
    }

    private static bool TryGetName(BsonDocument index, out string name)
    {
        if (index.TryGetValue("name", out var value) && value.IsString)
        {
            name = value.AsString;
            return true;
        }

        name = string.Empty;
        return false;
    }
}
