using System.Data;
using System.Data.Common;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using AuditEvent = ZB.MOM.WW.Audit.AuditEvent;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

/// <summary>
/// <see cref="DbCommand"/> decorator that emits
/// exactly one <c>DbOutbound</c>/<c>DbWrite</c> audit event per execution.
/// </summary>
/// <remarks>
/// <para>
/// <b>Vocabulary lock:</b> both writes (Execute / ExecuteScalar) and
/// reads (ExecuteReader) emit <see cref="AuditKind.DbWrite"/> on the
/// <see cref="AuditChannel.DbOutbound"/> channel. The <c>Extra</c> JSON column
/// distinguishes them — <c>{"op":"write","rowsAffected":N}</c> for writes,
/// <c>{"op":"read","rowsReturned":N}</c> for reads.
/// </para>
/// <para>
/// <b>Best-effort emission (alog.md §7):</b> mirrors
/// <see cref="ScriptRuntimeContext.ExternalSystemHelper"/>'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 <see cref="IAuditWriter"/> never
/// aborts the SQL call.
/// </para>
/// </remarks>
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;

    /// <summary>
    /// ParentExecutionId: the spawning execution's id when this
    /// run was inbound-API-routed; <c>null</c> for non-routed runs. Threaded
    /// alongside <see cref="_executionId"/> and stamped onto the <c>DbWrite</c>
    /// audit row.
    /// </summary>
    private readonly Guid? _parentExecutionId;

    private readonly ILogger _logger;
    private DbConnection? _wrappingConnection;

    /// <summary>
    /// Initializes a new auditing database command decorator.
    /// </summary>
    /// <param name="inner">The inner database command to wrap and audit.</param>
    /// <param name="auditWriter">Writer for audit log entries.</param>
    /// <param name="connectionName">Name of the database connection being used.</param>
    /// <param name="siteId">Identifier of the site executing the command.</param>
    /// <param name="instanceName">Unique name of the instance executing the command.</param>
    /// <param name="sourceScript">Optional name of the source script for audit trail.</param>
    /// <param name="logger">Logger for diagnostics and warnings.</param>
    /// <param name="executionId">Unique identifier for this script execution.</param>
    /// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
    // Parameter ordering: executionId sits immediately after the ILogger,
    // consistent with the other three audit-threaded ctors (ExternalSystemHelper,
    // DatabaseHelper, AuditingDbConnection). parentExecutionId is a trailing
    // optional param so existing positional callers stay source-compatible.
    public AuditingDbCommand(
        DbCommand inner,
        IAuditWriter auditWriter,
        string connectionName,
        string siteId,
        string instanceName,
        string? sourceScript,
        ILogger logger,
        Guid executionId,
        Guid? parentExecutionId = null)
    {
        _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;
        _parentExecutionId = parentExecutionId;
    }

    // -- Forwarded surface ------------------------------------------------

#pragma warning disable CS8765 // ADO.NET base members carry pre-NRT signatures with permissive nullability
    /// <inheritdoc />
    public override string CommandText
    {
        get => _inner.CommandText;
        set => _inner.CommandText = value;
    }
