const { SchemaDirectiveVisitor } = require('graphql-tools')
const { defaultFieldResolver } = require('graphql')
const _ = require('lodash')

class AuthDirective extends SchemaDirectiveVisitor {
  visitObject(type) {
    this.ensureFieldsWrapped(type)
    type._requiredAuthScopes = this.args.requires
  }
  // Visitor methods for nested types like fields and arguments
  // also receive a details object that provides information about
  // the parent and grandparent types.
  visitFieldDefinition(field, details) {
    this.ensureFieldsWrapped(details.objectType)
    field._requiredAuthScopes = this.args.requires
  }

  visitArgumentDefinition(argument, details) {
    this.ensureFieldsWrapped(details.objectType)
    argument._requiredAuthScopes = this.args.requires
  }

  ensureFieldsWrapped(objectType) {
    // Mark the GraphQLObjectType object to avoid re-wrapping:
    if (objectType._authFieldsWrapped) return
    objectType._authFieldsWrapped = true

    const fields = objectType.getFields()

    Object.keys(fields).forEach(fieldName => {
      const field = fields[fieldName]
      const { resolve = defaultFieldResolver } = field
      field.resolve = async function (...args) {
        // Get the required scopes from the field first, falling back
        // to the objectType if no scopes is required by the field:
        const requiredScopes = field._requiredAuthScopes || objectType._requiredAuthScopes

        if (!requiredScopes) {
          return resolve.apply(this, args)
        }

        const context = args[2]
        if (!context.req.user) {
          throw new Error('Unauthorized')
        }

        if (!_.some(context.req.user.permissions, pm => _.includes(requiredScopes, pm))) {
          throw new Error('Forbidden')
        }

        return resolve.apply(this, args)
      }
    })
  }
}

module.exports = AuthDirective
