import { wasChangePasswordPayload } from '../../types/server-cleaners'

import { decrypt, encrypt } from '../../util/crypto/crypto'

import { makeSnrp, scrypt, userIdSnrp } from '../scrypt/scrypt-selectors'
import { applyKit, serverLogin, syncLogin } from './login'
import { hashUsername } from './login-selectors'
import { saveStash } from './login-stash'


const passwordAuthSnrp = userIdSnrp

function makeHashInput(username, password) {
  return username + password
}

/**
 * Extracts the loginKey from the login stash.
 */
async function loginPasswordOffline(
  ai,
  stashTree,
  password,
  opts
) {
  const { now = new Date() } = opts

  const { passwordBox, passwordKeySnrp, username } = stashTree
  if (passwordBox == null || passwordKeySnrp == null || username == null) {
    throw new Error('Missing data for offline password login')
  }
  const up = makeHashInput(username, password)
  const passwordKey = await scrypt(ai, up, passwordKeySnrp)
  const sessionKey = {
    loginId: stashTree.loginId,
    loginKey: decrypt(passwordBox, passwordKey)
  }

  // Save the date:
  stashTree.lastLogin = now
  saveStash(ai, stashTree).catch(() => {})

  // Since we logged in offline, update the stash in the background:
  // TODO: If the user provides an OTP token, add that to the stash.
  const { log } = ai.props
  syncLogin(ai, sessionKey).catch(error => log.error(error))

  return sessionKey
}

/**
 * Fetches the loginKey from the server.
 */
async function loginPasswordOnline(
  ai,
  stashTree,
  password,
  opts
) {
  const { username } = stashTree
  if (username == null) throw new Error('Password login requires a username')

  // Request:
  const up = makeHashInput(username, password)
  const [userId, passwordAuth] = await Promise.all([
    hashUsername(ai, username),
    scrypt(ai, up, passwordAuthSnrp)
  ])
  const request = {
    userId,
    passwordAuth
  }
  return await serverLogin(
    ai,
    stashTree,
    stashTree,
    opts,
    request,
    async reply => {
      const { passwordBox, passwordKeySnrp } = reply
      if (
        passwordBox == null ||
        passwordBox === true ||
        passwordKeySnrp == null
      ) {
        throw new Error('Missing data for online password login')
      }
      const passwordKey = await scrypt(ai, up, passwordKeySnrp)
      return decrypt(passwordBox, passwordKey)
    }
  )
}

/**
 * Logs a user in using a password.
 * @param username string
 * @param password string
 * @return A `Promise` for the new root login.
 */
export async function loginPassword(
  ai,
  stashTree,
  password,
  opts
) {
  return await loginPasswordOffline(ai, stashTree, password, opts).catch(() =>
    loginPasswordOnline(ai, stashTree, password, opts)
  )
}

export async function changePassword(
  ai,
  accountId,
  password
) {
  const accountState = ai.props.state.accounts[accountId]
  const { loginTree, sessionKey } = accountState
  const { username } = accountState.stashTree
  if (username == null) throw new Error('Password login requires a username')

  const kit = await makePasswordKit(ai, loginTree, username, password)
  await applyKit(ai, sessionKey, kit)
}

/**
 * Returns true if the given password is correct.
 */
export async function checkPassword(
  ai,
  login,
  password
) {
  const { username, passwordAuth } = login
  if (username == null || passwordAuth == null) return false

  // Derive passwordAuth:
  const up = makeHashInput(username, password)
  const newPasswordAuth = await scrypt(ai, up, passwordAuthSnrp)

  // Compare what we derived with what we have:
  for (let i = 0; i < passwordAuth.length; ++i) {
    if (newPasswordAuth[i] !== passwordAuth[i]) return false
  }

  return true
}

export async function deletePassword(
  ai,
  accountId
) {
  const { loginTree, sessionKey } = ai.props.state.accounts[accountId]

  const kit = {
    loginId: loginTree.loginId,
    server: undefined,
    serverMethod: 'DELETE',
    serverPath: '/v2/login/password',
    stash: {
      passwordAuthSnrp: undefined,
      passwordBox: undefined,
      passwordKeySnrp: undefined
    }
  }
  // Only remove `passwordAuth` if we have another way to get in:
  if (loginTree.loginAuth != null) {
    kit.stash.passwordAuthBox = undefined
  }
  await applyKit(ai, sessionKey, kit)
}

/**
 * Creates the data needed to attach a password to a login.
 */
export async function makePasswordKit(
  ai,
  login,
  username,
  password
) {
  const up = makeHashInput(username, password)
  const { io } = ai.props

  const [{ passwordKeySnrp, passwordBox }, { passwordAuth, passwordAuthBox }] =
    await Promise.all([
      // The loginKey, encrypted by the passwordKey:
      makeSnrp(ai).then(async passwordKeySnrp => {
        const passwordKey = await scrypt(ai, up, passwordKeySnrp)
        const passwordBox = encrypt(io, login.loginKey, passwordKey)
        return { passwordKeySnrp, passwordBox }
      }),

      // The passwordAuth, encrypted by the loginKey:
      scrypt(ai, up, passwordAuthSnrp).then(passwordAuth => {
        const passwordAuthBox = encrypt(io, passwordAuth, login.loginKey)
        return { passwordAuth, passwordAuthBox }
      })
    ])

  return {
    loginId: login.loginId,
    server: wasChangePasswordPayload({
      passwordAuth,
      passwordAuthSnrp, // TODO: Use this on the other side
      passwordKeySnrp,
      passwordBox,
      passwordAuthBox
    }),
    serverPath: '/v2/login/password',
    stash: {
      passwordKeySnrp,
      passwordBox,
      passwordAuthBox
    }
  }
}
