# frozen_string_literal: true
module GraphQL
  module Execution
    # A valid execution strategy
    # @api private
    class Execute

      # @api private
      class Skip; end

      # Just a singleton for implementing {Query::Context#skip}
      # @api private
      SKIP = Skip.new

      # @api private
      class PropagateNull
      end
      # @api private
      PROPAGATE_NULL = PropagateNull.new

      def execute(ast_operation, root_type, query)
        result = resolve_root_selection(query)
        lazy_resolve_root_selection(result, **{query: query})
        GraphQL::Execution::Flatten.call(query.context)
      end

      def self.begin_multiplex(_multiplex)
      end

      def self.begin_query(query, _multiplex)
        ExecutionFunctions.resolve_root_selection(query)
      end

      def self.finish_multiplex(results, multiplex)
        ExecutionFunctions.lazy_resolve_root_selection(results, multiplex: multiplex)
      end

      def self.finish_query(query, _multiplex)
        {
          "data" => Execution::Flatten.call(query.context)
        }
      end

      # @api private
      module ExecutionFunctions
        module_function

        def resolve_root_selection(query)
          query.trace("execute_query", query: query) do
            operation = query.selected_operation
            op_type = operation.operation_type
            root_type = query.root_type_for_operation(op_type)
            if query.context[:__root_unauthorized]
              # This was set by member/instrumentation.rb so that we wouldn't continue.
            else
              resolve_selection(
                query.root_value,
                root_type,
                query.context,
                mutation: query.mutation?
              )
            end
          end
        end

        def lazy_resolve_root_selection(result, query: nil, multiplex: nil)
          if query.nil? && multiplex.queries.length == 1
            query = multiplex.queries[0]
          end

          tracer = (query || multiplex)
          tracer.trace("execute_query_lazy", {multiplex: multiplex, query: query}) do
            GraphQL::Execution::Lazy.resolve(result)
          end
        end

        def resolve_selection(object, current_type, current_ctx, mutation: false )
          # Assign this _before_ resolving the children
          # so that when a child propagates null, the selection result is
          # ready for it.
          current_ctx.value = {}

          selections_on_type = current_ctx.irep_node.typed_children[current_type]

          selections_on_type.each do |name, child_irep_node|
            field_ctx = current_ctx.spawn_child(
              key: name,
              object: object,
              irep_node: child_irep_node,
            )

            field_result = resolve_field(
              object,
              field_ctx
            )

            if field_result.is_a?(Skip)
              next
            end

            if mutation
              GraphQL::Execution::Lazy.resolve(field_ctx)
            end


            # If the last subselection caused a null to propagate to _this_ selection,
            # then we may as well quit executing fields because they
            # won't be in the response
            if current_ctx.invalid_null?
              break
            else
              current_ctx.value[name] = field_ctx
            end
          end

          current_ctx.value
        end

        def resolve_field(object, field_ctx)
          query = field_ctx.query
          irep_node = field_ctx.irep_node
          parent_type = irep_node.owner_type
          field = field_ctx.field

          raw_value = begin
            begin
              arguments = query.arguments_for(irep_node, field)
              field_ctx.trace("execute_field", { context: field_ctx }) do
                field_ctx.schema.middleware.invoke([parent_type, object, field, arguments, field_ctx])
              end
            rescue GraphQL::UnauthorizedFieldError => err
              err.field ||= field
              field_ctx.schema.unauthorized_field(err)
            rescue GraphQL::UnauthorizedError => err
              field_ctx.schema.unauthorized_object(err)
            end
          rescue GraphQL::ExecutionError => err
            err
          end

          if field_ctx.schema.lazy?(raw_value)
            field_ctx.value = Execution::Lazy.new {
              inner_value = field_ctx.trace("execute_field_lazy", {context: field_ctx}) {
                begin
                  begin
                    field_ctx.field.lazy_resolve(raw_value, arguments, field_ctx)
                  rescue GraphQL::UnauthorizedError => err
                    field_ctx.schema.unauthorized_object(err)
                  end
                rescue GraphQL::ExecutionError => err
                  err
                end
              }
              continue_or_wait(inner_value, field_ctx.type, field_ctx)
            }
          else
            continue_or_wait(raw_value, field_ctx.type, field_ctx)
          end
        end

        # If the returned object is lazy (unfinished),
        # assign the lazy object to `.value=` so we can resolve it later.
        # When we resolve it later, reassign it to `.value=` so that
        # the finished value replaces the unfinished one.
        #
        # If the returned object is finished, continue to coerce
        # and resolve child fields
        def continue_or_wait(raw_value, field_type, field_ctx)
          if field_ctx.schema.lazy?(raw_value)
            field_ctx.value = Execution::Lazy.new {
              inner_value = begin
                  begin
                    field_ctx.schema.sync_lazy(raw_value)
                  rescue GraphQL::UnauthorizedError => err
                    field_ctx.schema.unauthorized_object(err)
                  end
                rescue GraphQL::ExecutionError => err
                  err
                end

              field_ctx.value = continue_or_wait(inner_value, field_type, field_ctx)
            }
          else
            field_ctx.value = continue_resolve_field(raw_value, field_type, field_ctx)
          end
        end

        def continue_resolve_field(raw_value, field_type, field_ctx)
          if field_ctx.parent.invalid_null?
            return nil
          end
          query = field_ctx.query

          case raw_value
          when GraphQL::ExecutionError
            raw_value.ast_node ||= field_ctx.ast_node
            raw_value.path = field_ctx.path
            query.context.errors.push(raw_value)
          when Array
            if field_type.non_null?
              # List type errors are handled above, this is for the case of fields returning an array of errors
              list_errors = raw_value.each_with_index.select { |value, _| value.is_a?(GraphQL::ExecutionError) }
              if list_errors.any?
                list_errors.each do |error, index|
                  error.ast_node = field_ctx.ast_node
                  error.path = field_ctx.path + (field_ctx.type.list? ? [index] : [])
                  query.context.errors.push(error)
                end
              end
            end
          end

          resolve_value(
            raw_value,
            field_type,
            field_ctx,
          )
        end

        def resolve_value(value, field_type, field_ctx)
          field_defn = field_ctx.field

          if value.nil?
            if field_type.kind.non_null?
              parent_type = field_ctx.irep_node.owner_type
              type_error = GraphQL::InvalidNullError.new(parent_type, field_defn, value)
              field_ctx.schema.type_error(type_error, field_ctx)
              PROPAGATE_NULL
            else
              nil
            end
          elsif value.is_a?(GraphQL::ExecutionError)
            if field_type.kind.non_null?
              PROPAGATE_NULL
            else
              nil
            end
          elsif value.is_a?(Array) && value.any? && value.all? {|v| v.is_a?(GraphQL::ExecutionError)}
            if field_type.kind.non_null?
              PROPAGATE_NULL
            else
              nil
            end
          elsif value.is_a?(Skip)
            field_ctx.value = value
          else
            case field_type.kind
            when GraphQL::TypeKinds::SCALAR, GraphQL::TypeKinds::ENUM
              field_type.coerce_result(value, field_ctx)
            when GraphQL::TypeKinds::LIST
              inner_type = field_type.of_type
              i = 0
              result = []
              field_ctx.value = result

              value.each do |inner_value|
                inner_ctx = field_ctx.spawn_child(
                  key: i,
                  object: inner_value,
                  irep_node: field_ctx.irep_node,
                )

                inner_result = continue_or_wait(
                  inner_value,
                  inner_type,
                  inner_ctx,
                )

                return PROPAGATE_NULL if inner_result == PROPAGATE_NULL

                result << inner_ctx
                i += 1
              end

              result
            when GraphQL::TypeKinds::NON_NULL
              inner_type = field_type.of_type
              resolve_value(
                value,
                inner_type,
                field_ctx,
              )
            when GraphQL::TypeKinds::OBJECT
              resolve_selection(
                value,
                field_type,
                field_ctx
              )
            when GraphQL::TypeKinds::UNION, GraphQL::TypeKinds::INTERFACE
              query = field_ctx.query
              resolved_type_or_lazy = field_type.resolve_type(value, field_ctx)
              query.schema.after_lazy(resolved_type_or_lazy) do |resolved_type|
                possible_types = query.possible_types(field_type)

                if !possible_types.include?(resolved_type)
                  parent_type = field_ctx.irep_node.owner_type
                  type_error = GraphQL::UnresolvedTypeError.new(value, field_defn, parent_type, resolved_type, possible_types)
                  field_ctx.schema.type_error(type_error, field_ctx)
                  PROPAGATE_NULL
                else
                  resolve_value(
                    value,
                    resolved_type,
                    field_ctx,
                  )
                end
              end
            else
              raise("Unknown type kind: #{field_type.kind}")
            end
          end
        end
      end

      include ExecutionFunctions

      # A `.call`-able suitable to be the last step in a middleware chain
      module FieldResolveStep
        # Execute the field's resolve method
        def self.call(_parent_type, parent_object, field_definition, field_args, context, _next = nil)
          field_definition.resolve(parent_object, field_args, context)
        end
      end
    end
  end
end
