# frozen_string_literal: true

require 'scientist'
require 'request_store'
require 'active_support/callbacks'
require 'active_support/cache'
require 'active_support/concern'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/module/delegation'

require 'gitlab/experiment/errors'
require 'gitlab/experiment/base_interface'
require 'gitlab/experiment/cache'
require 'gitlab/experiment/callbacks'
require 'gitlab/experiment/rollout'
require 'gitlab/experiment/configuration'
require 'gitlab/experiment/cookies'
require 'gitlab/experiment/context'
require 'gitlab/experiment/dsl'
require 'gitlab/experiment/middleware'
require 'gitlab/experiment/nestable'
require 'gitlab/experiment/variant'
require 'gitlab/experiment/version'
require 'gitlab/experiment/engine' if defined?(Rails::Engine)

module Gitlab
  class Experiment
    include BaseInterface
    include Cache
    include Callbacks
    include Nestable

    class << self
      def default_rollout(rollout = nil, options = {})
        return @rollout ||= Configuration.default_rollout if rollout.blank?

        @rollout = Rollout.resolve(rollout).new(options)
      end

      def exclude(*filter_list, **options, &block)
        build_callback(:exclusion_check, filter_list.unshift(block), **options) do |target, callback|
          throw(:abort) if target.instance_variable_get(:@excluded) || callback.call(target, nil) == true
        end
      end

      def segment(*filter_list, variant:, **options, &block)
        build_callback(:segmentation_check, filter_list.unshift(block), **options) do |target, callback|
          target.variant(variant) if target.instance_variable_get(:@variant_name).nil? && callback.call(target, nil)
        end
      end

      def published_experiments
        RequestStore.store[:published_gitlab_experiments] || {}
      end
    end

    def name
      [Configuration.name_prefix, @name].compact.join('_')
    end

    def control(&block)
      candidate(:control, &block)
    end
    alias_method :use, :control

    def candidate(name = nil, &block)
      name = (name || :candidate).to_s
      behaviors[name] = block
    end
    alias_method :try, :candidate

    def context(value = nil)
      return @context if value.blank?

      @context.value(value)
      @context
    end

    def variant(value = nil)
      @variant_name = cache_variant(value) if value.present?
      return Variant.new(name: (@variant_name || :unresolved).to_s) if @variant_name || @resolving_variant

      if enabled?
        @resolving_variant = true
        @variant_name = cached_variant_resolver(@variant_name)
      end

      run_callbacks(segmentation_callback_chain) do
        @variant_name ||= :control
        Variant.new(name: @variant_name.to_s)
      end
    ensure
      @resolving_variant = false
    end

    def rollout(rollout = nil, options = {})
      return @rollout ||= self.class.default_rollout(nil, options) if rollout.blank?

      @rollout = Rollout.resolve(rollout).new(options)
    end

    def exclude!
      @excluded = true
    end

    def run(variant_name = nil)
      return @result if context.frozen?

      @result = run_callbacks(:run) { super(variant(variant_name).name) }
    rescue Scientist::BehaviorMissing => e
      raise Error, e
    end

    def publish(result)
      instance_exec(result, &Configuration.publishing_behavior)

      (RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
    end

    def track(action, **event_args)
      return unless should_track?

      instance_exec(action, event_args, &Configuration.tracking_behavior)
    end

    def process_redirect_url(url)
      return unless Configuration.redirect_url_validator&.call(url)

      track('visited', url: url)
      url # return the url, which allows for mutation
    end

    def enabled?
      true
    end

    def excluded?
      return @excluded if defined?(@excluded)

      @excluded = !run_callbacks(:exclusion_check) { :not_excluded }
    end

    def experiment_group?
      instance_exec(@variant_name, &Configuration.inclusion_resolver)
    end

    def should_track?
      enabled? && @context.trackable? && !excluded?
    end

    def signature
      { variant: variant.name, experiment: name }.merge(context.signature)
    end

    def key_for(source, seed = name)
      # TODO: Added deprecation in release 0.6.0
      if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
        return instance_exec(source, seed, &block)
      end

      return source if source.is_a?(String)

      source = source.keys + source.values if source.is_a?(Hash)

      ingredients = Array(source).map { |v| identify(v) }
      ingredients.unshift(seed).unshift(Configuration.context_key_secret)

      Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
    end

    protected

    def identify(object)
      (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
    end

    def segmentation_callback_chain
      return :segmentation_check if @variant_name.nil? && enabled? && !excluded?

      :unsegmented
    end

    def resolve_variant_name
      rollout.rollout_for(self) if experiment_group?
    end
  end
end
