module GraphQL
  module Relay
    # Define a Relay mutation:
    #   - give it a name (used for derived inputs & outputs)
    #   - declare its inputs
    #   - declare its outputs
    #   - declare the mutation procedure
    #
    # `resolve` should return a hash with a key for each of the `return_field`s
    #
    # Inputs may also contain a `clientMutationId`
    #
    # @example Updating the name of an item
    #   UpdateNameMutation = GraphQL::Relay::Mutation.define do
    #     name "UpdateName"
    #
    #     input_field :name, !types.String
    #     input_field :itemId, !types.ID
    #
    #     return_field :item, ItemType
    #
    #     resolve ->(inputs, ctx) {
    #       item = Item.find_by_id(inputs[:id])
    #       item.update(name: inputs[:name])
    #       {item: item}
    #     }
    #   end
    #
    #   MutationType = GraphQL::ObjectType.define do
    #     # The mutation object exposes a field:
    #     field :updateName, field: UpdateNameMutation.field
    #   end
    #
    #   # Then query it:
    #   query_string = %|
    #     mutation updateName {
    #       updateName(input: {itemId: 1, name: "new name", clientMutationId: "1234"}) {
    #         item { name }
    #         clientMutationId
    #     }|
    #
    #    GraphQL::Query.new(MySchema, query_string).result
    #    # {"data" => {
    #    #   "updateName" => {
    #    #     "item" => { "name" => "new name"},
    #    #     "clientMutationId" => "1234"
    #    #   }
    #    # }}
    #
    class Mutation
      include GraphQL::Define::InstanceDefinable
      accepts_definitions(
        :name, :description, :resolve,
        :return_type,
        input_field: GraphQL::Define::AssignArgument,
        return_field: GraphQL::Define::AssignObjectField,
      )
      lazy_defined_attr_accessor :name, :description
      lazy_defined_attr_accessor :fields, :arguments, :return_type

      # For backwards compat, but do we need this separate API?
      alias :return_fields :fields
      alias :input_fields :arguments

      def initialize
        @fields = {}
        @arguments = {}
        @has_generated_return_type = false
      end

      def has_generated_return_type?
        # Trigger the generation of the return type, if it is dynamically generated:
        return_type
        @has_generated_return_type
      end

      def resolve=(new_resolve_proc)
        ensure_defined
        @resolve_proc = MutationResolve.new(self, new_resolve_proc, wrap_result: has_generated_return_type?)
      end

      def field
        @field ||= begin
          ensure_defined
          relay_mutation = self
          field_resolve_proc = @resolve_proc
          GraphQL::Field.define do
            type(relay_mutation.return_type)
            description(relay_mutation.description)
            argument :input, !relay_mutation.input_type
            resolve(field_resolve_proc)
            mutation(relay_mutation)
          end
        end
      end

      def return_type
        ensure_defined
        @return_type ||= begin
          @has_generated_return_type = true
          relay_mutation = self
          GraphQL::ObjectType.define do
            name("#{relay_mutation.name}Payload")
            description("Autogenerated return type of #{relay_mutation.name}")
            field :clientMutationId, types.String, "A unique identifier for the client performing the mutation.", property: :client_mutation_id
            relay_mutation.return_fields.each do |name, field_obj|
              field name, field: field_obj
            end
            mutation(relay_mutation)
          end
        end
      end

      def input_type
        @input_type ||= begin
          ensure_defined
          relay_mutation = self
          GraphQL::InputObjectType.define do
            name("#{relay_mutation.name}Input")
            description("Autogenerated input type of #{relay_mutation.name}")
            input_field :clientMutationId, types.String, "A unique identifier for the client performing the mutation."
            relay_mutation.input_fields.each do |input_field_name, field_obj|
              input_field input_field_name, field_obj.type, field_obj.description, default_value: field_obj.default_value
            end
            mutation(relay_mutation)
          end
        end
      end

      def result_class
        @result_class ||= begin
          ensure_defined
          Result.define_subclass(self)
        end
      end

      private

      def get_arity(callable)
        case callable
        when Proc
          callable.arity
        else
          callable.method(:call).arity
        end
      end

      # Use this when the mutation's return type was generated from `return_field`s.
      # It delegates field lookups to the hash returned from `resolve`.
      class Result
        attr_reader :client_mutation_id
        def initialize(client_mutation_id:, result:)
          @client_mutation_id = client_mutation_id
          result.each do |key, value|
            self.public_send("#{key}=", value)
          end
        end

        class << self
          attr_accessor :mutation
        end

        def self.define_subclass(mutation_defn)
          subclass = Class.new(self) do
            attr_accessor(*mutation_defn.return_type.all_fields.map(&:name))
            self.mutation = mutation_defn
          end
          subclass
        end
      end

      class MutationResolve
        def initialize(mutation, resolve, wrap_result:)
          @mutation = mutation
          @resolve = resolve
          @wrap_result = wrap_result
        end

        def call(obj, args, ctx)
          mutation_result = @resolve.call(obj, args[:input], ctx)
          if @wrap_result
            @mutation.result_class.new(client_mutation_id: args[:input][:clientMutationId], result: mutation_result)
          else
            mutation_result
          end
        end
      end
    end
  end
end