import { safeParseJSON } from '@ai-sdk/provider-utils';
import type {
  RealtimeClientEvent,
  RealtimeModel,
  RealtimeServerEvent,
} from '../types/realtime-model';

/** One codec queue per connection; retired connections cannot deliver late work. */
export class RealtimeEventChannel {
  private active = true;
  private finishing = false;
  private readonly parse: (
    raw: unknown,
  ) => RealtimeServerEvent | RealtimeServerEvent[];
  private incoming = Promise.resolve();
  private outgoing = Promise.resolve();
  private incomingCount = 0;
  private incomingBytes = 0;
  private outgoingCount = 0;

  constructor(
    private readonly options: {
      model: RealtimeModel;
      send: (data: unknown) => void;
      onEvent: (event: RealtimeServerEvent) => void | Promise<void>;
      onError: (error: Error) => void;
      onFatalError?: (error: Error) => void;
      maxPending?: number;
      maxPendingBytes?: number;
    },
  ) {
    this.parse =
      options.model.createServerEventParser?.() ??
      options.model.parseServerEvent.bind(options.model);
  }

  get hasPendingIncoming(): boolean {
    return this.incomingCount > 0;
  }

  dispose(): void {
    this.active = false;
  }

  /** Stop accepting messages, but deliver already received terminal events. */
  async finish(): Promise<void> {
    this.stopWriting();
    await this.incoming;
    this.dispose();
  }

  stopWriting(): void {
    this.finishing = true;
  }

  send(event: RealtimeClientEvent, shouldSend?: () => boolean): Promise<void> {
    if (!this.active || this.finishing)
      throw new Error('Realtime connection is closed');
    if (this.outgoingCount >= (this.options.maxPending ?? 512)) {
      throw new Error('Realtime outgoing queue is full');
    }
    this.outgoingCount++;
    const operation = this.outgoing.then(async () => {
      if (!this.active || this.finishing)
        throw new Error('Realtime connection is closed');
      if (shouldSend?.() === false) return;
      if (!this.active || this.finishing)
        throw new Error('Realtime connection is closed');
      const data = await this.options.model.serializeClientEvent(event);
      if (!this.active || this.finishing)
        throw new Error('Realtime connection is closed');
      if (shouldSend?.() === false) return;
      if (!this.active || this.finishing)
        throw new Error('Realtime connection is closed');
      if (data != null) this.options.send(data);
    });
    this.outgoing = operation
      .catch(error => {
        if (!this.finishing) {
          // Capture owners escalate automatic audio failures; manual sends stay recoverable.
          if (
            event.type !== 'input-audio-append' &&
            event.type !== 'session-close'
          )
            this.report(error);
        }
      })
      .finally(() => {
        this.outgoingCount--;
      });
    return operation;
  }

  receive(data: unknown): void {
    if (!this.active || this.finishing) return;
    const bytes =
      typeof data === 'string'
        ? data.length * 2
        : data instanceof Blob
          ? data.size
          : ((data as ArrayBuffer).byteLength ?? 0);
    if (
      this.incomingCount >= (this.options.maxPending ?? 512) ||
      this.incomingBytes + bytes >
        (this.options.maxPendingBytes ?? 8 * 1024 * 1024)
    ) {
      this.finishing = true;
      this.fatal(
        new Error(
          'Realtime incoming queue is full; input protocol continuity was lost',
        ),
      );
      return;
    }
    this.incomingCount++;
    this.incomingBytes += bytes;
    this.incoming = this.incoming
      .then(async () => {
        if (!this.active) return;
        const text =
          typeof data === 'string'
            ? data
            : data instanceof Blob
              ? await data.text()
              : new TextDecoder().decode(data as ArrayBuffer);
        const parsed = await safeParseJSON({ text });
        if (!this.active) return;
        if (!parsed.success)
          throw new Error('Invalid JSON in realtime server message', {
            cause: parsed.error,
          });
        const health = this.options.model.getHealthCheckResponse?.(
          parsed.value,
        );
        if (!this.active) return;
        if (health != null && !this.finishing) this.options.send(health);
        if (!this.active) return;
        const result = this.parse(parsed.value);
        for (const event of Array.isArray(result) ? result : [result]) {
          if (!this.active) return;
          try {
            await this.options.onEvent(event);
          } catch (error) {
            this.report(error);
          }
        }
      })
      .catch(error => {
        this.finishing = true;
        this.fatal(error instanceof Error ? error : new Error(String(error)));
      })
      .finally(() => {
        this.incomingCount--;
        this.incomingBytes -= bytes;
      });
  }

  private fatal(error: Error): void {
    if (!this.active) return;
    if (this.options.onFatalError != null) this.options.onFatalError(error);
    else this.report(error);
  }

  private report(error: unknown): void {
    if (!this.active) return;
    try {
      this.options.onError(
        error instanceof Error ? error : new Error(String(error)),
      );
    } catch {
      /* Application callbacks cannot break the codec queue. */
    }
  }
}
