module BusinessFlow
  # Step is a conditional callable which can marshal its own inputs, and
  # returns a value which can marshal errors and outputs into a given object.
  class Step
    # Represents inputs needed to execute a step.
    class Inputs
      attr_reader :inputs

      def initialize(inputs)
        @inputs = inputs
      end

      def parameters_from_source(source)
        return source if inputs.blank?
        Hash[inputs.map do |input_name, input_value|
               [
                 input_name,
                 Inputs.process_input(source, input_value)
               ]
             end]
      end

      def self.process_input(source, input_value)
        case input_value
        when Symbol
          source.send(input_value)
        when Proc
          source.instance_exec(&input_value)
        else
          input_value
        end
      end
    end

    # Represents the result of a step, and allows setting response values on
    # an object, and merging error data into the same object.
    class Result
      def initialize(result, output_map, output_callable)
        @result = result
        @output_map = output_map
        @output_callable = output_callable
        @result_errors = begin
                           result.errors
                         rescue NoMethodError
                           nil
                         end
      end

      def merge_into(object)
        merge_errors_into(object.errors)
        merge_outputs_into(object)
      end

      def executed?
        true
      end

      def errors?
        @result_errors.present?
      end

      def output
        @output_callable.call(@result)
      end

      def self.process_output(object, output, output_setter)
        case output_setter
        when Symbol
          object.send("#{output_setter}=", output)
        when Proc
          object.instance_exec(output, &output_setter)
        end
      end

      private

      def merge_errors_into(errors)
        return if @result_errors.blank?
        @result_errors.each do |attribute, message|
          attribute = "#{@result.class.name.underscore}.#{attribute}"
          (errors[attribute] << message).uniq!
        end
        throw :halt_step
      end

      def merge_outputs_into(object)
        @output_map.each do |(output_name, output_setter)|
          output = @result.public_send(output_name)
          Result.process_output(object, output, output_setter)
        end
      end
    end

    # Returned if our conditional check failed. Does nothing.
    class ConditionFailedResult
      def executed?
        false
      end

      def errors?
        false
      end

      def merge_into(_object); end
    end

    # Manage creating results for our step
    ResultFactory = Struct.new(:outputs, :output_callable) do
      def result(step_result)
        Result.new(step_result, outputs, output_callable)
      end
    end

    def initialize(callable, opts)
      @callable = callable
      @input_object = Inputs.new(opts[:inputs] || {})
      outputs = opts[:outputs] || {}
      output = opts[:output] || ->(result) { result }
      @result_factory = ResultFactory.new(outputs, output)
      @condition = opts[:condition] || proc { true }
    end

    def call(parameter_source)
      parameters = @input_object.parameters_from_source(parameter_source)
      if @condition.call(parameter_source, parameters)
        @result_factory.result(@callable.call(parameter_source, parameters))
      else
        ConditionFailedResult.new
      end
    end

    def inputs
      @input_object.inputs
    end

    def outputs
      @result_factory.outputs
    end

    def to_s
      @callable.to_s
    end
  end
end