# frozen_string_literal: true

module Gitlab
  class Experiment
    module BaseInterface
      extend ActiveSupport::Concern

      class_methods do
        def configure
          yield Configuration
        end

        def experiment_name(name = nil, suffix: true, suffix_word: 'experiment')
          name = (name.presence || self.name).to_s.underscore.sub(%r{(?<char>[_/]|)#{suffix_word}$}, '')
          name = "#{name}#{Regexp.last_match(:char) || '_'}#{suffix_word}"
          suffix ? name : name.sub(/_#{suffix_word}$/, '')
        end

        def base?
          self == Gitlab::Experiment || name == Configuration.base_class
        end

        def constantize(name = nil)
          return self if name.nil?

          experiment_class = experiment_name(name).classify
          experiment_class.safe_constantize || begin
            return Configuration.base_class.constantize unless Configuration.strict_registration

            raise UnregisteredExperiment, <<~ERR
              No experiment registered for `#{name}`. Please register the experiment by defining a class:

              class #{experiment_class} < #{Configuration.base_class}
                control
                candidate { 'candidate' }
              end
            ERR
          end
        end

        def from_param(id)
          %r{/?(?<name>.*):(?<key>.*)$} =~ id
          name = CGI.unescape(name) if name
          constantize(name).new(name).tap { |e| e.context.key(key) }
        end
      end

      def initialize(name = nil, variant_name = nil, **context)
        raise ArgumentError, 'name is required' if name.blank? && self.class.base?

        @_name = self.class.experiment_name(name, suffix: false)
        @_context = Context.new(self, **context)
        @_assigned_variant_name = cache_variant(variant_name) { nil } if variant_name.present?

        yield self if block_given?
      end

      def inspect
        "#<#{self.class.name || 'AnonymousClass'}:#{format('0x%016X', __id__)} name=#{name} context=#{context.value}>"
      end

      def run(variant_name)
        behaviors.freeze
        context.freeze

        block = behaviors[variant_name]
        raise BehaviorMissingError, "the `#{variant_name}` variant hasn't been registered" if block.nil?

        result = block.call
        publish(result) if enabled?

        result
      end

      def id
        "#{name}:#{context.key}"
      end

      alias_method :to_param, :id

      def variant_names
        @_variant_names ||= behaviors.keys.map(&:to_sym) - [:control]
      end

      def behaviors
        @_behaviors ||= public_behaviors_with_deprecations(registered_behavior_callbacks)
      end

      # @deprecated
      def public_behaviors_with_deprecations(behaviors)
        named_variants = %w[control candidate]
        public_methods.each_with_object(behaviors) do |name, behaviors|
          name = name.to_s # fixes compatibility for ruby 2.6.x
          next unless name.end_with?('_behavior')

          behavior_name = name.sub(/_behavior$/, '')
          registration = named_variants.include?(behavior_name) ? behavior_name : "variant :#{behavior_name}"

          Configuration.deprecated(<<~MESSAGE, version: '0.7.0', stack: 2)
            using a public `#{name}` method is deprecated and will be removed from {{release}}, instead register variants using:

            class #{self.class.name} < #{Configuration.base_class}
              #{registration}

              private

              def #{name}
                #...
              end
            end
          MESSAGE

          behaviors[behavior_name] ||= -> { send(name) } # rubocop:disable GitlabSecurity/PublicSend
        end
      end

      # @deprecated
      def session_id
        Configuration.deprecated(:session_id, 'instead use `id` or use a custom rollout strategy', version: '0.7.0')
        id
      end

      # @deprecated
      def flipper_id
        Configuration.deprecated(:flipper_id, 'instead use `id` or use a custom rollout strategy', version: '0.7.0')
        "Experiment;#{id}"
      end

      # @deprecated
      def use(&block)
        Configuration.deprecated(:use, 'instead use `control`', version: '0.7.0')

        control(&block)
      end

      # @deprecated
      def try(name = nil, &block)
        if name.present?
          Configuration.deprecated(:try, "instead use `variant(:#{name})`", version: '0.7.0')
          variant(name, &block)
        else
          Configuration.deprecated(:try, 'instead use `candidate`', version: '0.7.0')
          candidate(&block)
        end
      end

      protected

      def cached_variant_resolver(provided_variant)
        return :control if excluded?

        result = cache_variant(provided_variant) { resolve_variant_name }
        result.to_sym if result.present?
      end
    end
  end
end
