# frozen_string_literal: true

module Gitlab
  class Experiment
    module BaseInterface
      extend ActiveSupport::Concern
      include Scientist::Experiment

      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_name(name).classify.safe_constantize || Configuration.base_class.constantize
        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)
        @variant_name = cache_variant(variant_name) { nil } if variant_name.present?

        compare { false }

        yield self if block_given?
      end

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

      def id
        "#{name}:#{context.key}"
      end
      alias_method :session_id, :id
      alias_method :to_param, :id

      def flipper_id
        "Experiment;#{id}"
      end

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

      def behaviors
        @behaviors ||= public_methods.each_with_object(super) do |name, behaviors|
          next unless name.end_with?('_behavior')

          behavior_name = name.to_s.sub(/_behavior$/, '')
          behaviors[behavior_name] ||= -> { send(name) } # rubocop:disable GitlabSecurity/PublicSend
        end
      end

      protected

      def raise_on_mismatches?
        false
      end

      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

      def generate_result(variant_name)
        observation = Scientist::Observation.new(variant_name, self, &behaviors[variant_name])
        Scientist::Result.new(self, [observation], observation)
      end
    end
  end
end
