import { User } from '../../models/User.mjs'
import UserCreator from './UserCreator.mjs'
import UserGetter from './UserGetter.mjs'
import AuthenticationManager from '../Authentication/AuthenticationManager.mjs'
import Modules from '../../infrastructure/Modules.mjs'
import logger from '@overleaf/logger'
import crypto from 'node:crypto'
import EmailHandler from '../Email/EmailHandler.mjs'
import OneTimeTokenHandler from '../Security/OneTimeTokenHandler.mjs'
import settings from '@overleaf/settings'
import EmailHelper from '../Helpers/EmailHelper.mjs'
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
import OError from '@overleaf/o-error'

const UserRegistrationHandler = {
  _registrationRequestIsValid(body) {
    const invalidEmail = AuthenticationManager.validateEmail(body.email || '')
    const invalidPassword = AuthenticationManager.validatePassword(
      body.password || '',
      body.email
    )
    return !(invalidEmail || invalidPassword)
  },

  async _createNewUserIfRequired(user, userDetails) {
    if (!user) {
      userDetails.holdingAccount = false
      return await UserCreator.promises.createNewUser(
        {
          holdingAccount: false,
          email: userDetails.email,
          first_name: userDetails.first_name,
          last_name: userDetails.last_name,
          analyticsId: userDetails.analyticsId,
        },
        {}
      )
    }
    return user
  },

  /**
   * @param {Object} userDetails
   * @return {Promise<{ _id: import('mongodb-legacy').ObjectId }>}
   */
  async registerNewUser(userDetails) {
    const requestIsValid =
      UserRegistrationHandler._registrationRequestIsValid(userDetails)

    if (!requestIsValid) {
      throw new Error('request is not valid')
    }
    userDetails.email = EmailHelper.parseEmail(userDetails.email)

    let user = await UserGetter.promises.getUserByAnyEmail(userDetails.email)
    if (user && user.holdingAccount === false) {
      // We add userId to the error object so that the calling function can access
      // the id of the already existing user account.
      throw new OError('EmailAlreadyRegistered', { userId: user._id })
    }

    user = await UserRegistrationHandler._createNewUserIfRequired(
      user,
      userDetails
    )

    await User.updateOne(
      { _id: user._id },
      { $set: { holdingAccount: false } }
    ).exec()

    await AuthenticationManager.promises.setUserPassword(
      user,
      userDetails.password
    )

    if (userDetails.subscribeToNewsletter === 'true') {
      try {
        await Modules.promises.hooks.fire(
          'updateTopicSubscription',
          user._id,
          'newsletter',
          true
        )
      } catch (error) {
        logger.warn(
          { err: error, user },
          'Failed to subscribe user to newsletter'
        )
        throw error
      }
    }

    return user
  },

  async registerNewUserAndSendActivationEmail(email) {
    let user
    try {
      user = await UserRegistrationHandler.registerNewUser({
        email,
        password: crypto.randomBytes(32).toString('hex'),
      })
    } catch (error) {
      if (error.message === 'EmailAlreadyRegistered') {
        logger.debug({ email }, 'user already exists, resending welcome email')
        user = await UserGetter.promises.getUserByAnyEmail(email)
      } else {
        throw error
      }
    }

    const ONE_WEEK = 7 * 24 * 60 * 60 // seconds
    const token = await OneTimeTokenHandler.promises.getNewToken(
      'password',
      { user_id: user._id.toString(), email: user.email },
      { expiresIn: ONE_WEEK }
    )

    const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}`

    await EmailHandler.promises
      .sendEmail('registered', {
        to: user.email,
        setNewPasswordUrl,
      })
      .catch(error => {
        logger.warn({ err: error }, 'failed to send activation email')
      })

    return { user, setNewPasswordUrl }
  },
}

export default {
  registerNewUser: callbackify(UserRegistrationHandler.registerNewUser),
  registerNewUserAndSendActivationEmail: callbackifyMultiResult(
    UserRegistrationHandler.registerNewUserAndSendActivationEmail,
    ['user', 'setNewPasswordUrl']
  ),
  promises: UserRegistrationHandler,
}
