import { addListenerTyped, Communicator, type ParserMap, type Transport } from "./communicator.js";
import { create, HeartBeatSchema, type PacketBase } from "./packets/message_common_pb.js";

export abstract class BaseClient implements Transport {
  readonly communicator = new Communicator();

  private readonly intervalMs: number;
  private readonly waitMs: number;
  private readonly doClose: () => Promise<void>;
  private isClosed = false;
  private closePromise: Promise<void> | null = null;
  private hbTimer: ReturnType<typeof setTimeout> | null = null;
  private waitingHbResponse = false;
  private disconnectListeners: Array<(self: BaseClient) => void | Promise<void>> = [];

  protected constructor(intervalSec: number, waitSec: number, doClose: () => Promise<void>) {
    this.intervalMs = intervalSec * 1000;
    this.waitMs = waitSec * 1000;
    this.doClose = doClose;
  }

  abstract writePacket(base: PacketBase): Promise<void>;

  protected initBase(parserMap: ParserMap): void {
    this.communicator.initialize(this, parserMap);
    this.communicator.setWriteErrorHandler(() => {
      void this.onDisconnected();
    });
    // transport read loop가 onReceivedFrame으로 흘리는 raw frame의 decode 실패(inline fallback 또는
    // attach된 gateway 모두)를 write-error와 동일하게 disconnect로 처리한다.
    this.communicator.setFrameErrorHandler(() => {
      void this.onDisconnected();
    });
    addListenerTyped(this.communicator, HeartBeatSchema, () => {
      void this.onHeartbeat();
    });
    void this.sendHeartbeat();
  }

  private closePromiseGrace: Promise<void> | null = null;
  private closePromiseForce: Promise<void> | null = null;

  async close(): Promise<void> {
    return this.closeWithGrace(true);
  }

  private async closeWithGrace(graceful: boolean): Promise<void> {
    if (this.isClosed) {
      return;
    }

    if (graceful) {
      if (this.closePromiseGrace !== null) {
        return this.closePromiseGrace;
      }
      this.closePromiseGrace = (async () => {
        this.isClosed = true;
        await this.communicator.close();
        this.stopHeartbeat();
        try {
          await this.doClose();
        } catch {}
        await this.notifyDisconnected();
      })();
      return this.closePromiseGrace;
    } else {
      if (this.closePromiseForce !== null) {
        return this.closePromiseForce;
      }
      this.closePromiseForce = (async () => {
        this.isClosed = true;
        this.communicator.shutdown();
        this.stopHeartbeat();
        try {
          await this.doClose();
        } catch {}
        await this.notifyDisconnected();
      })();
      return this.closePromiseForce;
    }
  }

  addDisconnectListener(fn: (self: this) => void | Promise<void>): void {
    this.disconnectListeners.push(fn as (self: BaseClient) => void | Promise<void>);
  }

  removeDisconnectListeners(): void {
    this.disconnectListeners = [];
  }

  protected async sendHeartbeat(): Promise<void> {
    if (!this.communicator.isAlive() || this.intervalMs <= 0) {
      return;
    }
    this.stopHeartbeat();
    this.hbTimer = setTimeout(() => {
      void this.onHeartbeatIntervalElapsed();
    }, this.intervalMs);
  }

  protected async onHeartbeat(): Promise<void> {
    if (this.waitingHbResponse) {
      this.waitingHbResponse = false;
      return;
    }
    try {
      await this.communicator.send(create(HeartBeatSchema));
    } catch {
      return;
    }
  }

  protected async onDisconnected(): Promise<void> {
    try {
      await this.closeWithGrace(false);
    } catch {
      return;
    }
  }

  private async onHeartbeatIntervalElapsed(): Promise<void> {
    if (!this.communicator.isAlive()) {
      return;
    }
    this.waitingHbResponse = true;
    try {
      await this.communicator.send(create(HeartBeatSchema));
    } catch {
      return;
    }

    this.stopHeartbeat();
    if (this.waitMs > 0) {
      this.hbTimer = setTimeout(() => {
        void this.onHeartbeatWaitElapsed();
      }, this.waitMs);
    }
  }

  private async onHeartbeatWaitElapsed(): Promise<void> {
    if (this.communicator.isAlive()) {
      await this.onDisconnected();
    }
  }

  private stopHeartbeat(): void {
    if (this.hbTimer !== null) {
      clearTimeout(this.hbTimer);
      this.hbTimer = null;
    }
  }

  private async notifyDisconnected(): Promise<void> {
    const listeners = [...this.disconnectListeners];
    this.disconnectListeners = [];
    for (const listener of listeners) {
      try {
        await listener(this);
      } catch {
        continue;
      }
    }
  }
}
