# frozen_string_literal: true

require "dry/transaction/step_failure"
require "dry/transaction/step_adapter"

module Dry
  module Transaction
    # @api private
    class Step
      UNDEFINED = Object.new.freeze
      RETURN = -> x { x }

      include Dry::Events::Publisher[name || object_id]
      include Dry::Monads[:result]

      register_event(:step)
      register_event(:step_succeeded)
      register_event(:step_failed)

      attr_reader :adapter
      attr_reader :name
      attr_reader :operation_name
      attr_reader :call_args

      # rubocop:disable Metrics/ParameterLists
      def initialize(adapter:, name:, operation_name:, options:, operation: nil, call_args: [])
        @adapter = StepAdapter[adapter, operation,
                               {**options, step_name: name, operation_name: operation_name}]
        @name = name
        @operation_name = operation_name
        @call_args = call_args
      end
      # rubocop:enable Metrics/ParameterLists

      def with(operation: UNDEFINED, call_args: UNDEFINED)
        return self if operation == UNDEFINED && call_args == UNDEFINED

        new_operation = operation == UNDEFINED ? adapter.operation : operation
        new_call_args = call_args == UNDEFINED ? self.call_args : Array(call_args)

        self.class.new(
          adapter: adapter,
          name: name,
          operation_name: operation_name,
          operation: new_operation,
          options: adapter.options,
          call_args: new_call_args
        )
      end

      def call(input, continue = RETURN)
        args = [input, *call_args]

        if adapter.yields?
          with_broadcast(args) { adapter.(args, &continue) }
        else
          continue.(with_broadcast(args) { adapter.(args) })
        end
      end

      def with_broadcast(args)
        publish(:step, step_name: name, args: args)

        yield.fmap { |value|
          publish(:step_succeeded, step_name: name, args: args, value: value)
          value
        }.or { |value|
          Failure(
            StepFailure.(self, value) {
              publish(:step_failed, step_name: name, args: args, value: value)
            }
          )
        }
      end

      def internal?
        !external?
      end

      def external?
        !!operation_name
      end

      def arity
        adapter.operation.arity
      end

      def operation
        adapter.operation
      end
    end
  end
end
