using System.Globalization;
using Microsoft.Data.Sqlite;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.Secrets.Sqlite;

/// <summary>
/// Creates the secret store schema and records the applied version. Idempotent: it is
/// safe to run repeatedly. Refuses to run against a database whose on-disk version is
/// newer than this build supports.
/// </summary>
public sealed class SqliteSecretsStoreMigrator(SecretsSqliteConnectionFactory connectionFactory)
    : ISecretsStoreMigrator
{
    /// <summary>Applies the schema migration to the secret store.</summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <exception cref="SecretStoreMigrationException">
    /// The on-disk schema version is newer than <see cref="SqliteSecretsSchema.CurrentVersion"/>.
    /// </exception>
    public async Task MigrateAsync(CancellationToken cancellationToken)
    {
        await using SqliteConnection connection =
            await connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);

        await using SqliteTransaction transaction =
            (SqliteTransaction)await connection.BeginTransactionAsync(System.Data.IsolationLevel.Serializable, cancellationToken).ConfigureAwait(false);

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

        if (existingVersion > SqliteSecretsSchema.CurrentVersion)
        {
            throw new SecretStoreMigrationException(
                $"Secret database schema version {existingVersion} is newer than supported version {SqliteSecretsSchema.CurrentVersion}.");
        }

        await ApplySchemaAsync(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", SqliteSecretsSchema.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, CultureInfo.InvariantCulture);
    }

    // Single-shot create of the final schema (all DDL is CREATE ... IF NOT EXISTS, so it is idempotent
    // against an already-provisioned database). The applied version is stamped separately by
    // WriteSchemaVersionAsync.
    //
    // Forward-migration hook: when CurrentVersion advances past 1, any additive column added to the
    // secret table must ALSO be applied here to pre-existing databases via an idempotent
    // "ALTER TABLE secret ADD COLUMN ..." guarded by a `PRAGMA table_info(secret)` column-existence
    // probe (SQLite has no "ADD COLUMN IF NOT EXISTS"). Fully consume and dispose the PRAGMA reader
    // before issuing the ALTER on the same connection/transaction. See the sibling Auth migrator
    // (SqliteAuthStoreMigrator.EnsureApiKeysColumnAsync) for the proven pattern. Schema v1 has no
    // additive ALTERs, so the CREATE above is the whole migration.
    private static async Task ApplySchemaAsync(
        SqliteConnection connection,
        SqliteTransaction transaction,
        CancellationToken cancellationToken)
    {
        await ExecuteNonQueryAsync(
            connection,
            transaction,
            string.Join(
                "\n",
                SqliteSecretsSchema.CreateSchemaVersionTable,
                SqliteSecretsSchema.CreateSecretTable,
                SqliteSecretsSchema.CreateIndexes),
            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", SqliteSecretsSchema.CurrentVersion);
        versionCommand.Parameters.AddWithValue("$applied_utc", DateTimeOffset.UtcNow.ToString("O"));

        await versionCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(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);
    }
}
