using Npgsql;

namespace Femto.Database;

internal class Migrator(string migrationDirectory, NpgsqlDataSource dataSource) : IAsyncDisposable
{
    private record MigrationScript(string Id, string Script);
    
    public async Task Migrate()
    {
        var migrations = await LoadMigrationScripts();
        
        await this.EnsureMigrationsTableExists();
        var executedMigrations = await GetExecutedMigrations();

        EnsureConsistentMigrationHistory(migrations, executedMigrations);

        if (executedMigrations.Count == migrations.Count)
        {
            Console.WriteLine("up to date");
            return;
        }

        
        await DoMigrations(migrations.Skip(executedMigrations.Count));
    }

    private async Task<IList<MigrationScript>> LoadMigrationScripts()
    {
        if (!Directory.Exists(migrationDirectory))
            throw new DirectoryNotFoundException($"Migration directory not found: {migrationDirectory}");
    
        var scriptFiles = Directory.EnumerateFiles(migrationDirectory, "*.sql")
                                    .OrderBy(Path.GetFileName)
                                    .ToList();
    
        if (!scriptFiles.Any())
            throw new Exception("No migration scripts found in the specified directory.");
    
        var migrationScripts = new List<MigrationScript>();
    
        foreach (var file in scriptFiles)
        {
            var scriptContent = await File.ReadAllTextAsync(file);
            var scriptId = Path.GetFileName(file);
    
            migrationScripts.Add(new MigrationScript(scriptId, scriptContent));
        }
    
        return migrationScripts;
    }

    private async Task EnsureMigrationsTableExists()
    {
        await using var createSchemaCommand = dataSource.CreateCommand(
            """CREATE SCHEMA IF NOT EXISTS __migrations;"""
        );

        await createSchemaCommand.ExecuteNonQueryAsync();

        await using var createTableCommand = dataSource.CreateCommand(
            """
            CREATE TABLE IF NOT EXISTS __migrations.__migration_history ( migration VARCHAR(127) PRIMARY KEY );
            """
        );

        await createTableCommand.ExecuteNonQueryAsync();
    }

    private async Task<IList<string>> GetExecutedMigrations()
    {
        await using var command = dataSource.CreateCommand(
            """SELECT migration FROM __migrations.__migration_history"""
        );
        var reader = await command.ExecuteReaderAsync();
        var migrations = new List<string>();
        while (await reader.ReadAsync())
        {
            migrations.Add(reader.GetString(0));
        }
        return migrations;
    }

    private void EnsureConsistentMigrationHistory(
        IList<MigrationScript> migrationScripts,
        IList<string> executedMigrationIds
    )
    {
        if (executedMigrationIds.Count > migrationScripts.Count)
            throw new Exception("inconsistent migration history");

        for (var i = 0; i < executedMigrationIds.Count; i++)
        {
            var migration = migrationScripts[i];
            var executedMigrationId = executedMigrationIds[i];

            if (migration.Id != executedMigrationId)
                throw new Exception($"unexpected migration in history {executedMigrationId}");
        }
    }

    private async Task DoMigrations(IEnumerable<MigrationScript> migrationScripts)
    {
        foreach (var migration in migrationScripts)
        {
            Console.WriteLine($"applying migration {migration.Id}");
            await using var command = dataSource.CreateCommand(migration.Script);
            await command.ExecuteNonQueryAsync();
        
            await using var addToHistoryCommand = dataSource.CreateCommand(
                """INSERT INTO __migrations.__migration_history (migration) VALUES (@migration)"""
            );
            addToHistoryCommand.Parameters.AddWithValue("@migration", migration.Id);
            await addToHistoryCommand.ExecuteNonQueryAsync();
        }
    }

    public ValueTask DisposeAsync()
    {
        return dataSource.DisposeAsync();
    }
}
