import * as R from 'ramda';
import { IBusAdapter } from '../interfaces/bus/bus-adapter.interface';
import { IOnInit } from '../interfaces/lifecycles';
import { ITransferData } from '../interfaces/transfer-data';
import { TransferDataDto } from '../interfaces/transfer-data-dto.interface';
import { ISetOptions } from '../interfaces/set-options.interface';
import { loadPackage } from '../utils/load-package.util';
import { CraftsLogger } from '../logger/services/logger.service';
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class RabbitMQBusAdapter implements IBusAdapter, IOnInit, ISetOptions {
  private options: any = {};
  private readonly rabbitmqPackage: any;
  private rabbitmq: any;
  private pubChannel: any;
  private subChannel: any;

  constructor(private readonly logger: CraftsLogger) {
    logger.setContext(RabbitMQBusAdapter.name);
    this.rabbitmqPackage = loadPackage('amqplib', RabbitMQBusAdapter.name);
  }

  async onInit(): Promise<void> {
    this.logger.verbose('init connection');
    this.rabbitmq = await this.rabbitmqPackage.connect(this.options.host);
    this.subChannel = await this.rabbitmq.createChannel();
    this.pubChannel = await this.rabbitmq.createChannel();
  }

  async publish(
    data: ITransferData<TransferDataDto>,
    options?: any
  ): Promise<void> {
    options = options || {};
    const { action, context } = data;
    const exchange = R.or(context, this.options.exchange);
    const type = R.or(this.options.type, 'topic');

    await this.pubChannel
      .assertExchange(
        exchange,
        type,
        this.exchangeDefault(options.exchange || {})
      )
      .then(() =>
        this.pubChannel.publish(
          exchange,
          action,
          Buffer.from(JSON.stringify(data)),
          this.publishDefault(options.publish || {})
        )
      );
  }

  async subscribe(
    handle: Function,
    data: ITransferData<TransferDataDto>,
    options?: any
  ): Promise<void> {
    options = options || {};
    const { action, context } = data;
    const exchange = R.or(context, this.options.exchange);
    const service = R.or(options.service, this.options.service);
    const queue = `${service}.${exchange}.${action}`;
    const type = R.or(this.options.type, 'topic');
    const prefetch = 1;

    this.subChannel.prefetch(R.or(options.prefetch, prefetch));

    await this.subChannel
      .assertExchange(
        exchange,
        type,
        this.exchangeDefault(options.exchange || {})
      )
      .then(() =>
        this.subChannel.assertQueue(
          queue,
          this.queueDefault(options.queue || {})
        )
      )
      .then(() => this.subChannel.bindQueue(queue, exchange, action))
      .then(() =>
        this.subChannel.consume(
          queue,
          this.subscribeHandle(handle),
          this.subscribeDefault(options.subscribe || {})
        )
      );
  }

  private subscribeHandle = (handle: Function) => async (message) => {
    try {
      this.logger.verbose('handle message');
      await handle(JSON.parse(message.content.toString()));
      this.subChannel.ack(message);
      this.logger.verbose('ack message');
    } catch (error) {
      this.logger.error('subscribe handle:', error);
      this.subChannel.nack(message);
    }
  };

  setOptions(options: any): void {
    this.options = options;
  }

  async close(): Promise<void> {
    this.logger.verbose('close connection');
    await this.rabbitmq.close();
  }

  private queueDefault = R.mergeDeepRight({
    durable: true,
    autoDelete: false,
    exclusive: false,
  });

  private subscribeDefault = R.mergeDeepRight({
    noAck: false,
    exclusive: false,
  });

  private exchangeDefault = R.mergeDeepRight({
    durable: true,
    autoDelete: false,
  });

  private publishDefault = R.mergeDeepRight({
    persistent: true,
  });
}
