import crypto from 'node:crypto'
import V1Api from '../V1/V1Api.mjs'
import Features from '../../infrastructure/Features.mjs'
import { callbackify } from 'node:util'
import OError from '@overleaf/o-error'

// (From Overleaf `random_token.rb`)
//   Letters (not numbers! see generate_token) used in tokens. They're all
//   consonants, to avoid embarassing words (I can't think of any that use only
//   a y), and lower case "l" is omitted, because in many fonts it is
//   indistinguishable from an upper case "I" (and sometimes even the number 1).
const TOKEN_LOWERCASE_ALPHA = 'bcdfghjkmnpqrstvwxyz'
const TOKEN_NUMERICS = '123456789'
const TOKEN_ALPHANUMERICS =
  TOKEN_LOWERCASE_ALPHA + TOKEN_LOWERCASE_ALPHA.toUpperCase() + TOKEN_NUMERICS

// This module mirrors the token generation in Overleaf (`random_token.rb`),
// for the purposes of implementing token-based project access, like the
// 'unlisted-projects' feature in Overleaf

function _randomString(length, alphabet) {
  const result = crypto
    .randomBytes(length)
    .toJSON()
    .data.map(b => alphabet[b % alphabet.length])
    .join('')
  return result
}

// Generate a 12-char token with only characters from TOKEN_LOWERCASE_ALPHA,
// suitable for use as a read-only token for a project
function readOnlyToken() {
  return _randomString(12, TOKEN_LOWERCASE_ALPHA)
}

// Generate a longer token, with a numeric prefix,
// suitable for use as a read-and-write token for a project
function readAndWriteToken() {
  const numerics = _randomString(10, TOKEN_NUMERICS)
  const token = _randomString(12, TOKEN_LOWERCASE_ALPHA)
  const fullToken = `${numerics}${token}`
  return { token: fullToken, numericPrefix: numerics }
}

function generateReferralId() {
  return _randomString(16, TOKEN_ALPHANUMERICS)
}

async function generateUniqueReadOnlyToken() {
  const retryOperation = async () => {
    const token = readOnlyToken()

    if (!Features.hasFeature('saas')) {
      return token
    }

    const { response, body } = await V1Api.promises.request({
      url: `/api/v1/overleaf/docs/read_token/${token}/exists`,
      json: true,
    })

    if (response.statusCode !== 200) {
      throw new OError('non-200 response from v1 read-token-exists api', {
        statusCode: response.statusCode,
      })
    }

    if (body.exists === true) {
      throw new OError('token already exists in v1', { token })
    }

    return token
  }

  const MAX_RETRIES = 10
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      return await retryOperation()
    } catch (err) {
      if (attempt === MAX_RETRIES) {
        throw err
      }
    }
  }
}

const TokenGenerator = {
  _randomString,
  readOnlyToken,
  readAndWriteToken,
  generateReferralId,
  generateUniqueReadOnlyToken: callbackify(generateUniqueReadOnlyToken),
}

TokenGenerator.promises = {
  generateUniqueReadOnlyToken,
}
export default TokenGenerator
