import nconf from 'nconf';
import redis from 'redis';
import {
  RateLimiterRedis,
  RateLimiterMemory,
  RateLimiterRes,
} from 'rate-limiter-flexible';
import {
  TooManyRequests,
} from '../libs/errors';
import logger from '../libs/logger';
import { apiError } from '../libs/apiError';

// Middleware to rate limit requests to the API

// More info on the API rate limits can be found on the wiki at
// https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools

const IS_TEST = nconf.get('IS_TEST');
const RATE_LIMITER_ENABLED = nconf.get('RATE_LIMITER_ENABLED') === 'true';
const REDIS_HOST = nconf.get('REDIS_HOST');
const REDIS_PASSWORD = nconf.get('REDIS_PASSWORD');
const REDIS_PORT = nconf.get('REDIS_PORT');
const LIVELINESS_PROBE_KEY = nconf.get('LIVELINESS_PROBE_KEY');
const REGISTRATION_COST = nconf.get('RATE_LIMITER_REGISTRATION_COST') || 5;
const IP_RATE_LIMIT_COST = nconf.get('RATE_LIMITER_IP_COST') || 5;

let redisClient;
let rateLimiter;

const rateLimiterOpts = {
  keyPrefix: 'api-v3',
  points: 30, // 30 requests
  duration: 60, // per 1 minute by User ID or IP
};

if (RATE_LIMITER_ENABLED) {
  if (IS_TEST) {
    rateLimiter = new RateLimiterMemory({
      ...rateLimiterOpts,
    });
  } else {
    redisClient = redis.createClient({
      host: REDIS_HOST,
      password: REDIS_PASSWORD,
      port: REDIS_PORT,
      enable_offline_queue: false,
    });

    redisClient.on('error', error => {
      logger.error(error, 'Redis Error');
    });

    rateLimiter = new RateLimiterRedis({
      ...rateLimiterOpts,
      storeClient: redisClient,
    });
  }
}

function setResponseHeaders (res, rateLimiterRes) {
  const headers = {
    'X-RateLimit-Limit': rateLimiterOpts.points,
    'X-RateLimit-Remaining': rateLimiterRes.remainingPoints,
    'X-RateLimit-Reset': new Date(Date.now() + rateLimiterRes.msBeforeNext),
  };

  if (rateLimiterRes.remainingPoints < 1) {
    headers['Retry-After'] = rateLimiterRes.msBeforeNext / 1000;
  }

  res.set(headers);
}

export default function rateLimiterMiddleware (req, res, next) {
  if (!RATE_LIMITER_ENABLED) return next();
  if (LIVELINESS_PROBE_KEY && req.query.liveliness === LIVELINESS_PROBE_KEY) return next();

  const userId = req.header('x-api-user');

  let cost = 1;
  if (req.path === '/api/v4/user/auth/local/register' || req.path === '/api/v3/user/auth/local/register') {
    cost = REGISTRATION_COST;
  } else if (!userId) {
    cost = IP_RATE_LIMIT_COST;
  }

  return rateLimiter.consume(userId || req.ip, cost)
    .then(rateLimiterRes => {
      setResponseHeaders(res, rateLimiterRes);
      return next();
    })
    .catch(rateLimiterRes => {
      if (rateLimiterRes instanceof RateLimiterRes) {
        setResponseHeaders(res, rateLimiterRes);
        return next(new TooManyRequests(apiError('clientRateLimited')));
      }

      // In case of an unhandled error we skip the middleware as it could mean
      // , for example, that the connection to the redis database is not working.
      // We do not want to block all requests in these cases.
      logger.error(rateLimiterRes, 'Rate Limiter Error');
      return next();
    });
}
