 # frozen_string_literal: true

# frozen_string_literal: true
module GraphQL
  module StaticValidation
    module FieldsWillMerge
      # Validates that a selection set is valid if all fields (including spreading any
      # fragments) either correspond to distinct response names or can be merged
      # without ambiguity.
      #
      # Original Algorithm: https://github.com/graphql/graphql-js/blob/master/src/validation/rules/OverlappingFieldsCanBeMerged.js
      NO_ARGS = {}.freeze

      Field = Struct.new(:node, :definition, :owner_type, :parents)
      FragmentSpread = Struct.new(:name, :parents)

      def initialize(*)
        super
        @visited_fragments = {}
        @compared_fragments = {}
        @conflict_count = 0
      end

      def on_operation_definition(node, _parent)
        setting_errors { conflicts_within_selection_set(node, type_definition) }
        super
      end

      def on_field(node, _parent)
        setting_errors { conflicts_within_selection_set(node, type_definition) }
        super
      end

      private

      def field_conflicts
        @field_conflicts ||= Hash.new do |errors, field|
          errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :field, field_name: field)
        end
      end

      def arg_conflicts
        @arg_conflicts ||= Hash.new do |errors, field|
          errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
        end
      end

      def setting_errors
        @field_conflicts = nil
        @arg_conflicts = nil

        yield

        field_conflicts.each_value { |error| add_error(error) }
        arg_conflicts.each_value { |error| add_error(error) }
      end

      def conflicts_within_selection_set(node, parent_type)
        return if parent_type.nil?

        fields, fragment_spreads = fields_and_fragments_from_selection(node, owner_type: parent_type, parents: [])

        # (A) Find find all conflicts "within" the fields of this selection set.
        find_conflicts_within(fields)

        fragment_spreads.each_with_index do |fragment_spread, i|
          are_mutually_exclusive = mutually_exclusive?(
            fragment_spread.parents,
            [parent_type]
          )

          # (B) Then find conflicts between these fields and those represented by
          # each spread fragment name found.
          find_conflicts_between_fields_and_fragment(
            fragment_spread,
            fields,
            mutually_exclusive: are_mutually_exclusive,
          )

          # (C) Then compare this fragment with all other fragments found in this
          # selection set to collect conflicts between fragments spread together.
          # This compares each item in the list of fragment names to every other
          # item in that same list (except for itself).
          fragment_spreads[i + 1..-1].each do |fragment_spread2|
            are_mutually_exclusive = mutually_exclusive?(
              fragment_spread.parents,
              fragment_spread2.parents
            )

            find_conflicts_between_fragments(
              fragment_spread,
              fragment_spread2,
              mutually_exclusive: are_mutually_exclusive,
            )
          end
        end
      end

      def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:)
        fragment_name1 = fragment_spread1.name
        fragment_name2 = fragment_spread2.name
        return if fragment_name1 == fragment_name2

        cache_key = compared_fragments_key(
          fragment_name1,
          fragment_name2,
          mutually_exclusive,
        )
        if @compared_fragments.key?(cache_key)
          return
        else
          @compared_fragments[cache_key] = true
        end

        fragment1 = context.fragments[fragment_name1]
        fragment2 = context.fragments[fragment_name2]

        return if fragment1.nil? || fragment2.nil?

        fragment_type1 = context.warden.get_type(fragment1.type.name)
        fragment_type2 = context.warden.get_type(fragment2.type.name)

        return if fragment_type1.nil? || fragment_type2.nil?

        fragment_fields1, fragment_spreads1 = fields_and_fragments_from_selection(
          fragment1,
          owner_type: fragment_type1,
          parents: [*fragment_spread1.parents, fragment_type1]
        )
        fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection(
          fragment2,
          owner_type: fragment_type1,
          parents: [*fragment_spread2.parents, fragment_type2]
        )

        # (F) First, find all conflicts between these two collections of fields
        # (not including any nested fragments).
        find_conflicts_between(
          fragment_fields1,
          fragment_fields2,
          mutually_exclusive: mutually_exclusive,
        )

        # (G) Then collect conflicts between the first fragment and any nested
        # fragments spread in the second fragment.
        fragment_spreads2.each do |fragment_spread|
          find_conflicts_between_fragments(
            fragment_spread1,
            fragment_spread,
            mutually_exclusive: mutually_exclusive,
          )
        end

        # (G) Then collect conflicts between the first fragment and any nested
        # fragments spread in the second fragment.
        fragment_spreads1.each do |fragment_spread|
          find_conflicts_between_fragments(
            fragment_spread2,
            fragment_spread,
            mutually_exclusive: mutually_exclusive,
          )
        end
      end

      def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:)
        fragment_name = fragment_spread.name
        return if @visited_fragments.key?(fragment_name)
        @visited_fragments[fragment_name] = true

        fragment = context.fragments[fragment_name]
        return if fragment.nil?

        fragment_type = context.warden.get_type(fragment.type.name)
        return if fragment_type.nil?

        fragment_fields, fragment_spreads = fields_and_fragments_from_selection(fragment, owner_type: fragment_type, parents: [*fragment_spread.parents, fragment_type])

        # (D) First find any conflicts between the provided collection of fields
        # and the collection of fields represented by the given fragment.
        find_conflicts_between(
          fields,
          fragment_fields,
          mutually_exclusive: mutually_exclusive,
        )

        # (E) Then collect any conflicts between the provided collection of fields
        # and any fragment names found in the given fragment.
        fragment_spreads.each do |fragment_spread|
          find_conflicts_between_fields_and_fragment(
            fragment_spread,
            fields,
            mutually_exclusive: mutually_exclusive,
          )
        end
      end

      def find_conflicts_within(response_keys)
        response_keys.each do |key, fields|
          next if fields.size < 2
          # find conflicts within nodes
          for i in 0..fields.size - 1
            for j in i + 1..fields.size - 1
              find_conflict(key, fields[i], fields[j])
            end
          end
        end
      end

      def find_conflict(response_key, field1, field2, mutually_exclusive: false)
        return if @conflict_count >= context.max_errors

        node1 = field1.node
        node2 = field2.node

        are_mutually_exclusive = mutually_exclusive ||
                                 mutually_exclusive?(field1.parents, field2.parents)

        if !are_mutually_exclusive
          if node1.name != node2.name
            conflict = field_conflicts[response_key]

            conflict.add_conflict(node1, node1.name)
            conflict.add_conflict(node2, node2.name)

            @conflict_count += 1
          end

          if !same_arguments?(node1, node2)
            conflict = arg_conflicts[response_key]

            conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
            conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))

            @conflict_count += 1
          end
        end

        find_conflicts_between_sub_selection_sets(
          field1,
          field2,
          mutually_exclusive: are_mutually_exclusive,
        )
      end

      def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
        return if field1.definition.nil? || field2.definition.nil?

        return_type1 = field1.definition.type.unwrap
        return_type2 = field2.definition.type.unwrap
        parents1 = [return_type1]
        parents2 = [return_type2]

        fields, fragment_spreads = fields_and_fragments_from_selection(
          field1.node,
          owner_type: return_type1,
          parents: parents1
        )

        fields2, fragment_spreads2 = fields_and_fragments_from_selection(
          field2.node,
          owner_type: return_type2,
          parents: parents2
        )

        # (H) First, collect all conflicts between these two collections of field.
        find_conflicts_between(fields, fields2, mutually_exclusive: mutually_exclusive)

        # (I) Then collect conflicts between the first collection of fields and
        # those referenced by each fragment name associated with the second.
        fragment_spreads2.each do |fragment_spread|
          find_conflicts_between_fields_and_fragment(
            fragment_spread,
            fields,
            mutually_exclusive: mutually_exclusive,
          )
        end

        # (I) Then collect conflicts between the second collection of fields and
        # those referenced by each fragment name associated with the first.
        fragment_spreads.each do |fragment_spread|
          find_conflicts_between_fields_and_fragment(
            fragment_spread,
            fields2,
            mutually_exclusive: mutually_exclusive,
          )
        end

        # (J) Also collect conflicts between any fragment names by the first and
        # fragment names by the second. This compares each item in the first set of
        # names to each item in the second set of names.
        fragment_spreads.each do |frag1|
          fragment_spreads2.each do |frag2|
            find_conflicts_between_fragments(
              frag1,
              frag2,
              mutually_exclusive: mutually_exclusive
            )
          end
        end
      end

      def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
        response_keys.each do |key, fields|
          fields2 = response_keys2[key]
          if fields2
            fields.each do |field|
              fields2.each do |field2|
                find_conflict(
                  key,
                  field,
                  field2,
                  mutually_exclusive: mutually_exclusive,
                )
              end
            end
          end
        end
      end

      NO_SELECTIONS = [{}.freeze, [].freeze].freeze

      def fields_and_fragments_from_selection(node, owner_type:, parents:)
        if node.selections.empty?
          NO_SELECTIONS
        else
          fields, fragment_spreads = find_fields_and_fragments(node.selections, owner_type: owner_type, parents: parents, fields: [], fragment_spreads: [])
          response_keys = fields.group_by { |f| f.node.alias || f.node.name }
          [response_keys, fragment_spreads]
        end
      end

      def find_fields_and_fragments(selections, owner_type:, parents:, fields:, fragment_spreads:)
        selections.each do |node|
          case node
          when GraphQL::Language::Nodes::Field
            definition = context.schema.get_field(owner_type, node.name)
            fields << Field.new(node, definition, owner_type, parents)
          when GraphQL::Language::Nodes::InlineFragment
            fragment_type = node.type ? context.warden.get_type(node.type.name) : owner_type
            find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: owner_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
          when GraphQL::Language::Nodes::FragmentSpread
            fragment_spreads << FragmentSpread.new(node.name, parents)
          end
        end

        [fields, fragment_spreads]
      end

      def same_arguments?(field1, field2)
        # Check for incompatible / non-identical arguments on this node:
        arguments1 = field1.arguments
        arguments2 = field2.arguments

        return false if arguments1.length != arguments2.length

        arguments1.all? do |argument1|
          argument2 = arguments2.find { |argument| argument.name == argument1.name }
          return false if argument2.nil?

          serialize_arg(argument1.value) == serialize_arg(argument2.value)
        end
      end

      def serialize_arg(arg_value)
        case arg_value
        when GraphQL::Language::Nodes::AbstractNode
          arg_value.to_query_string
        when Array
          "[#{arg_value.map { |a| serialize_arg(a) }.join(", ")}]"
        else
          GraphQL::Language.serialize(arg_value)
        end
      end

      def serialize_field_args(field)
        serialized_args = {}
        field.arguments.each do |argument|
          serialized_args[argument.name] = serialize_arg(argument.value)
        end
        serialized_args
      end

      def compared_fragments_key(frag1, frag2, exclusive)
        # Cache key to not compare two fragments more than once.
        # The key includes both fragment names sorted (this way we
        # avoid computing "A vs B" and "B vs A"). It also includes
        # "exclusive" since the result may change depending on the parent_type
        "#{[frag1, frag2].sort.join('-')}-#{exclusive}"
      end

      # Given two list of parents, find out if they are mutually exclusive
      # In this context, `parents` represends the "self scope" of the field,
      # what types may be found at this point in the query.
      def mutually_exclusive?(parents1, parents2)
        if parents1.empty? || parents2.empty?
          false
        elsif parents1.length == parents2.length
          parents1.length.times.any? do |i|
            type1 = parents1[i - 1]
            type2 = parents2[i - 1]
            if type1 == type2
              # If the types we're comparing are the same type,
              # then they aren't mutually exclusive
              false
            else
              # Check if these two scopes have _any_ types in common.
              possible_right_types = context.query.possible_types(type1)
              possible_left_types = context.query.possible_types(type2)
              (possible_right_types & possible_left_types).empty?
            end
          end
        else
          true
        end
      end
    end
  end
end
