#nullable enable

using System.Data;
using System.Data.Common;
using System.Text.RegularExpressions;
using MMoneyCoderSharp.Data;
using Oracle.ManagedDataAccess.Client;

namespace MBN_STOCK_WEBVIEW.Infrastructure;

/// <summary>
/// Executes the closed GraphE INPUT_* write contract on one Oracle connection
/// and one transaction. Each operation takes the profile's exclusive table lock,
/// executes its bound command once, validates affected rows before commit, and
/// deliberately contains no retry loop.
/// </summary>
public sealed partial class OracleManualFinancialMutationExecutor
    : IManualFinancialMutationExecutor
{
    private readonly IDatabaseConnectionFactory _connectionFactory;
    private readonly ITransientDatabaseErrorDetector _errorDetector;
    private readonly IDatabaseMutationAuthorization _mutationAuthorization;
    private readonly TimeSpan _operationTimeout;
    private readonly int _commandTimeoutSeconds;
    private readonly Action<DbCommand> _configureCommand;

    public OracleManualFinancialMutationExecutor(
        IDatabaseConnectionFactory connectionFactory,
        DatabaseResilienceOptions resilienceOptions,
        ITransientDatabaseErrorDetector? errorDetector = null)
        : this(
            connectionFactory,
            resilienceOptions,
            errorDetector ?? new TransientDatabaseErrorDetector(),
            ConfigureOracleCommand,
            DatabaseMutationAuthorization.AllowAll)
    {
    }

    public OracleManualFinancialMutationExecutor(
        IDatabaseConnectionFactory connectionFactory,
        DatabaseResilienceOptions resilienceOptions,
        ITransientDatabaseErrorDetector? errorDetector,
        IDatabaseMutationAuthorization mutationAuthorization)
        : this(
            connectionFactory,
            resilienceOptions,
            errorDetector ?? new TransientDatabaseErrorDetector(),
            ConfigureOracleCommand,
            mutationAuthorization)
    {
    }

    internal OracleManualFinancialMutationExecutor(
        IDatabaseConnectionFactory connectionFactory,
        DatabaseResilienceOptions resilienceOptions,
        ITransientDatabaseErrorDetector errorDetector,
        Action<DbCommand> configureCommand)
        : this(
            connectionFactory,
            resilienceOptions,
            errorDetector,
            configureCommand,
            DatabaseMutationAuthorization.AllowAll)
    {
    }

    internal OracleManualFinancialMutationExecutor(
        IDatabaseConnectionFactory connectionFactory,
        DatabaseResilienceOptions resilienceOptions,
        ITransientDatabaseErrorDetector errorDetector,
        Action<DbCommand> configureCommand,
        IDatabaseMutationAuthorization mutationAuthorization)
    {
        _connectionFactory = connectionFactory ??
            throw new ArgumentNullException(nameof(connectionFactory));
        ArgumentNullException.ThrowIfNull(resilienceOptions);
        _errorDetector = errorDetector ?? throw new ArgumentNullException(nameof(errorDetector));
        _configureCommand = configureCommand ?? throw new ArgumentNullException(nameof(configureCommand));
        _mutationAuthorization = mutationAuthorization ??
            throw new ArgumentNullException(nameof(mutationAuthorization));

        var validationOptions = new DatabaseOptions { Resilience = resilienceOptions };
        validationOptions.ValidateRuntimeSettings();
        _operationTimeout = TimeSpan.FromSeconds(resilienceOptions.OperationTimeoutSeconds);
        _commandTimeoutSeconds = connectionFactory.GetCommandTimeoutSeconds(DataSourceKind.Oracle);
        if (_commandTimeoutSeconds is < 1 or > 600)
        {
            throw new DatabaseConfigurationException(
                "The Oracle manual-financial command timeout must be between 1 and 600 seconds.");
        }
    }

    public async Task<ManualFinancialDbTransactionResult> ExecuteTransactionAsync(
        DataSourceKind source,
        ManualFinancialDbTransaction transaction,
        CancellationToken cancellationToken = default)
    {
        if (source != DataSourceKind.Oracle)
        {
            throw new ArgumentOutOfRangeException(
                nameof(source),
                source,
                "Manual-financial GraphE records are stored in Oracle.");
        }

        ValidateTransaction(transaction);
        cancellationToken.ThrowIfCancellationRequested();
        try
        {
            DatabaseMutationAuthorizationGuard.Demand(
                _mutationAuthorization,
                new DatabaseMutationAuthorizationRequest(
                    DatabaseMutationTarget.ManualFinancial,
                    DataSourceKind.Oracle,
                    transaction.OperationId,
                    transaction.Kind.ToString()));
        }
        catch (DatabaseMutationAuthorizationException exception)
        {
            throw new ManualFinancialMutationException(
                $"The {transaction.Kind} transaction was denied before it started and did not commit.",
                outcomeUnknown: false,
                exception);
        }
        using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutSource.CancelAfter(_operationTimeout);
        var operationToken = timeoutSource.Token;

        DbConnection? connection = null;
        DbTransaction? databaseTransaction = null;
        var commitStarted = false;
        try
        {
            connection = _connectionFactory.Create(DataSourceKind.Oracle);
            await connection.OpenAsync(operationToken).ConfigureAwait(false);
            databaseTransaction = await connection.BeginTransactionAsync(
                    IsolationLevel.ReadCommitted,
                    operationToken)
                .ConfigureAwait(false);

            await ExecuteLockAsync(
                connection,
                databaseTransaction,
                transaction.ExclusiveTableLockSql,
                operationToken).ConfigureAwait(false);

            var affectedRows = new List<int>(transaction.Commands.Count);
            foreach (var commandSpec in transaction.Commands)
            {
                operationToken.ThrowIfCancellationRequested();
                var count = await ExecuteCommandAsync(
                    connection,
                    databaseTransaction,
                    commandSpec,
                    operationToken).ConfigureAwait(false);
                if (!Matches(commandSpec.AffectedRowsRule, count))
                {
                    throw Rejected(transaction.Kind, count);
                }

                affectedRows.Add(count);
            }

            commitStarted = true;
            await databaseTransaction.CommitAsync(operationToken).ConfigureAwait(false);
            return new ManualFinancialDbTransactionResult(
                transaction.OperationId,
                Array.AsReadOnly(affectedRows.ToArray()),
                DateTimeOffset.UtcNow);
        }
        catch (Exception exception)
        {
            if (commitStarted)
            {
                // The client cannot prove whether Oracle committed. Issuing a
                // rollback or repeating the command would make the situation worse.
                throw UnknownOutcome(transaction.Kind, exception);
            }

            var isCancellationOrTimeout = exception is OperationCanceledException ||
                _errorDetector.IsTimeout(DataSourceKind.Oracle, exception);
            var rollbackFailed = false;
            if (databaseTransaction is not null)
            {
                rollbackFailed = !await TryRollbackAsync(databaseTransaction)
                    .ConfigureAwait(false);
            }

            if (databaseTransaction is not null && (isCancellationOrTimeout || rollbackFailed))
            {
                throw UnknownOutcome(transaction.Kind, exception);
            }

            if (exception is ManualFinancialMutationRejectedException rejected)
            {
                throw rejected;
            }

            if (exception is ManualFinancialMutationException mutationException)
            {
                throw mutationException;
            }

            throw new ManualFinancialMutationException(
                $"The {transaction.Kind} Oracle transaction did not commit.",
                outcomeUnknown: false,
                exception);
        }
        finally
        {
            if (databaseTransaction is not null)
            {
                await databaseTransaction.DisposeAsync().ConfigureAwait(false);
            }

            if (connection is not null)
            {
                await connection.DisposeAsync().ConfigureAwait(false);
            }
        }
    }

    internal static void ConfigureOracleCommand(DbCommand command)
    {
        ArgumentNullException.ThrowIfNull(command);
        if (command is not OracleCommand oracleCommand)
        {
            throw new DatabaseConfigurationException(
                "The manual-financial mutation connection did not create an Oracle command.");
        }

        oracleCommand.BindByName = true;
    }

    private async Task ExecuteLockAsync(
        DbConnection connection,
        DbTransaction transaction,
        string sql,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        Configure(command, transaction, sql);
        // Execute exactly once. LOCK TABLE has no operator value and no retry.
        _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }

    private async Task<int> ExecuteCommandAsync(
        DbConnection connection,
        DbTransaction transaction,
        ManualFinancialDbCommand commandSpec,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        Configure(command, transaction, commandSpec.Sql);
        foreach (var parameterSpec in commandSpec.Parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = parameterSpec.Name;
            parameter.Direction = ParameterDirection.Input;
            parameter.DbType = parameterSpec.DbType;
            parameter.Value = parameterSpec.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }

        // Execute exactly once. There is deliberately no transient retry.
        return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }

    private void Configure(DbCommand command, DbTransaction transaction, string sql)
    {
        command.CommandText = sql;
        command.CommandType = CommandType.Text;
        command.CommandTimeout = _commandTimeoutSeconds;
        command.Transaction = transaction;
        _configureCommand(command);
    }

    private async Task<bool> TryRollbackAsync(DbTransaction transaction)
    {
        try
        {
            using var rollbackTimeout = new CancellationTokenSource(
                TimeSpan.FromSeconds(_commandTimeoutSeconds));
            await transaction.RollbackAsync(rollbackTimeout.Token).ConfigureAwait(false);
            return true;
        }
        catch
        {
            return false;
        }
    }

    private static void ValidateTransaction(ManualFinancialDbTransaction transaction)
    {
        ArgumentNullException.ThrowIfNull(transaction);
        var expected = transaction.Screen switch
        {
            ManualFinancialScreenKind.RevenueComposition =>
                (Table: "INPUT_PIE", Lock: "LOCK TABLE INPUT_PIE IN EXCLUSIVE MODE"),
            ManualFinancialScreenKind.GrowthMetrics =>
                (Table: "INPUT_GROW", Lock: "LOCK TABLE INPUT_GROW IN EXCLUSIVE MODE"),
            ManualFinancialScreenKind.Sales =>
                (Table: "INPUT_SELL", Lock: "LOCK TABLE INPUT_SELL IN EXCLUSIVE MODE"),
            ManualFinancialScreenKind.OperatingProfit =>
                (Table: "INPUT_PROFIT", Lock: "LOCK TABLE INPUT_PROFIT IN EXCLUSIVE MODE"),
            _ => throw InvalidTransaction()
        };
        if (transaction.OperationId == Guid.Empty ||
            !transaction.RequiresExclusiveTableLock ||
            !string.Equals(transaction.TableName, expected.Table, StringComparison.Ordinal) ||
            !string.Equals(transaction.ExclusiveTableLockSql, expected.Lock, StringComparison.Ordinal) ||
            transaction.Commands.Count != 1 || transaction.Commands[0] is null)
        {
            throw InvalidTransaction();
        }

        var command = transaction.Commands[0];
        var validShape = transaction.Kind switch
        {
            ManualFinancialMutationKind.Create =>
                command.Kind == ManualFinancialDbCommandKind.Insert &&
                command.AffectedRowsRule == ManualFinancialAffectedRowsRule.ExactlyOne,
            ManualFinancialMutationKind.Update =>
                command.Kind == ManualFinancialDbCommandKind.Update &&
                command.AffectedRowsRule == ManualFinancialAffectedRowsRule.ExactlyOne,
            ManualFinancialMutationKind.DeleteOne =>
                command.Kind == ManualFinancialDbCommandKind.DeleteOne &&
                command.AffectedRowsRule == ManualFinancialAffectedRowsRule.ExactlyOne,
            ManualFinancialMutationKind.DeleteAll =>
                command.Kind == ManualFinancialDbCommandKind.DeleteAll &&
                command.AffectedRowsRule == ManualFinancialAffectedRowsRule.AnyNonNegative,
            _ => false
        };
        if (!validShape || string.IsNullOrWhiteSpace(command.Sql) || command.Sql.Contains(';'))
        {
            throw InvalidTransaction();
        }

        var placeholders = PlaceholderPattern()
            .Matches(command.Sql)
            .Select(static match => match.Groups[1].Value)
            .ToArray();
        var parameters = command.Parameters.ToArray();
        if (parameters.Any(static parameter =>
                parameter is null ||
                parameter.DbType != DbType.String ||
                parameter.Value is not string) ||
            parameters.Select(static parameter => parameter.Name)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .Count() != parameters.Length ||
            !placeholders.SequenceEqual(
                parameters.Select(static parameter => parameter.Name),
                StringComparer.OrdinalIgnoreCase))
        {
            throw InvalidTransaction();
        }
    }

    private static bool Matches(ManualFinancialAffectedRowsRule rule, int count) =>
        rule switch
        {
            ManualFinancialAffectedRowsRule.ExactlyOne => count == 1,
            ManualFinancialAffectedRowsRule.AnyNonNegative => count >= 0,
            _ => false
        };

    private static ManualFinancialMutationRejectedException Rejected(
        ManualFinancialMutationKind kind,
        int affectedRows)
    {
        var reason = affectedRows is < 0 or > 1
            ? ManualFinancialMutationRejectionReason.AmbiguousIdentity
            : kind == ManualFinancialMutationKind.Create
                ? ManualFinancialMutationRejectionReason.DuplicateIdentity
                : ManualFinancialMutationRejectionReason.NotFoundOrChanged;
        return new ManualFinancialMutationRejectedException(
            reason,
            $"The {kind} precondition failed and the Oracle transaction was rolled back.");
    }

    private static ManualFinancialMutationException UnknownOutcome(
        ManualFinancialMutationKind kind,
        Exception exception) =>
        new(
            $"The {kind} Oracle transaction outcome is unknown; do not retry automatically.",
            outcomeUnknown: true,
            exception);

    private static ArgumentException InvalidTransaction() =>
        new("The manual-financial transaction is invalid.", "transaction");

    [GeneratedRegex(@":([A-Za-z_][A-Za-z0-9_]*)", RegexOptions.CultureInvariant)]
    private static partial Regex PlaceholderPattern();
}
