// tslint:disable max-classes-per-file typedef member-access no-object-mutation object-literal-sort-keys readonly-keyword no-let no-delete only-arrow-functions ban-types
import { client as IPCClient, server as IPCServer } from 'fast-ipc'
import {
  IRateLimiterOptions,
  RateLimiterMemory,
  RateLimiterRes
} from 'rate-limiter-flexible'

const channel = 'rate_limiter_flexible'
let masterInstance = null

const workerProcessResponse = function(
  msg,
  resolve: Function,
  reject: Function
) {
  if (!msg || msg.channel !== channel || msg.keyPrefix !== this.keyPrefix) {
    return false
  }

  let res
  if (msg.data === null || msg.data === true || msg.data === false) {
    res = msg.data
  } else {
    res = new RateLimiterRes(
      msg.data.remainingPoints,
      msg.data.msBeforeNext,
      msg.data.consumedPoints,
      msg.data.isFirstInDuration // eslint-disable-line comma-dangle
    )
  }

  switch (msg.type) {
    case 'resolve':
      resolve(res)
      break
    case 'reject':
      reject(res)
      break
    default:
      throw new Error(`RateLimiterCluster: no such message type '${msg.type}'`)
  }

  return undefined
}

/**
 * Prepare options to send to master
 * Master will create rate limiter depending on options
 *
 * @returns {{points: *, duration: *, blockDuration: *, execEvenly: *, execEvenlyMinDelayMs: *, keyPrefix: *}}
 */
const getOpts = function() {
  return {
    points: this.points,
    duration: this.duration,
    blockDuration: this.blockDuration,
    execEvenly: this.execEvenly,
    execEvenlyMinDelayMs: this.execEvenlyMinDelayMs,
    keyPrefix: this.keyPrefix
  }
}

export class RateLimiterIPCWorker extends RateLimiterMemory {
  keyPrefix: any
  // tslint:disable-next-line: variable-name
  _initiated: boolean

  ipcClient: IPCClient

  constructor(opts: IRateLimiterOptions) {
    super(opts)

    this.ipcClient = new IPCClient(channel)

    process.setMaxListeners(0)

    this._initiated = false

    const rlOpts = getOpts.call(this)

    this.ipcClient.send(
      'init',
      [rlOpts.keyPrefix, JSON.stringify(rlOpts)],
      () => {
        this._initiated = true
      }
    )
  }

  consume(key, pointsToConsume = 1, options = {}) {
    return new Promise<RateLimiterRes>((resolve, reject) => {
      this.ipcClient.send(
        'consume',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            pointsToConsume,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }

  penalty(key, points = 1, options = {}) {
    return new Promise<RateLimiterRes>((resolve, reject) => {
      this.ipcClient.send(
        'penalty',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            points,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }

  reward(key, points = 1, options = {}) {
    return new Promise<RateLimiterRes>((resolve, reject) => {
      this.ipcClient.send(
        'reward',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            points,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }

  block(key, secDuration, options = {}) {
    return new Promise<RateLimiterRes>((resolve, reject) => {
      this.ipcClient.send(
        'block',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            secDuration,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }

  get(key, options = {}) {
    return new Promise<RateLimiterRes>((resolve, reject) => {
      this.ipcClient.send(
        'get',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }

  delete(key, options = {}) {
    return new Promise<boolean>((resolve, reject) => {
      this.ipcClient.send(
        'delete',
        [
          this.keyPrefix,
          JSON.stringify({
            key,
            options
          })
        ],
        res => workerProcessResponse.call(this, res, resolve, reject)
      )
    })
  }
}

export class RateLimiterIPCMaster {
  // tslint:disable-next-line: variable-name
  _rateLimiters: Record<string, RateLimiterMemory>
  ipcServer: IPCServer

  constructor() {
    if (masterInstance) {
      return masterInstance
    }

    this.ipcServer = new IPCServer(channel)

    this._rateLimiters = {}

    this.ipcServer.on('init', ([keyPrefix, dataStr], ack) => {
      const existing = this._rateLimiters[keyPrefix]
      const opts = JSON.parse(dataStr)
      if (existing) {
        if (opts.points) {
          // @ts-ignore
          existing.points = opts.points
        }
        if (opts.duration) {
          // @ts-ignore
          existing.duration = opts.duration
        }
      } else {
        this._rateLimiters[keyPrefix] = new RateLimiterMemory(opts)
      }
      ack([])
    })

    this.ipcServer.on('consume', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].consume(
          data.key,
          data.pointsToConsume,
          data.opts
        )
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    this.ipcServer.on('penalty', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].penalty(
          data.key,
          data.points,
          data.opts
        )
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    this.ipcServer.on('reward', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].reward(
          data.key,
          data.points,
          data.opts
        )
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    this.ipcServer.on('block', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].block(
          data.key,
          data.secDuration,
          data.opts
        )
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    this.ipcServer.on('get', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].get(data.key, data.opts)
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    this.ipcServer.on('delete', async ([keyPrefix, dataStr], ack) => {
      const data = JSON.parse(dataStr)
      try {
        const res = await this._rateLimiters[keyPrefix].delete(
          data.key,
          data.opts
        )
        ack({
          channel,
          keyPrefix,
          data: res,
          type: 'resolve'
        })
      } catch (err) {
        ack({
          channel,
          keyPrefix,
          data: err,
          type: 'reject'
        })
      }
    })

    masterInstance = this
  }
}