 function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }import { asMaybe } from 'cleaners'
import { base64 } from 'rfc4648'

import {
  asChallengeErrorPayload,
  asLoginResponseBody,
  wasLoginRequestBody
} from '../../types/server-cleaners'

import {
  ChallengeError,


  NetworkError,
  ObsoleteApiError,
  OtpError,
  PasswordError,
  UsernameError
} from '../../types/types'
import { hmacSha256 } from '../../util/crypto/hashes'
import { utf8 } from '../../util/encoding'
import { timeout } from '../../util/promise'


export function parseReply(json) {
  const clean = asLoginResponseBody(json)

  switch (clean.status_code) {
    case 0: // Success
      return clean.results

    case 2: // Account exists
      throw new UsernameError('Account already exists on server')

    case 3: // Account does not exist
      throw new UsernameError('Account does not exist on server')

    case 4: // Invalid password
    case 5: // Invalid answers
      throw new PasswordError(clean.results)

    case 6: // Invalid API key
      throw new Error('Invalid API key')

    case 8: // Invalid OTP
      throw new OtpError(clean.results)

    case 1000: // Endpoint obsolete
      throw new ObsoleteApiError()

    case 1: // Error
    case 7: // Pin expired
    case 9: // Invalid voucher
    case 10: // Conflicting change
    case 11: // Rate limiting
    default: {
      const results = asMaybe(asChallengeErrorPayload)(clean.results)
      if (results != null) {
        throw new ChallengeError(results)
      }
      throw new Error(`Server error: ${clean.message}`)
    }
  }
}

/**
 * Picks a random login server and makes a request.
 *
 * We don't use the normal async waterfall,
 * since we never want these requests to happen in parallel.
 * The first server needs to fully fail before we can try again,
 * or we risk having document update conflicts, duplicated keys,
 * redundant login notifications, or other corruption.
 */
export async function loginFetch(
  ai,
  method,
  path,
  body
) {
  const { loginServers } = ai.props.state.login

  // This will be out of range, but the modulo brings it back:
  const startIndex = Math.floor(Math.random() * 255)

  let response
  let lastError = new Error('No login servers available')
  for (let i = 0; i < loginServers.length; ++i) {
    try {
      const index = (startIndex + i) % loginServers.length
      response = await loginFetchInner(
        ai,
        loginServers[index],
        method,
        path,
        body
      )
      break
    } catch (error) {
      lastError = error
    }
  }
  if (response == null) throw lastError

  const { status } = response
  const json = await response.json().catch(() => {
    throw new Error(`Invalid reply JSON, HTTP status ${status}`)
  })
  return parseReply(json)
}

export function loginFetchInner(
  ai,
  serverUri,
  method,
  path,
  body
) {
  const { state, io, log } = ai.props
  const { apiKey, apiSecret } = state.login

  const bodyText =
    method === 'GET' || body == null
      ? undefined
      : JSON.stringify(wasLoginRequestBody(body))

  // API key:
  let authorization = `Token ${apiKey}`
  if (apiSecret != null) {
    const requestText = `${method}\n/api${path}\n${_nullishCoalesce(bodyText, () => ( ''))}`
    const hash = hmacSha256(utf8.parse(requestText), apiSecret)
    authorization = `HMAC ${apiKey} ${base64.stringify(hash)}`
  }

  const opts = {
    body: bodyText,
    method,
    headers: {
      'content-type': 'application/json',
      accept: 'application/json',
      authorization
    },
    corsBypass: 'never'
  }

  const start = Date.now()
  const fullUri = `${serverUri}/api${path}`
  return timeout(io.fetch(fullUri, opts), 30000).then(
    response => {
      // Log the results:
      const time = Date.now() - start
      log(`${method} ${fullUri} returned ${response.status} in ${time}ms`)

      if (response.status === 409) {
        log.crash(`Login API conflict error`, {
          path
        })
      }

      return response
    },
    networkError => {
      const time = Date.now() - start
      log.error(
        `${method} ${fullUri} failed in ${time}ms, ${String(networkError)}`
      )
      throw new NetworkError(`Could not reach the auth server: ${path}`)
    }
  )
}
