# frozen_string_literal: true

module GraphqlDevise
  class SchemaPlugin
    # NOTE: Based on GQL-Ruby docs  https://graphql-ruby.org/schema/introspection.html
    INTROSPECTION_FIELDS = ['__schema', '__type', '__typename']
    DEFAULT_NOT_AUTHENTICATED = ->(field) { raise AuthenticationError, "#{field} field requires authentication" }

    def initialize(query: nil, mutation: nil, authenticate_default: true, public_introspection: !Rails.env.production?, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
      @query                = query
      @mutation             = mutation
      @resource_loaders     = resource_loaders
      @authenticate_default = authenticate_default
      @public_introspection = public_introspection
      @unauthenticated_proc = unauthenticated_proc

      # Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
      load_fields
    end

    def use(schema_definition)
      if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.3')
        schema_definition.trace_with(
          FieldAuthTracer,
          authenticate_default: @authenticate_default,
          public_introspection: public_introspection,
          unauthenticated_proc: @unauthenticated_proc
        )
      else
        schema_definition.tracer(self)
      end
    end

    def trace(event, trace_data)
      # Authenticate only root level queries
      return yield unless event == 'execute_field' && path(trace_data).count == 1

      field         = traced_field(trace_data)
      auth_required = authenticate_option(field, trace_data)
      context       = context_from_data(trace_data)

      if auth_required && !(public_introspection && introspection_field?(field))
        raise_on_missing_resource(context, field, auth_required)
      end

      yield
    end

    private

    attr_reader :public_introspection

    def raise_on_missing_resource(context, field, auth_required)
      @unauthenticated_proc.call(field.name) if context[:current_resource].blank?

      if auth_required.respond_to?(:call) && !auth_required.call(context[:current_resource])
        @unauthenticated_proc.call(field.name)
      end
    end

    def context_from_data(trace_data)
      query = if trace_data[:context]
        trace_data[:context].query
      else
        trace_data[:query]
      end

      query.context
    end

    def path(trace_data)
      if trace_data[:context]
        trace_data[:context].path
      else
        trace_data[:path]
      end
    end

    def traced_field(trace_data)
      if trace_data[:context]
        trace_data[:context].field
      else
        trace_data[:field]
      end
    end

    def authenticate_option(field, trace_data)
      auth_required = if trace_data[:context]
        field.metadata[:authenticate]
      else
        if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('2.0')
          # authenticate will only be defined if "field_class GraphqlDevise::Types::BaseField" is added to the type
          # returning nil here will use the default value used when mounting the plugin
          field.try(:authenticate)
        elsif Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.13.1')
          field.graphql_definition(silence_deprecation_warning: true).metadata[:authenticate]
        else
          field.graphql_definition.metadata[:authenticate]
        end
      end

      auth_required.nil? ? @authenticate_default : auth_required
    end

    def load_fields
      @resource_loaders.each do |resource_loader|
        raise ::GraphqlDevise::Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(ResourceLoader)

        resource_loader.call(@query, @mutation)
      end
    end

    def introspection_field?(field)
      INTROSPECTION_FIELDS.include?(field.name)
    end
  end
end
