#
# This file is part of the ballast gem. Copyright (C) 2013 and above Shogun <shogun@cowtech.it>.
# Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
#

module Ballast
  # A operation represents a single responsibility class. Subclasses should only expose and override the #perform method.
  class Operation
    extend ::Forwardable
    include Interactor
    def_delegators :context, :owner, :errors, :response, :output

    # Performs the operation.
    #
    # @param owner_or_context [Object|Context] If is a context, then it will be the context of the operation, otherwise a blank a context with the object
    #   as owner will be created.
    # @param context [NilClass] The context for the operation. *Ignored if `owner_or_context` is a context.*
    # @param params [Hash] The additional parameters for the new context. *Ignored if `owner_or_context` is a context.*
    # @return [Operation] The performed operation.
    def self.perform(owner_or_context, context: nil, params: {})
      arg = owner_or_context
      arg = (context || ::Ballast::Context.build(owner_or_context, params)) if !arg.is_a?(::Ballast::Context)
      super(arg)
    end

    # Creates a new operation.
    #
    # @param context [Context] The context for the operation.
    def initialize(context)
      @context = context
      setup
    end

    # Sets up the response hash from instance variables.
    #
    # @param force [Boolean] Whether to setup the response even if the operation failed.
    # @return [Hash] The response hash.
    def setup_response(force = false)
      if success? || force then
        vars = instance_variables
        vars.delete(:@context)

        context.response.merge!(vars.reduce({}){ |rv, var|
          rv[var.to_s.gsub(/[:@]/, "")] = instance_variable_get(var)
          rv
        })
      end

      context.response
    end

    # Imports the response hash into the target instance variables.
    #
    # @param target [Object] The target of the import.
    # @param fields [Array] The keys to import.
    # @param overwrite [Boolean] Whether to overwrite existing variables into the target. If set to `false`, any overwrite will raise an `ArgumentError`.
    def import_response(target, *fields, overwrite: true)
      fields.each do |field|
        raise ArgumentError.new(field) if target.instance_variable_get("@#{field}") && !overwrite
        target.instance_variable_set("@#{field}", response[field])
      end
    end

    # Performs the operation handling base errors.
    #
    # @param setup_response_after [Boolean] Whether to setup the response after processing.
    def perform_with_handling(setup_response_after = true)
      begin
        yield
      rescue Lazier::Exceptions::Debug => de
        raise de
      rescue => e
        e.is_a?(::Ballast::Errors::BaseError) ? fail!(e.response) : raise(e)
      end

      setup_response if setup_response_after
    end

    # Marks failure of the operation appending the error to the context.
    #
    # @param error [Object|NilClass] The error to store.
    def fail!(error = nil)
      errors << error if error
      super()
    end

    # Imports the current operation errors into the target's `@error` instance variable or in the flash hash.
    #
    # @param target [Object] The target of the import.
    # @param to_flash [Boolean] If to import the error in the target's flash object rather than the instance variable.
    # @param first_only [Boolean] If to only import the first error.
    def import_error(target, to_flash = true, first_only = true)
      values = errors
      values = values.map {|v| v[:error] } if to_flash
      values = values.first if first_only

      if to_flash then
        target.flash[:error] = values
      else
        target.instance_variable_set(:@error, values)
      end
    end

    # Resolves a numeric error to a human readable message.
    #
    # @param error [BaseError|Fixnum] The error to resolve.
    # @param supported_messages [Hash] The list of supported error codes.
    # @param only_message [Boolean] If to only return the message string rather than a full error hash.
    # @return [String|Hash] The error with a human readable message or the message alone.
    def resolve_error(error, supported_messages = {}, only_message = false)
      code = (error.respond_to?(:response) ? error.response : 500).to_integer(500)
      rv = {status: code, error: supported_messages.fetch(code, "Oops! We're having some issue. Please try again later.")}
      only_message ? rv[:error] : rv
    end

    # If running under eventmachine, run the block in a thread of its threadpool using EM::Synchrony, otherwise run the block normally.
    #
    # @param block [Proc] The block to run.
    def in_em_thread(&block)
      Ballast.in_em_thread(&block)
    end

    # Forwards any missing method to the owner.
    #
    # @param method [Symbol] The method to forward.
    # @param args [Array] The arguments to pass to the method.
    # @param block [Proc] The block to pass to the method.
    def method_missing(method, *args, &block)
      if owner.respond_to?(method)
        owner.send(method, *args, &block)
      else
        super(method, *args, &block)
      end
    end
  end
end