import { Channel, ChannelModel, connect } from 'amqplib';
import { EventPublisher } from './event-publisher.interface';
import { Logger, LoggerService } from '@nestjs/common';
import { CustomLogger } from '../../logger/custom-logger';
import { BrokerConfigInterface } from '../subscriber/broker-config.interface';

// Inspired by: https://docs.nestjs.com/fundamentals/custom-providers and https://www.rabbitmq.com/tutorials/tutorial-three-javascript.html
export class RabbitMQPublisher implements EventPublisher {

  private logger: LoggerService = new CustomLogger(RabbitMQPublisher.name);
  private channel: Channel;
  private server: ChannelModel;

  constructor(private readonly config: BrokerConfigInterface) {
  }

  public async publishRetry<T>(message: T, routingKey: string, maxRetries = 10, logEvent = true): Promise<void> {
    await this.publishAndRetry(message, routingKey, 0, maxRetries, logEvent);
  }

  private async publishAndRetry<T>(message: T, routingKey: string, retries: number, maxRetries: number, logEvent: boolean): Promise<void> {
    try {
      await this.publish(message, routingKey, logEvent);
    } catch (e) {
      const timeOut = setTimeout(async () => {
        const newRetries = retries + 1;
        this.logger.error(`Got error when publishing to ${routingKey}. Retrying for the ${newRetries}th time. ${e}`);

        try {
          await this.publish(message, routingKey, logEvent);
        } catch (err) {
          await this.publishRetry(message, routingKey, newRetries, logEvent);
        }
      }, this.config.PUBLISH_RETRY_TIMEOUT_MS * (2 ** retries));

      if (retries >= maxRetries) {
        clearTimeout(timeOut);
        Logger.error(`Stopped retrying to publish to ${routingKey} after ${retries} times.`);
      }
    }
  }

  public async publish<T>(message: T, routingKey: string, logEvent = true): Promise<void> {
    const channel = await this.getChannel();

    const createdExchange = await channel.assertExchange(
      this.config.EXCHANGE_NAME, this.config.EXCHANGE_TYPE, { durable: this.config.DURABLE_EXCHANGE },
    );

    const byteMessage = Buffer.from(JSON.stringify(message));
    const options = { persistent: this.config.PERSISTENT_MESSAGES };

    channel.publish(createdExchange.exchange, routingKey, byteMessage, options);
    if (logEvent) {
      this.logger.debug(`Published event to ${routingKey} with payload ${JSON.stringify(message, null, 2)}`);
    }
  }

  public async close(): Promise<void> {
    if (this.channel) {
      await this.channel.close();
    }
  }

  public async hasActiveConnection(): Promise<boolean> {
    try {
      const server = await this.getServer();
      const channel = await this.getChannel();
      return server !== undefined && channel !== undefined;
    } catch (err) {
      return false;
    }
  }

  private async getChannel(): Promise<Channel> {
    if (this.channel) {
      return this.channel;
    }
    const server = await this.getServer();
    this.channel = await server.createChannel();

    this.channel.on('close', () => {
      this.logger.debug('Channel closed');
      this.channel = undefined;
    });

    this.channel.on('err', () => {
      this.logger.debug('Channel got an error');
      this.channel = undefined;
    });

    return this.channel;
  }

  private async getServer(): Promise<ChannelModel> {
    if (this.server) {
      return this.server;
    }
    this.server = await this.connect();

    this.server.on('close', () => {
      this.server = undefined;
      this.logger.debug('RabbitMQ server closed');
    });

    this.server.on('err', () => {
      this.logger.debug('RabbitMQ server got an error');
      this.server = undefined;
    });
    return this.server;
  }

  private async connect(): Promise<ChannelModel> {
    return connect(this.config.AMQP_CONNECTION_URI);
  }
}
