module.exports = throttlingPlugin

const BottleneckLight = require('bottleneck/light')
const wrapRequest = require('./wrap-request')
const triggersNotificationPaths = require('./triggers-notification-paths')
const routeMatcher = require('./route-matcher')(triggersNotificationPaths)

// Workaround to allow tests to directly access the triggersNotification function.
const triggersNotification = throttlingPlugin.triggersNotification =
  routeMatcher.test.bind(routeMatcher)

const groups = {}

const createGroups = function (Bottleneck, common) {
  groups.global = new Bottleneck.Group({
    id: 'octokit-global',
    maxConcurrent: 10,
    ...common
  })
  groups.search = new Bottleneck.Group({
    id: 'octokit-search',
    maxConcurrent: 1,
    minTime: 2000,
    ...common
  })
  groups.write = new Bottleneck.Group({
    id: 'octokit-write',
    maxConcurrent: 1,
    minTime: 1000,
    ...common
  })
  groups.notifications = new Bottleneck.Group({
    id: 'octokit-notifications',
    maxConcurrent: 1,
    minTime: 3000,
    ...common
  })
}

function throttlingPlugin (octokit, octokitOptions = {}) {
  const {
    enabled = true,
    Bottleneck = BottleneckLight,
    id = 'no-id',
    timeout = 1000 * 60 * 2, // Redis TTL: 2 minutes
    connection
  } = octokitOptions.throttle || {}
  if (!enabled) {
    return
  }
  const common = { connection, timeout }

  if (groups.global == null) {
    createGroups(Bottleneck, common)
  }

  const state = Object.assign({
    clustering: connection != null,
    triggersNotification,
    minimumAbuseRetryAfter: 5,
    retryAfterBaseValue: 1000,
    retryLimiter: new Bottleneck(),
    id,
    ...groups
  }, octokitOptions.throttle)

  if (typeof state.onAbuseLimit !== 'function' || typeof state.onRateLimit !== 'function') {
    throw new Error(`octokit/plugin-throttling error:
        You must pass the onAbuseLimit and onRateLimit error handlers.
        See https://github.com/octokit/rest.js#throttling

        const octokit = new Octokit({
          throttle: {
            onAbuseLimit: (error, options) => {/* ... */},
            onRateLimit: (error, options) => {/* ... */}
          }
        })
    `)
  }

  const events = {}
  const emitter = new Bottleneck.Events(events)
  events.on('abuse-limit', state.onAbuseLimit)
  events.on('rate-limit', state.onRateLimit)
  events.on('error', e => console.warn('Error in throttling-plugin limit handler', e))

  state.retryLimiter.on('failed', async function (error, info) {
    const options = info.args[info.args.length - 1]
    const isGraphQL = options.url.startsWith('/graphql')

    if (!(isGraphQL || error.status === 403)) {
      return
    }

    const retryCount = ~~options.request.retryCount
    options.request.retryCount = retryCount

    const { wantRetry, retryAfter } = await (async function () {
      if (/\babuse\b/i.test(error.message)) {
        // The user has hit the abuse rate limit. (REST only)
        // https://developer.github.com/v3/#abuse-rate-limits

        // The Retry-After header can sometimes be blank when hitting an abuse limit,
        // but is always present after 2-3s, so make sure to set `retryAfter` to at least 5s by default.
        const retryAfter = Math.max(~~error.headers['retry-after'], state.minimumAbuseRetryAfter)
        const wantRetry = await emitter.trigger('abuse-limit', retryAfter, options)
        return { wantRetry, retryAfter }
      }
      if (error.headers != null && error.headers['x-ratelimit-remaining'] === '0') {
        // The user has used all their allowed calls for the current time period (REST and GraphQL)
        // https://developer.github.com/v3/#rate-limiting

        const rateLimitReset = new Date(~~error.headers['x-ratelimit-reset'] * 1000).getTime()
        const retryAfter = Math.max(Math.ceil((rateLimitReset - Date.now()) / 1000), 0)
        const wantRetry = await emitter.trigger('rate-limit', retryAfter, options)
        return { wantRetry, retryAfter }
      }
      return {}
    })()

    if (wantRetry) {
      options.request.retryCount++
      return retryAfter * state.retryAfterBaseValue
    }
  })

  octokit.hook.wrap('request', wrapRequest.bind(null, state))
}