export type RetryPolicy = {
  initialBackoffSec: number
  retries: number
}

export const delay = (ms: number) => {
  return new Promise<void>((resolve) => setTimeout(() => resolve(), ms))
}

export async function withRetries<T>(q: PromiseLike<T>, policy?: RetryPolicy) {
  let err: Error | undefined
  let delaySec = policy?.initialBackoffSec ?? 5
  const maxRetries = policy?.retries ?? 5
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await q
    } catch (e) {
      err = e as Error
      if (i < maxRetries) {
        console.debug(`Error: ${err.message} - Retrying in ${delaySec}s.`)
        await delay(delaySec * 1000)
        delaySec *= 2
      }
    }
  }
  throw err
}

export const mapAsyncChunked = async <T, U>(
  items: T[],
  f: (item: T, index: number) => Promise<U>,
  chunkSize = 100
) => {
  const results: U[] = []

  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize)
    const chunkResults = await Promise.all(
      chunk.map((item, index) => f(item, i + index))
    )
    results.push(...chunkResults)
  }

  return results
}

export const mapAsync = <T, U>(
  items: T[],
  f: (item: T, index: number) => Promise<U>,
  maxConcurrentRequests = 100
) => {
  let index = 0
  let currRequests = 0
  const results: U[] = []

  // The following is a hack to fix a Node bug where the process exits before
  // the promise is resolved.
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  const intervalId = setInterval(() => {}, 10000)

  return new Promise((resolve: (results: U[]) => void, reject) => {
    const doWork = () => {
      while (index < items.length && currRequests < maxConcurrentRequests) {
        const itemIndex = index
        f(items[itemIndex], itemIndex)
          .then((data) => {
            results[itemIndex] = data
            currRequests--
            if (index === items.length && currRequests === 0) resolve(results)
            else doWork()
          })
          .catch(reject)

        index++
        currRequests++
      }
    }

    if (items.length === 0) resolve([])
    else doWork()
  }).finally(() => clearInterval(intervalId))
}
