using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;

namespace StellaOps.Feedser.Storage.Mongo.Migrations;

/// <summary>
/// Executes pending schema migrations tracked inside MongoDB to keep upgrades deterministic.
/// </summary>
public sealed class MongoMigrationRunner
{
    private readonly IMongoDatabase _database;
    private readonly IReadOnlyList<IMongoMigration> _migrations;
    private readonly ILogger<MongoMigrationRunner> _logger;
    private readonly TimeProvider _timeProvider;

    public MongoMigrationRunner(
        IMongoDatabase database,
        IEnumerable<IMongoMigration> migrations,
        ILogger<MongoMigrationRunner> logger,
        TimeProvider? timeProvider = null)
    {
        _database = database ?? throw new ArgumentNullException(nameof(database));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timeProvider = timeProvider ?? TimeProvider.System;
        _migrations = (migrations ?? throw new ArgumentNullException(nameof(migrations)))
            .OrderBy(m => m.Id, StringComparer.Ordinal)
            .ToArray();
    }

    public async Task RunAsync(CancellationToken cancellationToken)
    {
        if (_migrations.Count == 0)
        {
            return;
        }

        var collection = _database.GetCollection<MongoMigrationDocument>(MongoStorageDefaults.Collections.Migrations);
        await EnsureCollectionExistsAsync(_database, cancellationToken).ConfigureAwait(false);

        var appliedIds = await LoadAppliedMigrationIdsAsync(collection, cancellationToken).ConfigureAwait(false);
        foreach (var migration in _migrations)
        {
            if (appliedIds.Contains(migration.Id, StringComparer.Ordinal))
            {
                continue;
            }

            _logger.LogInformation("Applying Mongo migration {MigrationId}: {Description}", migration.Id, migration.Description);
            try
            {
                await migration.ApplyAsync(_database, cancellationToken).ConfigureAwait(false);
                var document = new MongoMigrationDocument
                {
                    Id = migration.Id,
                    Description = string.IsNullOrWhiteSpace(migration.Description) ? null : migration.Description,
                    AppliedAtUtc = _timeProvider.GetUtcNow().UtcDateTime,
                };

                await collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
                _logger.LogInformation("Mongo migration {MigrationId} applied", migration.Id);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Mongo migration {MigrationId} failed", migration.Id);
                throw;
            }
        }
    }

    private static async Task<HashSet<string>> LoadAppliedMigrationIdsAsync(
        IMongoCollection<MongoMigrationDocument> collection,
        CancellationToken cancellationToken)
    {
        using var cursor = await collection.FindAsync(FilterDefinition<MongoMigrationDocument>.Empty, cancellationToken: cancellationToken).ConfigureAwait(false);
        var applied = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
        var set = new HashSet<string>(StringComparer.Ordinal);
        foreach (var document in applied)
        {
            if (!string.IsNullOrWhiteSpace(document.Id))
            {
                set.Add(document.Id);
            }
        }

        return set;
    }

    private static async Task EnsureCollectionExistsAsync(IMongoDatabase database, CancellationToken cancellationToken)
    {
        using var cursor = await database.ListCollectionNamesAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
        var names = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false);
        if (!names.Contains(MongoStorageDefaults.Collections.Migrations, StringComparer.Ordinal))
        {
            await database.CreateCollectionAsync(MongoStorageDefaults.Collections.Migrations, cancellationToken: cancellationToken).ConfigureAwait(false);
        }
    }
}
