# frozen_string_literal: true

module Gitlab
  module Graphql
    module Authorize
      class AuthorizeFieldService
        def initialize(field)
          @field = field
          @old_resolve_proc = @field.resolve_proc
        end

        def authorizations?
          authorizations.present?
        end

        def authorized_resolve
          proc do |parent_typed_object, args, ctx|
            resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
            authorizing_object = authorize_against(parent_typed_object, resolved_type)

            filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
          end
        end

        private

        def authorizations
          @authorizations ||= (type_authorizations + field_authorizations).uniq
        end

        # Returns any authorize metadata from the return type of @field
        def type_authorizations
          type = @field.type

          # When the return type of @field is a collection, find the singular type
          if @field.connection?
            type = node_type_for_relay_connection(type)
          elsif type.list?
            type = node_type_for_basic_connection(type)
          end

          Array.wrap(type.metadata[:authorize])
        end

        # Returns any authorize metadata from @field
        def field_authorizations
          Array.wrap(@field.metadata[:authorize])
        end

        def authorize_against(parent_typed_object, resolved_type)
          if scalar_type?
            # The field is a built-in/scalar type, or a list of scalars
            # authorize using the parent's object
            parent_typed_object.object
          elsif resolved_type.respond_to?(:object)
            # The field is a type representing a single object, we'll authorize
            # against the object directly
            resolved_type.object
          elsif @field.connection? || resolved_type.is_a?(Array)
            # The field is a connection or a list of non-built-in types, we'll
            # authorize each element when rendering
            nil
          else
            # Resolved type is a single object that might not be loaded yet by
            # the batchloader, we'll authorize that
            resolved_type
          end
        end

        def filter_allowed(current_user, resolved_type, authorizing_object)
          if authorizing_object
            # Authorizing fields representing scalars, or a simple field with an object
            resolved_type if allowed_access?(current_user, authorizing_object)
          elsif @field.connection?
            # A connection with pagination, modify the visible nodes in on the
            # connection type in place
            resolved_type.edge_nodes.to_a.keep_if { |node| allowed_access?(current_user, node) }
            resolved_type
          elsif resolved_type.is_a? Array
            # A simple list of rendered types  each object being an object to authorize
            resolved_type.select do |single_object_type|
              allowed_access?(current_user, single_object_type.object)
            end
          elsif resolved_type.nil?
            # We're not rendering anything, for example when a record was not found
            # no need to do anything
          else
            raise "Can't authorize #{@field}"
          end
        end

        def allowed_access?(current_user, object)
          object = object.sync if object.respond_to?(:sync)

          authorizations.all? do |ability|
            Ability.allowed?(current_user, ability, object)
          end
        end

        # Returns the singular type for relay connections.
        # This will be the type class of edges.node
        def node_type_for_relay_connection(type)
          type.unwrap.get_field('edges').type.unwrap.get_field('node').type
        end

        # Returns the singular type for basic connections, for example `[Types::ProjectType]`
        def node_type_for_basic_connection(type)
          type.unwrap
        end

        def scalar_type?
          node_type_for_basic_connection(@field.type).kind.scalar?
        end
      end
    end
  end
end
