using System.Data.Common;
using System.Globalization;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI;

/// <summary>
/// Scoped, parameterized database access exposed to inbound API scripts as
/// <c>InboundScriptContext.Database</c>. This is the dedicated, curated data-access
/// helper authorized by the design doc (Component-InboundAPI.md, "Database access") —
/// not a raw connection handed to the script. All ADO.NET stays internal here: scripts
/// call <see cref="QueryAsync"/> / <see cref="QuerySingleAsync{T}"/> / <see cref="ExecuteAsync"/>
/// by name and never reference <c>System.Data</c>.
///
/// <para>
/// <b>SQL-injection protection.</b> Statement text is authored by the
/// (design-time) method script, but every <em>value</em> is bound as a named SQL
/// parameter (anonymous-object properties become <c>@</c>-prefixed parameters via
/// <see cref="AddParameters"/>) and is NEVER string-concatenated into the command text.
/// Request-derived values therefore reach the database only through parameter binding,
/// closing the injection vector. Connection access is restricted to the named
/// connections configured on the central <see cref="IDatabaseGateway"/> — a script
/// cannot supply an arbitrary connection string.
/// </para>
///
/// <para>
/// <b>Reads and writes are both permitted</b>: the move-in
/// integration needs to record results, not just read them. Use <see cref="QueryAsync"/>
/// / <see cref="QuerySingleAsync{T}"/> for reads and <see cref="ExecuteAsync"/> for writes.
/// </para>
///
/// <para>
/// <b>Async + deadline-bound.</b> Every call uses the async ADO.NET path
/// end-to-end (no <c>.GetAwaiter().GetResult()</c> blocking a pool thread) and honours the
/// executing method's deadline token on the command itself, with a <see cref="DbCommand.CommandTimeout"/>
/// backstop derived from the method timeout — so a slow query is bounded by the method
/// timeout instead of running unbounded.
/// </para>
/// </summary>
public sealed class InboundDatabaseHelper
{
    private readonly IDatabaseGateway? _gateway;
    private readonly CancellationToken _ct;
    private readonly int _commandTimeoutSeconds;

    /// <summary>
    /// Initializes the helper.
    /// </summary>
    /// <param name="gateway">The central database gateway, or null when no gateway is registered (the helper then throws on first use).</param>
    /// <param name="ct">The executing method's deadline token; forwarded to every async DB call so a slow query is bounded by the method timeout.</param>
    /// <param name="commandTimeout">The method timeout; used to derive a <see cref="DbCommand.CommandTimeout"/> backstop. <see cref="TimeSpan.Zero"/> (the default) leaves the provider default.</param>
    public InboundDatabaseHelper(IDatabaseGateway? gateway, CancellationToken ct, TimeSpan commandTimeout = default)
    {
        _gateway = gateway;
        _ct = ct;
        _commandTimeoutSeconds = commandTimeout > TimeSpan.Zero
            ? (int)Math.Ceiling(commandTimeout.TotalSeconds)
            : 0;
    }

    /// <summary>First column of the first row converted to <typeparamref name="T"/> (default if no rows).</summary>
    /// <typeparam name="T">The type to convert the scalar result to.</typeparam>
    /// <param name="connectionName">Name of a connection configured on the central database gateway.</param>
    /// <param name="sql">The SQL statement. Values MUST be supplied via <paramref name="parameters"/>, not concatenated.</param>
    /// <param name="parameters">Optional anonymous object whose properties become bound <c>@</c>-prefixed parameters.</param>
    /// <returns>The converted scalar, or <c>default</c> when there are no rows / a NULL value.</returns>
    public async Task<T?> QuerySingleAsync<T>(string connectionName, string sql, object? parameters = null)
    {
        if (_gateway is null) throw new InvalidOperationException("Database is not available for this inbound method");
        await using var conn = await _gateway.GetConnectionAsync(connectionName, _ct);
        await using var cmd = CreateCommand(conn, sql, parameters);
        var result = await cmd.ExecuteScalarAsync(_ct);
        if (result is null or DBNull) return default;
        if (result is T t) return t;
        return (T)Convert.ChangeType(result, typeof(T), CultureInfo.InvariantCulture);
    }

    /// <summary>All rows as column→value dictionaries (case-insensitive keys).</summary>
    /// <param name="connectionName">Name of a connection configured on the central database gateway.</param>
    /// <param name="sql">The SQL statement. Values MUST be supplied via <paramref name="parameters"/>, not concatenated.</param>
    /// <param name="parameters">Optional anonymous object whose properties become bound <c>@</c>-prefixed parameters.</param>
    /// <returns>The result rows; empty when nothing matches.</returns>
    public async Task<IReadOnlyList<IReadOnlyDictionary<string, object?>>> QueryAsync(
        string connectionName, string sql, object? parameters = null)
    {
        if (_gateway is null) throw new InvalidOperationException("Database is not available for this inbound method");
        await using var conn = await _gateway.GetConnectionAsync(connectionName, _ct);
        await using var cmd = CreateCommand(conn, sql, parameters);
        await using var reader = await cmd.ExecuteReaderAsync(_ct);
        var rows = new List<IReadOnlyDictionary<string, object?>>();
        while (await reader.ReadAsync(_ct))
        {
            var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
            for (var i = 0; i < reader.FieldCount; i++)
            {
                var v = reader.GetValue(i);
                row[reader.GetName(i)] = v is DBNull ? null : v;
            }
            rows.Add(row);
        }
        return rows;
    }

    /// <summary>
    /// Executes a write statement (INSERT/UPDATE/DELETE/DDL) and returns the number of
    /// rows affected. Writes are authorized for inbound API scripts;
    /// values are still bound as parameters, never concatenated.
    /// </summary>
    /// <param name="connectionName">Name of a connection configured on the central database gateway.</param>
    /// <param name="sql">The SQL statement. Values MUST be supplied via <paramref name="parameters"/>, not concatenated.</param>
    /// <param name="parameters">Optional anonymous object whose properties become bound <c>@</c>-prefixed parameters.</param>
    /// <returns>The number of rows affected.</returns>
    public async Task<int> ExecuteAsync(string connectionName, string sql, object? parameters = null)
    {
        if (_gateway is null) throw new InvalidOperationException("Database is not available for this inbound method");
        await using var conn = await _gateway.GetConnectionAsync(connectionName, _ct);
        await using var cmd = CreateCommand(conn, sql, parameters);
        return await cmd.ExecuteNonQueryAsync(_ct);
    }

    private DbCommand CreateCommand(DbConnection conn, string sql, object? parameters)
    {
        var cmd = conn.CreateCommand();
        cmd.CommandText = sql;
        // A CommandTimeout backstop derived from the method timeout so a
        // slow query cannot outrun the method deadline even if the provider does not
        // honour token cancellation mid-statement.
        if (_commandTimeoutSeconds > 0) cmd.CommandTimeout = _commandTimeoutSeconds;
        AddParameters(cmd, parameters);
        return cmd;
    }

    private static void AddParameters(DbCommand cmd, object? parameters)
    {
        if (parameters is null) return;
        foreach (var prop in parameters.GetType().GetProperties())
        {
            var p = cmd.CreateParameter();
            p.ParameterName = "@" + prop.Name;
            p.Value = prop.GetValue(parameters) ?? DBNull.Value;
            cmd.Parameters.Add(p);
        }
    }
}
