using Microsoft.Data.Sqlite;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;

public sealed class SqliteAuthStoreMigrator(AuthSqliteConnectionFactory connectionFactory) : IAuthStoreMigrator
{
    /// <summary>Applies database migrations to the authentication store.</summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    public async Task MigrateAsync(CancellationToken cancellationToken)
    {
        await using SqliteConnection connection = await connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);

        await using SqliteTransaction transaction =
            (SqliteTransaction)await connection.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);

        int existingVersion = await ReadExistingSchemaVersionAsync(connection, transaction, cancellationToken)
            .ConfigureAwait(false);

        if (existingVersion > SqliteAuthSchema.CurrentVersion)
        {
            throw new AuthStoreMigrationException(
                $"Auth database schema version {existingVersion} is newer than supported version {SqliteAuthSchema.CurrentVersion}.");
        }

        await ApplyVersionOneAsync(connection, transaction, cancellationToken).ConfigureAwait(false);
        await ApplyVersionTwoAsync(connection, transaction, cancellationToken).ConfigureAwait(false);
        await WriteSchemaVersionAsync(connection, transaction, cancellationToken).ConfigureAwait(false);

        await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
    }

    private static async Task<int> ReadExistingSchemaVersionAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        CancellationToken cancellationToken)
    {
        await using SqliteCommand tableExistsCommand = connection.CreateCommand();
        tableExistsCommand.Transaction = transaction;
        tableExistsCommand.CommandText = """
            SELECT COUNT(*)
            FROM sqlite_master
            WHERE type = 'table' AND name = $table_name;
            """;
        tableExistsCommand.Parameters.AddWithValue("$table_name", SqliteAuthSchema.SchemaVersionTable);

        long tableCount = (long)(await tableExistsCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false) ?? 0L);

        if (tableCount == 0)
        {
            return 0;
        }

        await using SqliteCommand versionCommand = connection.CreateCommand();
        versionCommand.Transaction = transaction;
        versionCommand.CommandText = """
            SELECT version
            FROM schema_version
            WHERE id = 1;
            """;

        object? version = await versionCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

        return version is null || version == DBNull.Value
            ? 0
            : Convert.ToInt32(version, System.Globalization.CultureInfo.InvariantCulture);
    }

    private static async Task ApplyVersionOneAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        CancellationToken cancellationToken)
    {
        await ExecuteNonQueryAsync(
            connection,
            transaction,
            """
            CREATE TABLE IF NOT EXISTS schema_version (
                id INTEGER PRIMARY KEY CHECK (id = 1),
                version INTEGER NOT NULL,
                applied_utc TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS api_keys (
                key_id TEXT PRIMARY KEY,
                key_prefix TEXT NOT NULL,
                secret_hash BLOB NOT NULL,
                display_name TEXT NOT NULL,
                scopes TEXT NOT NULL,
                constraints TEXT NULL,
                created_utc TEXT NOT NULL,
                last_used_utc TEXT NULL,
                revoked_utc TEXT NULL
            );

            CREATE TABLE IF NOT EXISTS api_key_audit (
                audit_id INTEGER PRIMARY KEY AUTOINCREMENT,
                key_id TEXT NULL,
                event_type TEXT NOT NULL,
                remote_address TEXT NULL,
                created_utc TEXT NOT NULL,
                details TEXT NULL
            );

            CREATE INDEX IF NOT EXISTS ix_api_keys_revoked_utc
                ON api_keys (revoked_utc);

            CREATE INDEX IF NOT EXISTS ix_api_key_audit_key_id_created_utc
                ON api_key_audit (key_id, created_utc);
            """,
            cancellationToken).ConfigureAwait(false);

    }

    private static async Task ApplyVersionTwoAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        CancellationToken cancellationToken)
    {
        if (await ColumnExistsAsync(connection, transaction, SqliteAuthSchema.ApiKeysTable, "constraints", cancellationToken)
            .ConfigureAwait(false))
        {
            return;
        }

        await ExecuteNonQueryAsync(
            connection,
            transaction,
            """
            ALTER TABLE api_keys
            ADD COLUMN constraints TEXT NULL;
            """,
            cancellationToken).ConfigureAwait(false);
    }

    private static async Task WriteSchemaVersionAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        CancellationToken cancellationToken)
    {
        await using SqliteCommand versionCommand = connection.CreateCommand();
        versionCommand.Transaction = transaction;
        versionCommand.CommandText = """
            INSERT INTO schema_version (id, version, applied_utc)
            VALUES (1, $version, $applied_utc)
            ON CONFLICT(id) DO UPDATE SET
                version = excluded.version,
                applied_utc = excluded.applied_utc;
            """;
        versionCommand.Parameters.AddWithValue("$version", SqliteAuthSchema.CurrentVersion);
        versionCommand.Parameters.AddWithValue("$applied_utc", DateTimeOffset.UtcNow.ToString("O"));

        await versionCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }

    private static async Task<bool> ColumnExistsAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string tableName,
        string columnName,
        CancellationToken cancellationToken)
    {
        await using SqliteCommand command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = $"PRAGMA table_info({tableName});";

        await using SqliteDataReader reader = await command.ExecuteReaderAsync(cancellationToken)
            .ConfigureAwait(false);

        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            if (string.Equals(reader.GetString(1), columnName, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }

        return false;
    }

    private static async Task ExecuteNonQueryAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        string commandText,
        CancellationToken cancellationToken)
    {
        await using SqliteCommand command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = commandText;

        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }
}
