module BusinessFlow
  # Core DSL for BusinessFlow. The relevant methods are all in
  # ClassMethods.
  module DSL
    # Contains the DSL for BusinessFlow
    module ClassMethods
      # Requires that a field be retrievable from the initialization parameters
      #
      # This will only require that the field is not nil. The field may still
      # be #empty?
      #
      # @param fields The fields required from the initialization parameters
      def needs(*fields)
        @needs ||= FieldList.new([], ParameterField, self)
        @needs.add_fields(fields)
      end

      # Allows a field to be retrieved from the initialiaztion parameters
      def wants(field, default = proc { nil }, opts = {})
        internal_name = "wants_#{field}".to_sym
        uses(internal_name, default, opts)
        ParameterField.new(field, internal_name).add_to(self)
      end

      # Declares that you will expose a field to the outside world.
      def provides(*fields)
        @provides ||= FieldList.new([], PublicField, [self, const_get(:Result)])
        @result_copy ||= 'def from_flow(flow)'
        @result_copy += fields.map { |field| "@#{field} = flow.#{field}" }
                              .join("\n")
        @provides.add_fields(fields)
      end

      def uses(field, klass, opts = {})
        step = Step.new(Callable.new(klass), opts)
        retriever = proc { step.call(self).output }
        UsesField.new(field, retriever).add_to(self)
      end

      def step(klass, opts = {})
        step = Step.new(Callable.new(klass), opts)
        step_queue.push(step)
        step.outputs
            .values
            .select { |field| field.is_a?(Symbol) }
            .each { |field| Field.new(field).add_to(self) }
      end

      def call(parameter_object = {})
        execute(build(parameter_object))
      end

      # :reek:UtilityFunction This is a function on us so that other modules
      # can change execution behavior.
      def execute(flow)
        catch(:halt_step) { flow.call }
        result_from(flow)
      end

      def build(parameter_object)
        finalize_initializer
        allocate.tap do |flow|
          catch(:halt_step) do
            flow.send(:_business_flow_dsl_initialize,
                      ParameterObject.new(parameter_object))
          end
        end
      end

      def call!(*args)
        flow = call(*args)
        raise FlowFailedException, flow if flow.errors?
        flow
      end

      def step_queue
        @step_queue ||= []
      end

      def step_executor(executor_class = nil)
        if executor_class
          @executor_class = executor_class
        else
          @executor_class ||= ::BusinessFlow::DefaultStepExecutor
        end
      end

      def result_from(flow)
        finalize_result_provider
        # We use instance_variable_get here instead of making it part of
        # from_flow to ensure that we do not create the errors object unless
        # we need it.
        result = const_get(:Result).new(flow.instance_variable_get(:@errors))
        result.from_flow(flow) if @result_copy
        result
      end

      def finalize_result_provider
        return if @finalized_result_provider || !@result_copy
        const_get(:Result).class_eval "#{@result_copy}\nend", __FILE__, __LINE__
        @finalized_result_provider = true
      end

      def needs_code
        needs.map do |need|
          %(if #{need}.nil?
              errors[:#{need}] << 'must not be nil'
              throw :halt_step
            end
          )
        end.join("\n")
      end

      def finalize_initializer
        return if @finalized_initializer
        class_eval %{
          private def _business_flow_dsl_initialize(parameter_object)
            @parameter_object = parameter_object
            #{needs_code}
            initialize
          end
        }, __FILE__, __LINE__ - 6
        @finalized_initializer = true
      end
    end

    RESULT_DEF = %(
      class Result
        def initialize(errors)
          @errors = errors
        end

        def errors
          @errors ||= ActiveModel::Errors.new(self)
        end

        def errors?
          # We're explicitly using the instance variable here so that if no
          # errors have been created, we don't initialize the error object.
          @errors.present?
        end

        def valid?(_context = nil)
          # We're explicitly using the instance variable here so that if no
          # errors have been created, we don't initialize the error object.
          @errors.blank?
        end

        def invalid?(context = nil)
          !valid?(context)
        end
      end
    ).freeze

    # Provides the minimum necessary methods to support the use of
    # ActiveModel::Errors
    module ErrorSupport
      def human_attribute_name(key, _opts = {})
        key
      end
    end

    # :reek:ManualDispatch I have no need to actually call human_attribute_name,
    # I just need to know if I have to provide my own.
    def self.included(klass)
      klass.extend(ClassMethods)
      klass.class_eval RESULT_DEF, __FILE__, __LINE__
      klass.extend(ErrorSupport) unless klass.respond_to?(:human_attribute_name)
    end

    attr_reader :parameter_object
    private :parameter_object

    def call
      return if invalid?
      klass = self.class
      klass.step_executor.new(klass.step_queue, self).call
    end

    # Responsible for setting the parameter object and validating inputs.
    # This is a method directly on the object instead of something we
    # handle through instance_eval/exec for performance reasons.
    # :reek:NilCheck This is where we ensure that our needs are non-nil.
    private def _business_flow_dsl_initialize(parameter_object, needs)
      @parameter_object = parameter_object
      needs.each do |need|
        if send(need).nil?
          errors[need] << 'must not be nil'
          throw :halt_step
        end
      end
      initialize
    end

    def errors
      @errors ||= ActiveModel::Errors.new(self)
    end

    def errors?
      # We're explicitly using the instance variable here so that if no
      # errors have been created, we don't initialize the error object.
      @errors.present?
    end

    def valid?(_context = nil)
      # We're explicitly using the instance variable here so that if no
      # errors have been created, we don't initialize the error object.
      @errors.blank?
    end

    def invalid?(context = nil)
      !valid?(context)
    end

    # Responsible for creating fields on one or more classes and noting the of
    # field
    class FieldList
      attr_reader :field_list

      def initialize(field_list, field_klass, klasses)
        @field_list = []
        @field_klass = field_klass
        @klasses = [klasses].flatten
        add_fields(field_list)
      end

      def add_fields(fields)
        fields.each do |field|
          add_field(@field_klass.new(field))
        end
        @field_list.concat(fields)
      end

      private

      def add_field(field)
        @klasses.each { |klass| field.add_to(klass) }
      end
    end

    # Helper class to manage logic around adding fields
    class Field
      def initialize(field)
        @field = field
        # For proc bindings.
        ivar_name = instance_variable_name
        @getter = ivar_name
        @setter = self.class.setter_factory(field, ivar_name)
      end

      def add_to(klass)
        Field.eval_method(klass, field, getter)
        Field.eval_method(klass, setter_name, setter)
      end

      def self.eval_method(klass, name, str)
        return if klass.method_defined?(name) ||
                  klass.private_method_defined?(name)
        unsafe_eval_method(klass, name, str)
      end

      def self.unsafe_eval_method(klass, name, str)
        body = ["private def #{name}", str, 'end'].join("\n")
        klass.class_eval body, __FILE__, __LINE__
      end

      def self.setter_factory(field, ivar_name)
        <<-SETTER
          #{ivar_name} = new_value
          throw :halt_step unless valid?(:#{field})
          new_value
        SETTER
      end

      private

      attr_reader :field, :getter, :setter

      def setter_name
        @setter_name ||= "#{field}=(new_value)"
      end

      def instance_variable_name
        @instance_variable_name ||= "@#{field}"
      end
    end

    # Create a field with a public getter
    class PublicField
      def initialize(field)
        @name = field
        @field = Field.new(field)
      end

      def add_to(klass)
        @field.add_to(klass)
        klass.send(:public, @name)
      end
    end

    # Helper class around memoized fields
    class MemoizedField
      def initialize(field, retriever, setter_factory)
        @field = field
        @retriever = retriever
        @setter_factory = setter_factory
      end

      def add_to(klass)
        setter = setter_factory.call(field, safe_ivar_name)
        Field.unsafe_eval_method(
          klass, field, memoized(safe_ivar_name, setter, retriever)
        )
      end

      private

      attr_reader :field, :retriever, :setter_factory

      def memoized(ivar_name, setter, retriever)
        <<-MEMOIZED
          return #{ivar_name} if defined?(#{ivar_name})
          new_value = begin
            #{retriever}
          end
          #{setter}
        MEMOIZED
      end

      def safe_ivar_name
        @safe_ivar_name ||= begin
          "@business_flow_dsl_#{field}"
            .sub(/\?$/, '_query')
            .sub(/\!$/, '_bang')
            .to_sym
        end
      end
    end

    # Responsible for declaring fields which will be memoized and validated
    # when first set
    class UsesField
      def initialize(field, retriever)
        @name = field
        @retriever = retriever
        @field = MemoizedField.new(field, retriever_method_name,
                                   Field.method(:setter_factory))
      end

      def add_to(klass)
        klass.send(:define_method, retriever_method_name, &@retriever)
        klass.send(:private, retriever_method_name)
        @field.add_to(klass)
      end

      private

      def retriever_method_name
        @retriever_method_name ||=
          "_business_flow_dsl_execute_step_for_#{@name}".to_sym
      end
    end

    # Helper class around input parameter fields
    class ParameterField
      def initialize(field, fallback = nil)
        retriever = "@parameter_object.fetch(:#{field})"
        retriever += " { send(:#{fallback}) }" if fallback
        @field = MemoizedField.new(field, retriever, method(:setter_factory))
      end

      def add_to(klass)
        @field.add_to(klass)
      end

      private

      def setter_factory(_field, ivar_name)
        "#{ivar_name} = new_value"
      end
    end

    # Manage logic around input parameters
    class ParameterObject
      def initialize(parameters)
        @parameters = parameters
      end

      def fetch(key)
        value = inner_fetch(key)
        return yield if !value && block_given?
        value
      end

      def to_s
        @parameters.to_s
      end

      private

      def inner_fetch(key)
        if @parameters.is_a?(Hash) && @parameters.key?(key)
          @parameters[key]
        else
          @parameters.public_send(key)
        end
      rescue NoMethodError
        nil
      end
    end
  end
end