# frozen_string_literal: true

module Gitlab
  module Search
    class AbuseDetection
      include ActiveModel::Validations
      include AbuseValidators

      MAX_PIPE_SYNTAX_FILTERS = 5
      ABUSIVE_TERM_SIZE = 100
      ALLOWED_CHARS_REGEX = %r{\A[[:alnum:]_\-\+\/\.!]+\z}

      ALLOWED_SCOPES = %w[
        blobs
        code
        commits
        epics
        issues
        merge_requests
        milestones
        notes
        projects
        snippet_titles
        users
        wiki_blobs
      ].freeze

      READABLE_PARAMS = %i[
        group_id
        project_id
        project_ref
        query_string
        repository_ref
        scope
      ].freeze

      STOP_WORDS = %w[
        a an and are as at be but by for if in into is it no not of on or such that the their then there these they this
        to was will with
      ].freeze

      validates :project_id, :group_id,
        numericality: { only_integer: true, message: "abusive ID detected" }, allow_blank: true

      validates :scope, inclusion: { in: ALLOWED_SCOPES, message: 'abusive scope detected' }, allow_blank: true

      validates :repository_ref, :project_ref,
        format: { with: ALLOWED_CHARS_REGEX, message: "abusive characters detected" }, allow_blank: true

      validates :query_string,
        exclusion: { in: STOP_WORDS, message: 'stopword only abusive search detected' }, allow_blank: true

      validates :query_string,
        length: { minimum: Params::MIN_TERM_LENGTH, message: 'abusive tiny search detected' },
        unless: :skip_tiny_search_validation?, allow_blank: true

      validates :query_string,
        no_abusive_term_length: { maximum: ABUSIVE_TERM_SIZE, maximum_for_url: ABUSIVE_TERM_SIZE * 2 }

      validates :query_string, :repository_ref, :project_ref, no_abusive_coercion_from_string: true

      attr_reader(*READABLE_PARAMS, :raw_params)

      def initialize(params)
        @raw_params = {}
        READABLE_PARAMS.each do |p|
          instance_variable_set(:"@#{p}", params[p])
          @raw_params[p] = params[p]
        end
      end

      def abusive_pipes?
        pipes = query_string.to_s.split('|')
        errors.add(:query_string, 'too many pipe syntax filters') if pipes.length > MAX_PIPE_SYNTAX_FILTERS
        pipes.each do |q|
          self.class.new(raw_params.merge(query_string: q)).tap do |p|
            p.validate

            p.errors.messages_for(:query_string).each do |msg|
              next if errors.added?(:query_string, msg)

              errors.add(:query_string, msg)
            end
          end
        end
        errors.any?
      end

      private

      def skip_tiny_search_validation?
        wildcard_search? || stop_word_search?
      end

      def wildcard_search?
        query_string == '*'
      end

      def stop_word_search?
        STOP_WORDS.include? query_string
      end
    end
  end
end
