import { IResolvers } from 'apollo-server-express'

import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { promisify } from 'util'
import randombytes from 'randombytes'

if (!process.env.APP_SECRET) {
  throw Error('App secret (APP_SECRET) must be set in the .env file')
}

export const resolvers: IResolvers = {
  Query: {
    /* Public functions without login/permission check */

    /* Functions that require a login */
    currentUser: (parent, args, context, info) => {
      checkPermission(context)
      return context.db.query.user(
        {
          where: { id: context.req.userId }
        },
        info
      )
    },

    /* Functions that require higher permissions */
    users: async (parent, args, context, info) => {
      checkPermission(context, 'ADMIN')
      return context.db.query.users(null, info)
    }
  },

  Mutation: {
    /* Public functions without login/permission check */
    userSignup: async (parent, { email, password, ...args }, ctx, info) => {
      const lowercaseEmail = email.toLowerCase()
      const encryptedPassword = await bcrypt.hash(password, 10)
      const user = await ctx.db.mutation.createUser(
        {
          data: {
            ...args,
            email: lowercaseEmail,
            password: encryptedPassword
          }
        },
        info
      )
      const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET || '')
      ctx.res.cookie('token', token, {
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000
      })
      return user
    },
    userLogin: async (parent, { email, password }, context, info) => {
      const user = await context.db.query.user({ where: { email } })
      if (!user) throw new Error('Email address not found')
      const valid = await bcrypt.compare(password, user.password)
      if (!valid) throw new Error('Invalid password')
      const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET || '')
      context.res.cookie(
        'token',
        token,
        {
          httpOnly: true,
          maxAge: 7 * 24 * 3600 * 1000
        },
        info
      )
      return user
    },
    userLogout: async (parent, args, context, info) => {
      context.res.clearCookie('token')
      return 'Logged out.'
    },
    requestReset: async (parent, { email }, context, info) => {
      const user = await context.db.query.user({ where: { email } })
      if (!user) throw Error('Email address not found')
      const randombytesPromisified = promisify(randombytes)
      const resetToken = (await randombytesPromisified(20)).toString('hex')
      const resetTokenExpiry = Date.now() + 3600000 // 1 hour from now
      await context.db.mutation.updateUser({
        where: { email },
        data: { resetToken, resetTokenExpiry }
      })
      /* await transport.sendMail({
        from: 'wes@wesbos.com',
        to: user.email,
        subject: 'Your Password Reset Token',
        html: emailTemplate(`Your Password Reset Token is here!
        \n\n
        <a href="${process.env
            .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`)
      }) */
      return 'Success.'
    },
    resetPassword: async (parent, { token, password }, context, info) => {
      const [user] = await context.db.query.users({
        where: {
          resetToken: token,
          resetTokenExpiry_gte: Date.now() - 3600000
        }
      })
      if (!user) {
        throw Error('Token invalid or expired.')
      }
      const encryptedPassword = await bcrypt.hash(password, 10)
      const updatedUser = await context.db.mutation.updateUser({
        where: { email: user.email },
        data: {
          password: encryptedPassword,
          resetToken: null,
          resetTokenExpiry: null
        }
      })
      const cookieToken = jwt.sign(
        { userId: updatedUser.id },
        process.env.APP_SECRET || ''
      )
      context.res.cookie('token', cookieToken, {
        httpOnly: true,
        maxAge: 1000 * 60 * 60 * 24 * 365
      })
      return updatedUser
    },

    /* Functions that require higher permissions */
    createUser: async (parent, { email, password, ...args }, context, info) => {
      checkPermission(context, 'ADMIN')
      const lowercaseEmail = email.toLowerCase()
      const encryptedPassword = await bcrypt.hash(password, 10)
      return context.db.mutation.createUser(
        {
          data: {
            ...args,
            email: lowercaseEmail,
            password: encryptedPassword
          }
        },
        info
      )
    },
    updateUser: async (parent, { email, password, ...args }, context, info) => {
      checkPermission(context, 'ADMIN')
      const updatedData = { ...args }
      if (!!email) updatedData.email = email.toLowerCase()
      if (!!password) updatedData.password = await bcrypt.hash(password, 10)
      return context.db.mutation.updateUser(
        {
          data: {
            ...updatedData
          },
          where: { email }
        },
        info
      )
    },
    deleteUser: (parent, { email }, context, info) => {
      checkPermission(context, 'ADMIN')
      return context.db.mutation.deleteUser({ where: { email } })
    }
  }
}

/**
 * Checks whether the current user has the required permissions. Raises an error if he doesn't.
 * @param context - Resolver context. Needs to have 'userId' and 'user' populated in req
 * @param permission - String or list of strings with required permissions
 */
export function checkPermission(context: any, permission?: string | string[]) {
  if (!context.req.userId) throw new Error('Login required.')
  if (typeof permission === 'string') {
    if (!context.req.user.permissions.includes(permission))
      throw Error(
        `No permission. This operation requires ${permission} privilege.`
      )
  } else if (Array.isArray(permission)) {
    if (
      !context.req.user.permissions.reduce(
        (state: boolean, userPermission: string) =>
          state || permission.includes(userPermission),
        false
      )
    )
      throw Error(
        `No permission. This operation requires ${permission.join(
          ' or '
        )} privilege.`
      )
  }
}
