using MongoDB.Bson;
using MongoDB.Driver;

namespace Guestlist;

public sealed record DatabaseBackupResult(
    string BackupDatabaseName,
    int CollectionCount,
    long DocumentCount);

public sealed class DatabaseBackupService(MongoDbContext db)
{
    private const string BackupMetadataCollectionName = "backup_metadata";
    private const string BackupSeparator = "_backup_";

    public async Task<DatabaseBackupResult> BackupAndDropActiveDatabaseAsync(string reason)
    {
        var backupDatabaseName = CreateBackupDatabaseName();
        var backupDatabase = db.Client.GetDatabase(backupDatabaseName);
        DatabaseBackupResult backupResult;

        try
        {
            backupResult = await CopyDatabaseAsync(db.Database, backupDatabase);
            await WriteBackupMetadataAsync(backupDatabase, reason, backupResult);
        }
        catch
        {
            await db.Client.DropDatabaseAsync(backupDatabaseName);
            throw;
        }

        await db.Client.DropDatabaseAsync(db.DatabaseName);

        return backupResult with { BackupDatabaseName = backupDatabaseName };
    }

    public async Task<DatabaseBackupResult?> RestoreLatestDatabaseBackupAsync()
    {
        var backupDatabaseName = await GetLatestBackupDatabaseNameAsync();

        if (backupDatabaseName is null)
        {
            return null;
        }

        var backupDatabase = db.Client.GetDatabase(backupDatabaseName);

        await db.Client.DropDatabaseAsync(db.DatabaseName);

        return await CopyDatabaseAsync(backupDatabase, db.Database) with
        {
            BackupDatabaseName = backupDatabaseName
        };
    }

    public async Task<bool> HasAnyDatabaseBackupsAsync()
    {
        return await GetLatestBackupDatabaseNameAsync() is not null;
    }

    private async Task<string?> GetLatestBackupDatabaseNameAsync()
    {
        var databaseNames = await db.Client.ListDatabaseNames().ToListAsync();
        var backupPrefix = CreateBackupPrefix();

        return databaseNames
            .Where(name => name.StartsWith(backupPrefix, StringComparison.Ordinal))
            .OrderByDescending(name => name)
            .FirstOrDefault();
    }

    private string CreateBackupDatabaseName()
    {
        var suffix = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}"[..23];
        return $"{CreateBackupPrefix(suffix.Length)}{suffix}";
    }

    private string CreateBackupPrefix(int suffixLength = 23)
    {
        var maxBaseNameLength = Math.Max(1, 63 - BackupSeparator.Length - suffixLength);
        var baseName = db.DatabaseName.Length > maxBaseNameLength
            ? db.DatabaseName[..maxBaseNameLength]
            : db.DatabaseName;

        return $"{baseName}{BackupSeparator}";
    }

    private static async Task<DatabaseBackupResult> CopyDatabaseAsync(
        IMongoDatabase sourceDatabase,
        IMongoDatabase targetDatabase)
    {
        var collectionNames = await sourceDatabase.ListCollectionNames().ToListAsync();
        var copiedCollections = 0;
        long copiedDocuments = 0;

        foreach (var collectionName in collectionNames.Where(IsBackupEligibleCollection))
        {
            var sourceCollection = sourceDatabase.GetCollection<BsonDocument>(collectionName);
            var targetCollection = targetDatabase.GetCollection<BsonDocument>(collectionName);
            var documents = await sourceCollection
                .Find(FilterDefinition<BsonDocument>.Empty)
                .ToListAsync();

            if (documents.Count > 0)
            {
                await targetCollection.InsertManyAsync(documents);
                copiedDocuments += documents.Count;
            }
            else
            {
                await targetDatabase.CreateCollectionAsync(collectionName);
            }

            copiedCollections++;
        }

        return new DatabaseBackupResult("", copiedCollections, copiedDocuments);
    }

    private static bool IsBackupEligibleCollection(string collectionName)
    {
        return collectionName != BackupMetadataCollectionName &&
               !collectionName.StartsWith("system.", StringComparison.Ordinal);
    }

    private static async Task WriteBackupMetadataAsync(
        IMongoDatabase backupDatabase,
        string reason,
        DatabaseBackupResult backupResult)
    {
        var metadata = new BsonDocument
        {
            ["reason"] = reason,
            ["backedUpAt"] = DateTime.UtcNow,
            ["collectionCount"] = backupResult.CollectionCount,
            ["documentCount"] = backupResult.DocumentCount
        };

        await backupDatabase
            .GetCollection<BsonDocument>(BackupMetadataCollectionName)
            .InsertOneAsync(metadata);
    }
}
