require 'determinator/version'
require 'determinator/control'
require 'determinator/feature'
require 'determinator/target_group'
require 'determinator/fixed_determination'
require 'determinator/explainer'
require 'determinator/cache/fetch_wrapper'
require 'determinator/serializers/json'
require 'determinator/missing_response'
require 'determinator/error_response'
require 'determinator/tracking'


module Determinator
  class << self
    attr_reader :feature_cache, :retrieval
    # @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
    # @param :feature_cache [#call] a caching proc, accepting a feature name, which will return the named feature or yield (and store) if not available
    # @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
    # @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
    def configure(retrieval:, feature_cache:, errors: nil, missing_feature: nil)
      self.on_error(&errors) if errors
      self.on_missing_feature(&missing_feature) if missing_feature
      @feature_cache = feature_cache
      @retrieval = retrieval
      @instance = Control.new(retrieval: retrieval)
    end

    # Returns the currently configured Determinator::Control instance
    #
    # @return [Determinator::Control] The currently active instance of determinator.
    # @raises [RuntimeError] If no Determinator instance is set up (with `.configure`)
    def instance
      raise "No singleton Determinator instance defined" unless @instance
      @instance
    end

    # Defines how errors that shouldn't break your application should be logged
    def on_error(&block)
      @error_logger = block
    end

    # Defines how to record the moment when a feature which doesn't exist is requested.
    # If this happens a lot it indicates poor set up, so can be useful for tracking.
    def on_missing_feature(&block)
      @missing_feature_logger = block
    end

    # Defines code that should execute when a determination is completed. This is particularly
    # helpful for preparing or sending events to record that an actor has seen a particular experiment variant.
    #
    # Please note that this block will be executed _synchronously_ before delivering the determination to the callsite.
    #
    # @yield [id, guid, feature, determination] Will be called when a determination was requested for the
    #   specified `feature`, for the actor with `id` and `guid`, and received the determination `determination`.
    # @yieldparam id [String, nil] The ID that was used to request the determination
    # @yieldparam guid [String, nil] The GUID that was used to request the determination
    # @yieldparam feature [Determinator::Feature] The feature that was requested
    # @yieldparam determination [String,Boolean] The result of the determination
    def on_determination(&block)
      @determination_callback = block
    end

    # Returns the feature with the given name as Determinator uses it. This is useful for
    # debugging issues with the retrieval mechanism which delivers features to Determinator.
    # @returns [Determinator::Feature,nil] The feature details Determinator would use for a determination right now.
    def feature_details(name)
      with_retrieval_cache(name) { instance.retrieval.retrieve(name) }
    end

    # Allows Determinator to track that an error has happened with determination
    # @api private
    def notice_error(error)
      return unless @error_logger

      error = RuntimeError.new(error) unless error.is_a?(StandardError)
      @error_logger.call(error)
    end

    # Allows Determinator to track that a feature was requested but was missing
    # @api private
    def notice_missing_feature(name)
      return unless @missing_feature_logger

      @missing_feature_logger.call(name)
    end

    def notice_determination(id, guid, feature, determination)
      Determinator::Tracking.track(id, guid, feature, determination)
      return unless @determination_callback
      @determination_callback.call(id, guid, feature, determination)
    end

    # Allows access to the chosen caching mechanism for any retrieval plugin.
    # @api private
    def with_retrieval_cache(name)
      return yield unless @feature_cache.respond_to?(:call)

      @feature_cache.call(name) { yield }
    end

    def invalidate_cache(name)
      @feature_cache.expire(name)
    end
  end
end
