using System.Data;
using System.Data.Common;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Types.Enums;
namespace ScadaLink.SiteRuntime.Scripts;
///
/// Audit Log #23 — M4 Bundle A: decorator that emits
/// exactly one DbOutbound/DbWrite audit event per execution.
///
///
///
/// Vocabulary lock (M4 plan): both writes (Execute / ExecuteScalar) and
/// reads (ExecuteReader) emit on the
/// channel. The Extra JSON column
/// distinguishes them — {"op":"write","rowsAffected":N} for writes,
/// {"op":"read","rowsReturned":N} for reads.
///
///
/// Best-effort emission (alog.md §7): mirrors
/// 's 3-layer fail-safe.
/// The original ADO.NET result (or original exception) flows back to the
/// script untouched; audit-build, audit-write, and audit-continuation faults
/// are all logged + swallowed. A faulted never
/// aborts the SQL call.
///
///
internal sealed class AuditingDbCommand : DbCommand
{
private readonly DbCommand _inner;
private readonly IAuditWriter _auditWriter;
private readonly string _connectionName;
private readonly string _siteId;
private readonly string _instanceName;
private readonly string? _sourceScript;
private readonly Guid _executionId;
private readonly ILogger _logger;
private DbConnection? _wrappingConnection;
// Parameter ordering: executionId sits immediately after the ILogger,
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
// DatabaseHelper, AuditingDbConnection).
public AuditingDbCommand(
DbCommand inner,
IAuditWriter auditWriter,
string connectionName,
string siteId,
string instanceName,
string? sourceScript,
ILogger logger,
Guid executionId)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
_connectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName));
_siteId = siteId ?? string.Empty;
_instanceName = instanceName ?? string.Empty;
_sourceScript = sourceScript;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_executionId = executionId;
}
// -- Forwarded surface ------------------------------------------------
#pragma warning disable CS8765 // ADO.NET base members carry pre-NRT signatures with permissive nullability
public override string CommandText
{
get => _inner.CommandText;
set => _inner.CommandText = value;
}
#pragma warning restore CS8765
public override int CommandTimeout
{
get => _inner.CommandTimeout;
set => _inner.CommandTimeout = value;
}
public override CommandType CommandType
{
get => _inner.CommandType;
set => _inner.CommandType = value;
}
public override bool DesignTimeVisible
{
get => _inner.DesignTimeVisible;
set => _inner.DesignTimeVisible = value;
}
public override UpdateRowSource UpdatedRowSource
{
get => _inner.UpdatedRowSource;
set => _inner.UpdatedRowSource = value;
}
protected override DbConnection? DbConnection
{
// When the script has wrapped the connection (the normal path through
// ScriptRuntimeContext.DatabaseHelper.Connection) we keep returning
// the wrapper, but writes from the user go through to the inner
// command so the underlying provider keeps its wiring intact.
get => _wrappingConnection ?? _inner.Connection;
set
{
_wrappingConnection = value;
_inner.Connection = value switch
{
AuditingDbConnection auditing => auditing.GetType()
.GetField("_inner", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
!.GetValue(auditing) as DbConnection,
_ => value
};
}
}
protected override DbParameterCollection DbParameterCollection => _inner.Parameters;
protected override DbTransaction? DbTransaction
{
get => _inner.Transaction;
set => _inner.Transaction = value;
}
public override void Cancel() => _inner.Cancel();
public override void Prepare() => _inner.Prepare();
protected override DbParameter CreateDbParameter() => _inner.CreateParameter();
// -- Audited execution surface ---------------------------------------
public override int ExecuteNonQuery()
{
var occurredAtUtc = DateTime.UtcNow;
var startTicks = Stopwatch.GetTimestamp();
int rows = 0;
Exception? thrown = null;
try
{
rows = _inner.ExecuteNonQuery();
return rows;
}
catch (Exception ex)
{
thrown = ex;
throw;
}
finally
{
EmitAudit(
occurredAtUtc,
ElapsedMs(startTicks),
op: "write",
rowsAffected: thrown == null ? rows : (int?)null,
rowsReturned: null,
thrown);
}
}
public override object? ExecuteScalar()
{
var occurredAtUtc = DateTime.UtcNow;
var startTicks = Stopwatch.GetTimestamp();
object? scalar = null;
Exception? thrown = null;
try
{
scalar = _inner.ExecuteScalar();
return scalar;
}
catch (Exception ex)
{
thrown = ex;
throw;
}
finally
{
// ExecuteScalar is classified as "write" per the M4 vocabulary
// lock — it's a single-value execution; rowsAffected mirrors the
// inner command's value if exposed (DbCommand has no RecordsAffected
// property, so we report -1 when the provider didn't surface it).
EmitAudit(
occurredAtUtc,
ElapsedMs(startTicks),
op: "write",
rowsAffected: thrown == null ? -1 : (int?)null,
rowsReturned: null,
thrown);
}
}
public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken)
{
var occurredAtUtc = DateTime.UtcNow;
var startTicks = Stopwatch.GetTimestamp();
int rows = 0;
Exception? thrown = null;
try
{
rows = await _inner.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
return rows;
}
catch (Exception ex)
{
thrown = ex;
throw;
}
finally
{
EmitAudit(
occurredAtUtc,
ElapsedMs(startTicks),
op: "write",
rowsAffected: thrown == null ? rows : (int?)null,
rowsReturned: null,
thrown);
}
}
public override async Task