module BusinessFlow
  # Extends the DSL to support caching of completed processes
  module Cacheable
    def self.included(klass)
      klass.extend(ClassMethods)
    end

    def cache_key
      klass = self.class
      key = Digest::SHA256.hexdigest(klass.cache_key.call(self, nil).to_s)
      "#{klass.name.underscore}/#{key}/v3"
    end

    # DSL Methods
    module ClassMethods
      # Responsible for converting our DSL options into cache store options
      CacheOptions = Struct.new(:ttl) do
        def to_store_options(flow)
          # compact is not available in Ruby <2.4 or ActiveSupport < 4, so
          # we can't use it here.
          options = {}
          options[:expires_in] = ttl.call(flow, nil) if ttl
          options
        end
      end

      def cache_options
        @cache_options ||= CacheOptions.new
      end

      def cache_store(store = nil)
        if store
          @cache_store = store
        else
          @cache_store ||= if defined?(Rails)
                             Rails.cache
                           else
                             ActiveSupport::Cache::MemoryStore.new
                           end
        end
      end

      def cache_ttl(ttl = nil)
        if ttl.is_a?(Numeric)
          cache_options.ttl = proc { ttl }
        elsif ttl
          cache_options.ttl = Callable.new(ttl)
        else
          cache_options.ttl
        end
      end

      def cache_key(key = nil)
        if key
          @cache_key = Callable.new(key)
        else
          @cache_key ||= default_cache_key
        end
      end

      # :reek:UtilityFunction
      def default_cache_key
        Callable.new(:_business_flow_dsl_parameters)
      end

      def execute(flow)
        with_cache(flow) do
          super(flow)._business_flow_cacheable_finalize(flow.cache_key)
        end
      rescue FlowFailedException => exc
        exc.flow
      end

      def with_cache(flow, &blk)
        add_cache_key_to_result_class
        catch(:halt_step) do
          return instrument_cache_fetch(flow, &blk)
        end
        raise FlowFailedException, flow
      end

      def instrument_cache_fetch(flow)
        instrument(:cache, flow) do |payload|
          payload[:cache_hit] = true if payload
          cache_store.fetch(flow.cache_key,
                            cache_options.to_store_options(flow)) do
            payload[:cache_hit] = false if payload
            yield
          end
        end
      end

      RESULT_FINALIZE = proc do |cache_key|
        @cache_key = cache_key
        raise FlowFailedException, self if errors?
        self
      end

      def add_cache_key_to_result_class
        return if @cache_key_added
        result_class = const_get(:Result)
        DSL::PublicField.new(:cache_key).add_to(result_class)
        result_class.send(:define_method, :_business_flow_cacheable_finalize,
                          RESULT_FINALIZE)
        @cache_key_added = true
      end
    end
  end
end