import { Redis } from 'ioredis'
import { ECommand } from '../types'
import { DBClient } from './db'
import { logger } from '../logger'
import { ListResult, AddResult, RemoveResult, FlowResult } from './types'

/**
 * Redis table structure:
 *  HMSET key<SHA224 string> download bytes<number> upload bytes<number>
 *  SET user:acctId<number> key<SHA224 string>
 */
class RedisClient extends DBClient {
  private cl: Redis

  constructor(redisClient: Redis) {
    super()
    this.cl = redisClient
  }

  public listAccounts = async (): Promise<ListResult> => {
    try {
      const accounts = await this.cl.keys('user:*')
      let pipe = this.cl.pipeline()
      accounts.forEach((acct) => {
        pipe = pipe.get(acct)
      })
      const result = (await pipe.exec()).map((item, idx) => ({
        id: parseInt(accounts[idx].slice(5), 10),
        password: item[1] || '',
      }))
      logger.debug('List: ' + JSON.stringify(result))
      return { type: ECommand.List, data: result }
    } catch (e) {
      throw new Error("Query error on 'list': " + e.message)
    }
  }

  public addAccount = async (
    acctId: number,
    password: string,
  ): Promise<AddResult> => {
    const key = password
    try {
      if (await this.cl.exists(key)) {
        throw new Error('duplicate password.')
      }
      const currentKey = await this.cl.get('user:' + acctId.toString())
      if (currentKey) {
        await this.cl
          .multi()
          .eval(
            "local r = redis.call('hmget', KEYS[1], 'upload', 'download');" +
              "redis.call('hmset', KEYS[2], 'upload', r[1], 'download', r[2]);",
            2,
            currentKey,
            key,
          )
          .del(currentKey)
          .set('user:' + acctId.toString(), key)
          .exec()
      } else {
        await this.cl
          .multi()
          .set('user:' + acctId.toString(), key)
          .hmset(key, { download: '0', upload: '0' })
          .exec()
      }
      logger.debug(`Added user: ${acctId}`)
      return { type: ECommand.Add, id: acctId }
    } catch (e) {
      throw new Error("Query error on 'add': " + e.message)
    }
  }

  public removeAccount = async (acctId: number): Promise<RemoveResult> => {
    try {
      const currentKey = await this.cl.get('user:' + acctId.toString())
      if (currentKey) {
        await this.cl
          .multi()
          .del(currentKey)
          .del('user:' + acctId.toString())
          .exec()
      } else {
        await this.cl.del('user:' + acctId.toString())
      }
      logger.debug(`Removed user: ${acctId}`)
      return { type: ECommand.Delete, id: acctId }
    } catch (e) {
      throw new Error("Query error on 'del': " + e.message)
    }
  }

  public getFlow = async (): Promise<FlowResult> => {
    try {
      const accounts = await this.cl.keys('user:*')
      const result = (
        await Promise.all(
          accounts.map(async (user: string) => {
            const key = await this.cl.get(user)
            let flow = 0
            if (key) {
              const [dl, ul]: string[] = (
                await this.cl
                  .multi()
                  .hmget(key, 'download', 'upload')
                  .hmset(key, 'download', '0', 'upload', '0')
                  .exec()
              )[0][1]
              flow = parseInt(dl, 10) + parseInt(ul, 10)
            }
            return {
              id: parseInt(user.slice(5), 10),
              flow: flow,
            }
          }),
        )
      ).filter((user) => user.flow !== 0 && user.flow !== null)
      logger.debug('Flow: ' + JSON.stringify(result))
      return { type: ECommand.Flow, data: result }
    } catch (e) {
      throw new Error("Query error on 'flow': " + e.message)
    }
  }

  public disconnect = (): void => {
    this.cl.disconnect()
  }
}

export { RedisClient }