#pragma warning restore CS8765

    /// <inheritdoc />
    public override int CommandTimeout
    {
        get => _inner.CommandTimeout;
        set => _inner.CommandTimeout = value;
    }

    /// <inheritdoc />
    public override CommandType CommandType
    {
        get => _inner.CommandType;
        set => _inner.CommandType = value;
    }

    /// <inheritdoc />
    public override bool DesignTimeVisible
    {
        get => _inner.DesignTimeVisible;
        set => _inner.DesignTimeVisible = value;
    }

    /// <inheritdoc />
    public override UpdateRowSource UpdatedRowSource
    {
        get => _inner.UpdatedRowSource;
        set => _inner.UpdatedRowSource = value;
    }

    /// <inheritdoc />
    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;
        // Unwrap the AuditingDbConnection wrapper via its
        // own internal Inner accessor instead of reflecting into a private
        // _inner field. Reflection was the original anti-
        // pattern (and is forbidden inside script bodies by the trust
        // model) — both classes are internal sealed in the same assembly,
        // so the proper API surface is available without leaking anything
        // public.
        set
        {
            _wrappingConnection = value;
            _inner.Connection = value switch
            {
                AuditingDbConnection auditing => auditing.Inner,
                _ => value,
            };
        }
    }

    /// <inheritdoc />
    protected override DbParameterCollection DbParameterCollection => _inner.Parameters;

    /// <inheritdoc />
    protected override DbTransaction? DbTransaction
    {
        get => _inner.Transaction;
        set => _inner.Transaction = value;
    }

    /// <inheritdoc />
    public override void Cancel() => _inner.Cancel();

    /// <inheritdoc />
    public override void Prepare() => _inner.Prepare();

    /// <inheritdoc />
    protected override DbParameter CreateDbParameter() => _inner.CreateParameter();

    // -- Audited execution surface ---------------------------------------

    /// <inheritdoc />
    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);
        }
    }

    /// <inheritdoc />
    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 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);
        }
    }

    /// <inheritdoc />
    public override async Task<int> 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);
        }
    }

    /// <inheritdoc />
    public override async Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken)
    {
        var occurredAtUtc = DateTime.UtcNow;
        var startTicks = Stopwatch.GetTimestamp();
        object? scalar = null;
        Exception? thrown = null;
        try
        {
            scalar = await _inner.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
            return scalar;
        }
        catch (Exception ex)
        {
            thrown = ex;
            throw;
        }
        finally
        {
            EmitAudit(
                occurredAtUtc,
                ElapsedMs(startTicks),
                op: "write",
                rowsAffected: thrown == null ? -1 : (int?)null,
                rowsReturned: null,
                thrown);
        }
    }

    /// <inheritdoc />
    protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
    {
        var occurredAtUtc = DateTime.UtcNow;
        var startTicks = Stopwatch.GetTimestamp();
        DbDataReader? reader = null;
        Exception? thrown = null;
        try
        {
            reader = _inner.ExecuteReader(behavior);
            return new AuditingDbDataReader(
                reader,
                onClose: rows => EmitAudit(
                    occurredAtUtc,
                    ElapsedMs(startTicks),
                    op: "read",
                    rowsAffected: null,
                    rowsReturned: rows,
                    thrown: null));
        }
        catch (Exception ex)
        {
            thrown = ex;
            // Emit the failure row immediately — no reader to wait on.
            EmitAudit(
                occurredAtUtc,
                ElapsedMs(startTicks),
                op: "read",
                rowsAffected: null,
                rowsReturned: null,
                thrown);
            throw;
        }
    }

    /// <inheritdoc />
    protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(
        CommandBehavior behavior, CancellationToken cancellationToken)
    {
        var occurredAtUtc = DateTime.UtcNow;
        var startTicks = Stopwatch.GetTimestamp();
        DbDataReader? reader = null;
        Exception? thrown = null;
        try
        {
            reader = await _inner.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false);
            return new AuditingDbDataReader(
                reader,
                onClose: rows => EmitAudit(
                    occurredAtUtc,
                    ElapsedMs(startTicks),
                    op: "read",
                    rowsAffected: null,
                    rowsReturned: rows,
                    thrown: null));
        }
        catch (Exception ex)
        {
            thrown = ex;
            EmitAudit(
                occurredAtUtc,
                ElapsedMs(startTicks),
                op: "read",
                rowsAffected: null,
                rowsReturned: null,
                thrown);
            throw;
        }
    }

    /// <inheritdoc />
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _inner.Dispose();
        }
        base.Dispose(disposing);
    }

    // -- Emission ---------------------------------------------------------

    private static int ElapsedMs(long startTicks) =>
        (int)((Stopwatch.GetTimestamp() - startTicks) * 1000d / Stopwatch.Frequency);

    /// <summary>
    /// Best-effort emission of one <c>DbOutbound</c>/<c>DbWrite</c> audit row.
    /// Mirrors the <c>EmitCallAudit</c> 3-layer fail-safe pattern.
    /// </summary>
    private void EmitAudit(
        DateTime occurredAtUtc,
        int durationMs,
        string op,
        int? rowsAffected,
        int? rowsReturned,
        Exception? thrown)
    {
        AuditEvent evt;
        try
        {
            evt = BuildAuditEvent(occurredAtUtc, durationMs, op, rowsAffected, rowsReturned, thrown);
        }
        catch (Exception buildEx)
        {
            // Defensive: building the event from already-validated fields
            // shouldn't throw, but the alog.md §7 contract requires we never
            // propagate to the user-facing action regardless.
            _logger.LogWarning(buildEx,
                "Failed to build Audit Log #23 event for {Connection} (op={Op}) — skipping emission",
                _connectionName, op);
            return;
        }

        try
        {
            var writeTask = _auditWriter.WriteAsync(evt, CancellationToken.None);
            if (!writeTask.IsCompleted)
            {
                writeTask.ContinueWith(
                    t => _logger.LogWarning(t.Exception,
                        "Audit Log #23 write failed for EventId {EventId} ({Connection} op={Op})",
                        evt.EventId, _connectionName, op),
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
                    TaskScheduler.Default);
            }
            else if (writeTask.IsFaulted)
            {
                _logger.LogWarning(writeTask.Exception,
                    "Audit Log #23 write failed for EventId {EventId} ({Connection} op={Op})",
                    evt.EventId, _connectionName, op);
            }
        }
        catch (Exception writeEx)
        {
            // Synchronous throw from WriteAsync before its own try/catch.
            // Swallow + log per alog.md §7.
            _logger.LogWarning(writeEx,
                "Audit Log #23 write threw synchronously for EventId {EventId} ({Connection} op={Op})",
                evt.EventId, _connectionName, op);
        }
    }

    private AuditEvent BuildAuditEvent(
        DateTime occurredAtUtc,
        int durationMs,
        string op,
        int? rowsAffected,
        int? rowsReturned,
        Exception? thrown)
    {
        var status = thrown == null ? AuditStatus.Delivered : AuditStatus.Failed;

        // Target = "<connectionName>.<first 60 chars of SQL>" so the audit
        // row carries a human-recognisable handle without dragging the full
        // (potentially huge) statement into the index column. The full
        // statement + parameter values live in RequestSummary.
        string target = _connectionName;
        var sqlSnippet = _inner.CommandText ?? string.Empty;
        if (sqlSnippet.Length > 0)
        {
            var snippet = sqlSnippet.Length > 60
                ? sqlSnippet[..60]
                : sqlSnippet;
            target = $"{_connectionName}.{snippet}";
        }

        // RequestSummary captures the SQL statement + parameter values by
        // default per the alog.md acceptance criteria. Per-target SQL
        // parameter redaction is configured via AuditLogOptions.PerTargetOverrides
        // (RedactSqlParamsMatching) and applied at write time by the redactor.
        string? requestSummary = BuildRequestSummary();

        // Extra carries the op discriminator + row count per the vocabulary
        // lock. Build as a small hand-rolled JSON object to avoid pulling
        // in System.Text.Json on the hot path.
        string extra = op == "write"
            ? $"{{\"op\":\"write\",\"rowsAffected\":{(rowsAffected ?? 0)}}}"
            : $"{{\"op\":\"read\",\"rowsReturned\":{(rowsReturned ?? 0)}}}";

        return ScadaBridgeAuditEventFactory.Create(
            channel: AuditChannel.DbOutbound,
            kind: AuditKind.DbWrite,
            status: status,
            occurredAtUtc: DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
            // Outbound channel: per the Audit Log Actor-column spec the actor is
            // the calling script. Null when no single script owns the call
            // (e.g. a shared script running inline).
            actor: _sourceScript,
            target: target,
            // A sync one-shot DB write has no operation
            // lifecycle, so CorrelationId is null. ExecutionId carries the
            // per-execution id so this row shares an id with the other sync
            // trust-boundary rows from the same script run.
            correlationId: null,
            executionId: _executionId,
            // ParentExecutionId: the spawning execution's id;
            // null for non-routed runs.
            parentExecutionId: _parentExecutionId,
            sourceSiteId: string.IsNullOrEmpty(_siteId) ? null : _siteId,
            sourceInstanceId: _instanceName,
            sourceScript: _sourceScript,
            httpStatus: null,
            durationMs: durationMs,
            errorMessage: thrown?.Message,
            errorDetail: thrown?.ToString(),
            requestSummary: requestSummary,
            responseSummary: null,
            payloadTruncated: false,
            extra: extra);
    }

    /// <summary>
    /// Compose a JSON request summary capturing the SQL statement and
    /// parameter values. Parameter values are captured by default per the
    /// acceptance criteria; per-target SQL-parameter redaction is applied
    /// at write time via <c>AuditLogOptions.PerTargetOverrides.RedactSqlParamsMatching</c>.
    /// </summary>
    private string? BuildRequestSummary()
    {
        var sql = _inner.CommandText;
        if (string.IsNullOrEmpty(sql))
        {
            return null;
        }

        // Hand-roll the JSON so we don't pull in System.Text.Json for a
        // shape this small. Values are stringified with ToString().
        var sb = new System.Text.StringBuilder(sql.Length + 64);
        sb.Append("{\"sql\":");
        AppendJsonString(sb, sql);

        if (_inner.Parameters.Count > 0)
        {
            sb.Append(",\"parameters\":{");
            var first = true;
            foreach (DbParameter p in _inner.Parameters)
            {
                if (!first) sb.Append(',');
                first = false;
                AppendJsonString(sb, p.ParameterName);
                sb.Append(':');
                if (p.Value is null || p.Value is DBNull)
                {
                    sb.Append("null");
                }
                else
                {
                    AppendJsonString(sb, p.Value.ToString() ?? string.Empty);
                }
            }
            sb.Append('}');
        }

        sb.Append('}');
        return sb.ToString();
    }

    private static void AppendJsonString(System.Text.StringBuilder sb, string value)
    {
        sb.Append('"');
        foreach (var ch in value)
        {
            switch (ch)
            {
                case '"': sb.Append("\\\""); break;
                case '\\': sb.Append("\\\\"); break;
                case '\b': sb.Append("\\b"); break;
                case '\f': sb.Append("\\f"); break;
                case '\n': sb.Append("\\n"); break;
                case '\r': sb.Append("\\r"); break;
                case '\t': sb.Append("\\t"); break;
                default:
                    if (ch < 0x20)
                    {
                        sb.Append("\\u").Append(((int)ch).ToString("x4"));
                    }
                    else
                    {
                        sb.Append(ch);
                    }
                    break;
            }
        }
        sb.Append('"');
    }
}
