import Settings from '@overleaf/settings'
import Metrics from '@overleaf/metrics'
import logger from '@overleaf/logger'
import RedisWrapper from './RedisWrapper.mjs'
import RateLimiterFlexible from 'rate-limiter-flexible'
import OError from '@overleaf/o-error'

const rclient = RedisWrapper.client('ratelimiter')

/**
 * Wrapper over the RateLimiterRedis class
 */
export class RateLimiter {
  #opts

  /**
   * Create a rate limiter.
   *
   * @param name {string} The name that identifies this rate limiter. Different
   *                      rate limiters must have different names.
   * @param opts {object} Options to pass to RateLimiterRedis
   *
   * Some useful options:
   *
   *   points - number of points that can be consumed over the given duration
   *            (default: 4)
   *   subnetPoints - number of points that can be consumed over the given
   *                  duration accross a sub-network. This should only be used
   *                  ip-based rate limits.
   *   duration - duration of the fixed window in seconds (default: 1)
   *   blockDuration - additional seconds to block after all points are consumed
   *                   (default: 0)
   */
  constructor(name, opts = {}) {
    this.name = name
    this.#opts = Object.assign({}, opts)
    this._rateLimiter = new RateLimiterFlexible.RateLimiterRedis({
      ...opts,
      keyPrefix: `rate-limit:${name}`,
      storeClient: rclient,
    })
    if (opts.subnetPoints && !Settings.rateLimit?.subnetRateLimiterDisabled) {
      this._subnetRateLimiter = new RateLimiterFlexible.RateLimiterRedis({
        ...opts,
        points: opts.subnetPoints,
        keyPrefix: `rate-limit:${name}`,
        storeClient: rclient,
      })
    }
  }

  // Readonly access to the options, useful for aligning rate-limits.
  getOptions() {
    return Object.assign({}, this.#opts)
  }

  async consume(key, points = 1, options = { method: 'unknown' }) {
    if (Settings.disableRateLimits) {
      // Return a fake result in case it's used somewhere
      return {
        msBeforeNext: 0,
        remainingPoints: 100,
        consumedPoints: 0,
        isFirstInDuration: false,
      }
    }

    await this.consumeForRateLimiter(this._rateLimiter, key, options, points)

    if (options.method === 'ip' && this._subnetRateLimiter) {
      const subnetKey = this.getSubnetKeyFromIp(key)
      await this.consumeForRateLimiter(
        this._subnetRateLimiter,
        subnetKey,
        options,
        points,
        'ip-subnet'
      )
    }
  }

  async consumeForRateLimiter(rateLimiter, key, options, points, method) {
    try {
      const res = await rateLimiter.consume(key, points, options)
      return res
    } catch (err) {
      if (err instanceof Error) {
        throw err
      } else {
        // Only log the first time we exceed the rate limit for a given key and
        // duration. This happens when the previous amount of consumed points
        // was below the threshold.
        if (err.consumedPoints - points <= rateLimiter.points) {
          logger.warn({ path: this.name, key }, 'rate limit exceeded')
        }
        Metrics.inc('rate-limit-hit', 1, {
          path: this.name,
          method: method || options.method,
        })
        throw err
      }
    }
  }

  getSubnetKeyFromIp(ip) {
    if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip)) {
      throw new OError(
        'Cannot generate subnet key as the ip address is not of the expected format.',
        { ip }
      )
    }

    return ip.split('.').slice(0, 3).join('.')
  }

  async delete(key) {
    return await this._rateLimiter.delete(key)
  }
}

/*
 * Shared rate limiters
 */

export const openProjectRateLimiter = new RateLimiter('open-project', {
  points: 15,
  duration: 60,
})

// Keep in sync with the can-skip-captcha options.
export const overleafLoginRateLimiter = new RateLimiter(
  'overleaf-login',
  Settings.rateLimit?.login?.ip || {
    points: 20,
    subnetPoints: 200,
    duration: 60,
  }
)

export default {
  RateLimiter,
  openProjectRateLimiter,
  overleafLoginRateLimiter,
}
