import { Logger } from '@nestjs/common';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { ClientConfigInterface } from './client-config.interface';
import { ConnectableClient } from './connectable-client.interface';

const DISCONNECTED_STATUS = 'disconnected';

export class ClientConnector {
  private statusSubscription?: Subscription;
  private destroyed = false;
  private reconnectInFlight = false;

  constructor(
    private readonly config: ClientConfigInterface,
    private readonly client: ConnectableClient,
    private readonly serviceName: string,
    private readonly context: string,
  ) {}

  public async connect(): Promise<void> {
    try {
      await this.client.connect();
      Logger.log(`Connected to ${this.serviceName}`, this.context);
      this.watchStatusIfSupported();
    } catch (error) {
      Logger.warn(
        `Initial connection to ${this.serviceName} failed, retrying in background: ${error}`,
        this.context,
      );
      this.scheduleBackgroundReconnect();
    }
  }

  private scheduleBackgroundReconnect(): void {
    void this.connectWithRetry(0)
      .then(() => {
        if (!this.destroyed) {
          this.watchStatusIfSupported();
        }
      })
      .catch((error) => {
        Logger.error(
          `Unexpected error while reconnecting to ${this.serviceName}: ${error}`,
          this.context,
        );
      });
  }

  public destroy(): void {
    this.destroyed = true;
    this.statusSubscription?.unsubscribe();
    this.statusSubscription = undefined;
  }

  private watchStatusIfSupported(): void {
    this.statusSubscription?.unsubscribe();
    this.statusSubscription = this.client.status
      .pipe(filter((status) => status === DISCONNECTED_STATUS))
      .subscribe(() => {
        void this.handleDisconnect();
      });
  }

  private async handleDisconnect(): Promise<void> {
    if (this.destroyed || this.reconnectInFlight) {
      return;
    }

    this.reconnectInFlight = true;
    try {
      Logger.warn(
        `Disconnected from ${this.serviceName}, reconnecting...`,
        this.context,
      );
      await this.client.close();
      await this.connectWithRetry(0);
    } finally {
      this.reconnectInFlight = false;
    }
  }

  private async connectWithRetry(initialRetries: number): Promise<void> {
    let retries = initialRetries;

    while (!this.destroyed) {
      try {
        await this.client.connect();
        Logger.log(`Connected to ${this.serviceName}`, this.context);
        return;
      } catch {
        await this.sleep(this.getRetryTime(retries));
        retries += 1;
        Logger.log(
          `Retrying ${this.serviceName} connection: ${retries}th time`,
          this.context,
        );
      }
    }
  }

  private getRetryTime(retries: number): number {
    const time = this.config.INITIAL_RETRY_TIMEOUT_MS * 2 ** retries;
    return Math.min(time, this.config.MAX_RETRY_TIME_MS);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}
