using Microsoft.Data.SqlClient;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace XLAB2.Infrastructure;

internal static class SqlAsync
{
    public static async Task<List<T>> QueryAsync<T>(
        this SqlConnection connection,
        string commandText,
        Func<SqlDataReader, T> map,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        if (map == null)
        {
            throw new ArgumentNullException(nameof(map));
        }

        using var command = connection.CreateCommand();
        command.CommandText = commandText;
        configureCommand?.Invoke(command);

        using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        var items = new List<T>();

        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            items.Add(map(reader));
        }

        return items;
    }

    public static async Task<List<T>> QueryAsync<T>(
        this SqlConnection connection,
        SqlTransaction transaction,
        string commandText,
        Func<SqlDataReader, T> map,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        if (map == null)
        {
            throw new ArgumentNullException(nameof(map));
        }

        using var command = new SqlCommand(commandText, connection, transaction);
        configureCommand?.Invoke(command);

        using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
        var items = new List<T>();

        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
        {
            items.Add(map(reader));
        }

        return items;
    }

    public static async Task<int> ExecuteNonQueryAsync(
        this SqlConnection connection,
        string commandText,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        using var command = connection.CreateCommand();
        command.CommandText = commandText;
        configureCommand?.Invoke(command);
        return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }

    public static async Task<int> ExecuteNonQueryAsync(
        this SqlConnection connection,
        SqlTransaction transaction,
        string commandText,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        using var command = new SqlCommand(commandText, connection, transaction);
        configureCommand?.Invoke(command);
        return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }

    public static async Task<T> ExecuteScalarAsync<T>(
        this SqlConnection connection,
        string commandText,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        using var command = connection.CreateCommand();
        command.CommandText = commandText;
        configureCommand?.Invoke(command);

        var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
        if (result == null || result is DBNull)
        {
            return default!;
        }

        return (T)Convert.ChangeType(result, typeof(T));
    }

    public static async Task<T> ExecuteScalarAsync<T>(
        this SqlConnection connection,
        SqlTransaction transaction,
        string commandText,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connection == null)
        {
            throw new ArgumentNullException(nameof(connection));
        }

        using var command = new SqlCommand(commandText, connection, transaction);
        configureCommand?.Invoke(command);

        var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
        if (result == null || result is DBNull)
        {
            return default!;
        }

        return (T)Convert.ChangeType(result, typeof(T));
    }

    public static async Task<List<T>> QueryOpenConnectionAsync<T>(
        this IDatabaseConnectionFactory connectionFactory,
        string commandText,
        Func<SqlDataReader, T> map,
        Action<SqlCommand> configureCommand = null,
        CancellationToken cancellationToken = default)
    {
        if (connectionFactory == null)
        {
            throw new ArgumentNullException(nameof(connectionFactory));
        }

        await using var connection = await connectionFactory.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
        return await connection.QueryAsync(commandText, map, configureCommand, cancellationToken).ConfigureAwait(false);
    }
}
