using Microsoft.Data.Sqlite;

namespace ZB.MOM.WW.Auth.ApiKeys.Sqlite;

/// <summary>
/// Factory for creating and opening SQLite connections to the API-key store.
/// </summary>
public sealed class AuthSqliteConnectionFactory
{
    /// <summary>
    /// Busy timeout applied to every connection. SQLite retries a busy database for
    /// this long before surfacing <c>SQLITE_BUSY</c>, so the concurrent
    /// mark-used / audit-append writers degrade gracefully under load instead of
    /// failing the request path.
    /// </summary>
    private static readonly TimeSpan BusyTimeout = TimeSpan.FromSeconds(5);

    private readonly string _sqlitePath;

    /// <summary>Creates a factory targeting the database at <paramref name="sqlitePath"/>.</summary>
    /// <param name="sqlitePath">Filesystem path of the SQLite database file.</param>
    public AuthSqliteConnectionFactory(string sqlitePath)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(sqlitePath);
        _sqlitePath = sqlitePath;
    }

    /// <summary>
    /// Creates an unopened SQLite connection (Mode=ReadWriteCreate). Prefer
    /// <see cref="OpenConnectionAsync"/>, which also applies WAL journaling and the
    /// busy timeout.
    /// </summary>
    public SqliteConnection CreateConnection()
    {
        string? directory = Path.GetDirectoryName(_sqlitePath);

        if (!string.IsNullOrWhiteSpace(directory))
        {
            Directory.CreateDirectory(directory);
        }

        SqliteConnectionStringBuilder builder = new()
        {
            DataSource = _sqlitePath,
            Mode = SqliteOpenMode.ReadWriteCreate,
            Pooling = true,
            DefaultTimeout = (int)BusyTimeout.TotalSeconds,
        };

        return new SqliteConnection(builder.ToString());
    }

    /// <summary>
    /// Creates a SQLite connection, opens it, and configures WAL journaling and a
    /// non-zero busy timeout so concurrent readers and writers degrade gracefully
    /// rather than surfacing <c>SQLITE_BUSY</c> as a hard failure.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token for the operation.</param>
    /// <returns>An opened and configured SQLite connection.</returns>
    public async Task<SqliteConnection> OpenConnectionAsync(CancellationToken cancellationToken)
    {
        SqliteConnection connection = CreateConnection();
        try
        {
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
            await ConfigureConnectionAsync(connection, cancellationToken).ConfigureAwait(false);
            return connection;
        }
        catch
        {
            await connection.DisposeAsync().ConfigureAwait(false);
            throw;
        }
    }

    private static async Task ConfigureConnectionAsync(
        SqliteConnection connection,
        CancellationToken cancellationToken)
    {
        // WAL is a persistent, database-level setting; re-applying it per connection
        // is cheap and a no-op once set. busy_timeout is per-connection state.
        await using SqliteCommand command = connection.CreateCommand();
        command.CommandText =
            $"PRAGMA journal_mode=WAL; PRAGMA busy_timeout={(int)BusyTimeout.TotalMilliseconds};";
        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }
}
