import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import IORedis from 'ioredis';
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { EEnvVariable } from '../../../configuration/enums/env-variable.enum';

export enum ERedisEvent {
  EMIT_SOCKET_MESSAGE = 'emitSocketMessage',
  EMIT_SOCKET_MESSAGE_CHANGED = 'emitSocketMessageChanged',
}

export const REDIS_KEYS = {
  ATTACHMENT_LICENSEE_IDS_CHECKS: 'attachmentLicenseeIdChecks',
  CONNECTED_LICENSEE_IDS: 'connectedLicenseeIds',
  LAUNCHED_INSTANCE_COUNT: 'launchedInstanceCount',
};

const KNOWN_REDIS_MODES = ['normal', 'cluster'];

class RedisConnectError extends Error {
  name: 'RedisConnectError';

  constructor(triedMode: string) {
    super();

    this.message = `Failed to connect to Redis: unknown Redis mode "${triedMode}". `;

    this.message += `Known modes are: ${KNOWN_REDIS_MODES.join(', ')}`;
  }
}

@Injectable()
export class RedisService implements OnModuleDestroy {
  private logger: Logger = new Logger('RedisService');

  private pub: IORedis.Redis | IORedis.Cluster;
  private sub: IORedis.Redis | IORedis.Cluster;

  private message$: Subject<{
    channel: string;
    message: unknown;
  }> = new Subject();

  public onConnect$: Subject<void> = new Subject<void>();

  constructor() {
    this.connect();
  }

  onModuleDestroy() {
    this.sub.unsubscribe(this.getChannelList());
  }

  private async connect() {
    const redisMode = process.env[EEnvVariable.REDIS_MODE] || 'normal';
    const host = process.env[EEnvVariable.REDIS_HOST];
    const port = process.env[EEnvVariable.REDIS_PORT];

    const redisOptions: IORedis.RedisOptions = {
      keyPrefix: process.env[EEnvVariable.REDIS_SCOPE],
    };

    switch (redisMode) {
      case 'cluster': {
        const args: ConstructorParameters<typeof IORedis.Cluster> = [
          [{ host, port: +port }],
          // fixme: `keyPrefix: redisOptions.keyPrefix` and `as any` are workaround for prefixing. Remove when this will be fixed in ioredis
          { redisOptions, keyPrefix: redisOptions.keyPrefix } as any,
        ];
        this.pub = new IORedis.Cluster(...args);
        this.sub = new IORedis.Cluster(...args);
        break;
      }

      case 'normal': {
        this.pub = new IORedis(+port, host, redisOptions);
        this.sub = new IORedis(+port, host, redisOptions);
        break;
      }

      default:
        throw new RedisConnectError(redisMode);
    }

    try {
      const channelList = this.getChannelList();
      await this.sub.subscribe(channelList);
      this.logger.log(`Subscribed to channels: ${channelList}`);

      this.sub.on('message', (channel, message) => {
        let parsedMessage;
        try {
          parsedMessage = JSON.parse(message);
        } catch (e) {
          parsedMessage = message;
        }

        this.message$.next({ channel, message: parsedMessage });
      });
    } catch (e) {
      this.logger.error('Failed to subscribe to Redis messages');
      throw e;
    }

    this.onConnect$.next();
    this.logger.log('Successfully connected to Redis');
  }

  private getChannelList(): string[] {
    return Object.values(ERedisEvent);
  }

  on<T = { licenseeId: number; messageId: number }>(
    type: ERedisEvent.EMIT_SOCKET_MESSAGE
  ): Observable<{
    data: T;
    channel: string;
  }>;

  on<T = { licenseeId: number; messageId: number }>(
    type: ERedisEvent.EMIT_SOCKET_MESSAGE_CHANGED
  ): Observable<{
    data: T;
    channel: string;
  }>;

  on<T = any>(
    type: ERedisEvent
  ): Observable<{
    data: T;
    channel: string;
  }> {
    return this.message$.pipe(
      filter((m) => m.channel === type),
      map((m) => ({ data: m.message as T, channel: m.channel }))
    );
  }

  emit(
    type: ERedisEvent.EMIT_SOCKET_MESSAGE,
    data: { licenseeId: number; messageId: number }
  );
  emit(
    type: ERedisEvent.EMIT_SOCKET_MESSAGE_CHANGED,
    data: { licenseeId: number; messageId: number }
  );
  emit(type: ERedisEvent, data: unknown) {
    this.pub.publish(type, JSON.stringify(data));
  }

  pushToList(key: string, value: string) {
    this.pub.rpush(key, value);
  }

  getList(key: string): Promise<string[]> {
    return this.pub.lrange(key, 0, -1);
  }

  deleteKey(key: string) {
    this.pub.del(key);
  }

  removeValueFromList(key: string, value: string) {
    this.pub.lrem(key, -1, value);
  }

  increment(key: string): Promise<number> {
    return this.pub.incr(key);
  }

  decrement(key: string) {
    this.pub.decr(key);
  }
}
