const UserAuditLogHandler = require('../../../../app/src/Features/User/UserAuditLogHandler')
const EmailHandler = require('../../../../app/src/Features/Email/EmailHandler')
const EmailOptionsHelper = require('../../../../app/src/Features/Email/EmailOptionsHelper')
const Errors = require('../Errors/Errors')
const _ = require('lodash')
const logger = require('@overleaf/logger')
const settings = require('@overleaf/settings')
const { User } = require('../../../../app/src/models/User')
const { callbackify } = require('@overleaf/promise-utils')
const OError = require('@overleaf/o-error')

const oauthProviders = settings.oauthProviders || {}

async function getUser(providerId, externalUserId) {
  if (providerId == null || externalUserId == null) {
    throw new OError('invalid SSO arguments', {
      externalUserId,
      providerId,
    })
  }

  const query = _getUserQuery(providerId, externalUserId)
  const user = await User.findOne(query).exec()
  if (!user) {
    throw new Errors.ThirdPartyUserNotFoundError()
  }
  return user
}

async function login(providerId, externalUserId, externalData) {
  const user = await ThirdPartyIdentityManager.promises.getUser(
    providerId,
    externalUserId
  )
  if (!externalData) {
    return user
  }
  const query = _getUserQuery(providerId, externalUserId)
  const update = _thirdPartyIdentifierUpdate(
    user,
    providerId,
    externalUserId,
    externalData
  )
  return await User.findOneAndUpdate(query, update, { new: true }).exec()
}

async function link(
  userId,
  providerId,
  externalUserId,
  externalData,
  auditLog,
  retry
) {
  const accountLinked = true
  if (!oauthProviders[providerId]) {
    throw new Error('Not a valid provider')
  }

  await UserAuditLogHandler.promises.addEntry(
    userId,
    'link-sso',
    auditLog.initiatorId,
    auditLog.ipAddress,
    {
      providerId,
    }
  )

  const query = {
    _id: userId,
    'thirdPartyIdentifiers.providerId': {
      $ne: providerId,
    },
  }
  const update = {
    $push: {
      thirdPartyIdentifiers: {
        externalUserId,
        externalData,
        providerId,
      },
    },
  }
  // add new tpi only if an entry for the provider does not exist
  // projection includes thirdPartyIdentifiers for tests
  let res
  try {
    res = await User.findOneAndUpdate(query, update, { new: 1 }).exec()
  } catch (err) {
    if (err.code === 11000) {
      throw new Errors.ThirdPartyIdentityExistsError({
        info: { externalUserId },
      })
    }
    throw err
  }

  if (res) {
    _sendSecurityAlert(accountLinked, providerId, res, userId)
    return res
  }

  if (retry) {
    // if already retried then throw error
    throw new Error('update failed')
  }

  // attempt to clear existing entry then retry
  await ThirdPartyIdentityManager.promises.unlink(userId, providerId, auditLog)
  return await ThirdPartyIdentityManager.promises.link(
    userId,
    providerId,
    externalUserId,
    externalData,
    auditLog,
    true
  )
}

async function unlink(userId, providerId, auditLog) {
  const accountLinked = false
  if (!oauthProviders[providerId]) {
    throw new Error('Not a valid provider')
  }

  await UserAuditLogHandler.promises.addEntry(
    userId,
    'unlink-sso',
    auditLog.initiatorId,
    auditLog.ipAddress,
    {
      ...(auditLog.extraInfo || {}),
      providerId,
    }
  )

  const query = {
    _id: userId,
  }
  const update = {
    $pull: {
      thirdPartyIdentifiers: {
        providerId,
      },
    },
  }
  // projection includes thirdPartyIdentifiers for tests
  const res = await User.findOneAndUpdate(query, update, { new: 1 })
  if (!res) {
    throw new Error('update failed')
  }
  _sendSecurityAlert(accountLinked, providerId, res, userId)
  return res
}

function _getUserQuery(providerId, externalUserId) {
  externalUserId = externalUserId.toString()
  providerId = providerId.toString()
  const query = {
    'thirdPartyIdentifiers.externalUserId': externalUserId,
    'thirdPartyIdentifiers.providerId': providerId,
  }
  return query
}

function _sendSecurityAlert(accountLinked, providerId, user, userId) {
  const providerName = oauthProviders[providerId].name
  const emailOptions = EmailOptionsHelper.linkOrUnlink(
    accountLinked,
    providerName,
    user.email
  )
  EmailHandler.promises
    .sendEmail('securityAlert', emailOptions)
    .catch(error => {
      logger.error(
        { err: error, userId },
        `could not send security alert email when ${emailOptions.action.toLowerCase()}`
      )
    })
}

function _thirdPartyIdentifierUpdate(
  user,
  providerId,
  externalUserId,
  externalData
) {
  providerId = providerId.toString()
  // get third party identifier object from array
  const thirdPartyIdentifier = user.thirdPartyIdentifiers.find(
    tpi =>
      tpi.externalUserId === externalUserId && tpi.providerId === providerId
  )
  // do recursive merge of new data over existing data
  _.merge(thirdPartyIdentifier.externalData, externalData)
  const update = { 'thirdPartyIdentifiers.$': thirdPartyIdentifier }
  return update
}

const ThirdPartyIdentityManager = {
  getUser: callbackify(getUser),
  login: callbackify(login),
  link: callbackify(link),
  unlink: callbackify(unlink),
}

ThirdPartyIdentityManager.promises = {
  getUser,
  login,
  link,
  unlink,
}

module.exports = ThirdPartyIdentityManager
