import * as redis from 'redis';
import { DatabaseDriver, DatabaseOption, Encrypted, Index, Records, SortOrder } from '../typings';
import * as dbutils from './utils';

class Redis implements DatabaseDriver {
  private options: DatabaseOption;
  private client!: any;

  constructor(options: DatabaseOption) {
    this.options = options;
  }

  async init(): Promise<Redis> {
    const opts = {};

    if (this.options && this.options.url) {
      opts['socket'] = {
        url: this.options.url,
      };
    }

    this.client = redis.createClient(opts);
    this.client.on('error', (err: any) => console.info('Redis Client Error', err));

    await this.client.connect();

    return this;
  }

  async get(namespace: string, key: string): Promise<any> {
    const res = await this.client.get(dbutils.key(namespace, key));
    if (res) {
      return JSON.parse(res);
    }

    return null;
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async getAll(
    namespace: string,
    pageOffset?: number,
    pageLimit?: number,
    _?: string,
    sortOrder?: SortOrder
  ): Promise<Records> {
    const sortedSetKey = dbutils.keyFromParts(dbutils.createdAtPrefix, namespace);

    const { offset, limit: count } = dbutils.normalizeOffsetAndLimit({
      pageOffset,
      pageLimit,
      maxLimit: this.options.pageLimit!,
    });

    const zRangeOptions = {
      BY: 'SCORE',
      REV: sortOrder === 'ASC',
      LIMIT: {
        offset,
        count,
      },
    };

    const elements =
      sortOrder === 'ASC'
        ? await this.client.zRange(sortedSetKey, +Infinity, -Infinity, zRangeOptions)
        : await this.client.zRange(sortedSetKey, -Infinity, +Infinity, zRangeOptions);

    if (elements.length === 0) {
      return { data: [] };
    }

    const keyArray = elements.map((element) => {
      return dbutils.keyFromParts(namespace, element);
    });

    const records: string[] = [];
    const values = await this.client.MGET(keyArray);

    for (let i = 0; i < values.length; i++) {
      const valueObject = JSON.parse(values[i].toString());

      if (valueObject !== null && valueObject !== '') {
        records.push(valueObject);
      }
    }

    return { data: records };
  }

  async getByIndex(
    namespace: string,
    idx: Index,
    pageOffset?: number,
    pageLimit?: number,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    _?: string
  ): Promise<Records> {
    const { offset: skip, limit } = dbutils.normalizeOffsetAndLimit({
      pageOffset,
      pageLimit,
      maxLimit: this.options.pageLimit!,
    });
    let take = limit;

    let count = 0;
    take! += skip!;
    const returnValue: string[] = [];
    const keyArray: string[] = [];
    const idxKey = dbutils.keyForIndex(namespace, idx);
    const dbKeys = await this.client.sMembers(dbutils.keyFromParts(dbutils.indexPrefix, idxKey));
    for await (const { value } of this.client.zScanIterator(
      dbutils.keyFromParts(dbutils.createdAtPrefix, namespace),
      count + 1
    )) {
      if (dbKeys.indexOf(value) !== -1) {
        if (count >= take!) {
          break;
        }
        if (count >= skip!) {
          keyArray.push(dbutils.keyFromParts(namespace, value));
        }
        count++;
      }
    }
    if (keyArray.length > 0) {
      const value = await this.client.MGET(keyArray);
      for (let i = 0; i < value.length; i++) {
        const valueObject = JSON.parse(value[i].toString());
        if (valueObject !== null && valueObject !== '') {
          returnValue.push(valueObject);
        }
      }
    }
    return { data: returnValue || [] };
  }

  async put(namespace: string, key: string, val: Encrypted, ttl = 0, ...indexes: any[]): Promise<void> {
    let tx = this.client.multi();
    const k = dbutils.key(namespace, key);

    tx = tx.set(k, JSON.stringify(val));

    if (ttl) {
      tx = tx.expire(k, ttl);
    }

    // no ttl support for secondary indexes
    for (const idx of indexes || []) {
      const idxKey = dbutils.keyForIndex(namespace, idx);
      tx = tx.sAdd(dbutils.keyFromParts(dbutils.indexPrefix, idxKey), key);
      tx = tx.sAdd(dbutils.keyFromParts(dbutils.indexPrefix, k), idxKey);
    }
    const timestamp = Number(Date.now());
    //Converting Timestamp in negative so that when we get the value, it will be found in reverse order (descending order).
    const negativeTimestamp = -Math.abs(timestamp);
    const value = await this.client.get(k);
    if (!value) {
      tx = tx.zAdd(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), [
        { score: negativeTimestamp, value: key },
      ]);
    }
    tx = tx.zAdd(dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace), [
      { score: negativeTimestamp, value: key },
    ]);
    await tx.exec();
  }

  async delete(namespace: string, key: string): Promise<any> {
    let tx = this.client.multi();
    const k = dbutils.key(namespace, key);
    tx = tx.del(k);

    const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
    // delete secondary indexes and then the mapping of the seconary indexes
    const dbKeys = await this.client.sMembers(idxKey);
    for (const dbKey of dbKeys || []) {
      tx.sRem(dbutils.keyFromParts(dbutils.indexPrefix, dbKey), key);
    }
    tx.ZREM(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), key);
    tx.ZREM(dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace), key);
    tx.del(idxKey);

    return await tx.exec();
  }

  async deleteMany(namespace: string, keys: string[]): Promise<void> {
    if (keys.length === 0) {
      return;
    }

    let tx = this.client.multi();

    for (const key of keys) {
      const k = dbutils.key(namespace, key);

      tx = tx.del(k);

      const idxKey = dbutils.keyFromParts(dbutils.indexPrefix, k);
      const dbKeys = await this.client.sMembers(idxKey);

      for (const dbKey of dbKeys || []) {
        tx.sRem(dbutils.keyFromParts(dbutils.indexPrefix, dbKey), key);
      }

      tx.ZREM(dbutils.keyFromParts(dbutils.createdAtPrefix, namespace), key);
      tx.ZREM(dbutils.keyFromParts(dbutils.modifiedAtPrefix, namespace), key);

      tx.del(idxKey);
    }

    await tx.exec();
  }

  async close(): Promise<void> {
    await this.client.quit();
  }
}

export default {
  new: async (options: DatabaseOption): Promise<Redis> => {
    return await new Redis(options).init();
  },
};
