# frozen_string_literal: true

require "active_record"

require "after_commit_everywhere/version"
require "after_commit_everywhere/wrap"

# Module allowing to use ActiveRecord transactional callbacks outside of
# ActiveRecord models, literally everywhere in your application.
#
# Include it to your classes (e.g. your base service object class or whatever)
module AfterCommitEverywhere
  class NotInTransaction < RuntimeError; end

  # Runs +callback+ after successful commit of outermost transaction for
  # database +connection+.
  #
  # If called outside transaction it will execute callback immediately.
  #
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
  # @param callback   [#call] Callback to be executed
  # @return           void
  def after_commit(connection: ActiveRecord::Base.connection, &callback)
    AfterCommitEverywhere.register_callback(
      connection: connection,
      name: __callee__,
      callback: callback,
      no_tx_action: :execute,
    )
  end

  # Runs +callback+ before committing of outermost transaction for +connection+.
  #
  # If called outside transaction it will execute callback immediately.
  #
  # Available only since Ruby on Rails 5.0. See https://github.com/rails/rails/pull/18936
  #
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
  # @param callback   [#call] Callback to be executed
  # @return           void
  def before_commit(connection: ActiveRecord::Base.connection, &callback)
    if ActiveRecord::VERSION::MAJOR < 5
      raise NotImplementedError, "#{__callee__} works only with Rails 5.0+"
    end

    AfterCommitEverywhere.register_callback(
      connection: connection,
      name: __callee__,
      callback: callback,
      no_tx_action: :warn_and_execute,
    )
  end

  # Runs +callback+ after rolling back of transaction or savepoint (if declared
  # in nested transaction) for database +connection+.
  #
  # Caveat: do not raise +ActivRecord::Rollback+ in nested transaction block!
  # See http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
  #
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter]
  # @param callback   [#call] Callback to be executed
  # @return           void
  # @raise            [NotInTransaction] if called outside transaction.
  def after_rollback(connection: ActiveRecord::Base.connection, &callback)
    AfterCommitEverywhere.register_callback(
      connection: connection,
      name: __callee__,
      callback: callback,
      no_tx_action: :exception,
    )
  end

  class << self
    def register_callback(connection:, name:, no_tx_action:, callback:)
      raise ArgumentError, "Provide callback to #{name}" unless callback

      unless in_transaction?(connection)
        case no_tx_action
        when :warn_and_execute
          warn "#{name}: No transaction open. Executing callback immediately."
          return callback.call
        when :execute
          return callback.call
        when :exception
          raise NotInTransaction, "#{name} is useless outside transaction"
        end
      end
      wrap = Wrap.new(connection: connection, "#{name}": callback)
      connection.add_transaction_record(wrap)
    end

    # Helper method to determine whether we're currently in transaction or not
    def in_transaction?(connection = ActiveRecord::Base.connection)
      # service transactions (tests and database_cleaner) are not joinable
      connection.transaction_open? && connection.current_transaction.joinable?
    end
  end
end