import { Logger } from "@/utils/logger";

const log = new Logger("VoiceCallTransport");
const PROTOCOL_VERSION = 2 as const;
const WS_OPEN = 1;
const WS_CLOSING = 2;

export interface CallStartedPayload {
  callId: string;
  turnId: string;
  characterId: string;
  costPerPaidTurn: number;
  freeTurnsRemaining: number;
  creditBalance: number;
  canContinue: boolean;
  resumeVersion: number;
}

export interface CallBillingStatusPayload {
  callId?: string;
  turnId?: string;
  nextTurnId?: string;
  status: string;
  creditsCharged: number;
  creditBalance: number;
  freeTurnsRemaining: number;
  requiredCredits: number;
  shortfallCredits: number;
  canContinue: boolean;
}

export interface CallAudioChunkMeta {
  callId: string;
  turnId: string;
  index: number;
  mimeType: string;
  byteLength: number;
}

export interface VoiceCallTransportOptions {
  webSocketFactory?: (url: string) => WebSocket;
  reconnectBaseMs?: number;
}

export class VoiceCallTransport {
  private socket: WebSocket | null = null;
  private disposed = false;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private reconnectAttempt = 0;
  private disconnectedAt = 0;
  private pendingAudioMeta: CallAudioChunkMeta | null = null;
  private readonly factory: (url: string) => WebSocket;
  private readonly reconnectBaseMs: number;

  onOpen: (() => void) | null = null;
  onReconnecting: ((delayMs: number) => void) | null = null;
  onStarted: ((payload: CallStartedPayload) => void) | null = null;
  onState: ((state: "listening" | "thinking" | "speaking") => void) | null = null;
  onTranscript: ((text: string, final: boolean) => void) | null = null;
  onAiSentence: ((text: string, index: number) => void) | null = null;
  onBillingStatus: ((payload: CallBillingStatusPayload) => void) | null = null;
  onAudioChunk: ((meta: CallAudioChunkMeta, bytes: ArrayBuffer) => void) | null = null;
  onInterrupted: (() => void) | null = null;
  onEnded: (() => void) | null = null;
  onError: ((code: string, message: string) => void) | null = null;

  constructor(
    private readonly serverUrl: string,
    private readonly token: string,
    options: VoiceCallTransportOptions = {},
  ) {
    this.factory = options.webSocketFactory ?? ((url) => new WebSocket(url));
    this.reconnectBaseMs = options.reconnectBaseMs ?? 400;
  }

  get isConnected(): boolean {
    return this.socket?.readyState === WS_OPEN;
  }

  connect(): void {
    if (this.disposed || (this.socket && this.socket.readyState < WS_CLOSING)) return;
    const separator = this.serverUrl.includes("?") ? "&" : "?";
    const url = `${this.serverUrl}${separator}token=${encodeURIComponent(this.token)}`;
    const socket = this.factory(url);
    socket.binaryType = "arraybuffer";
    this.socket = socket;
    socket.onopen = () => {
      this.reconnectAttempt = 0;
      if (this.disconnectedAt) {
        this.sendClientMetric("reconnectLatency", Date.now() - this.disconnectedAt);
        this.disconnectedAt = 0;
      }
      this.onOpen?.();
    };
    socket.onmessage = (event) => this.handleMessage(event.data);
    socket.onerror = () => this.onError?.("CALL_NETWORK_ERROR", "通话网络连接异常");
    socket.onclose = () => {
      this.socket = null;
      this.disconnectedAt = Date.now();
      if (!this.disposed) this.scheduleReconnect();
    };
  }

  startCall(characterId: string, turnId: string, resumeVersion?: number): boolean {
    return this.sendJson({
      type: "call_start", protocolVersion: PROTOCOL_VERSION, characterId, turnId,
      ...(resumeVersion ? { resumeVersion } : {}),
    });
  }

  startAudio(callId: string, turnId: string, mimeType: string, sampleRate: number): boolean {
    return this.sendJson({
      type: "call_audio_start", protocolVersion: PROTOCOL_VERSION,
      callId, turnId, encoding: "opus", mimeType, sampleRate,
    });
  }

  sendAudioChunk(bytes: ArrayBuffer): boolean {
    if (!this.isConnected) return false;
    this.socket?.send(bytes);
    return true;
  }

  endAudio(callId: string, turnId: string, vadLatency = 0): boolean {
    return this.sendJson({
      type: "call_audio_end", protocolVersion: PROTOCOL_VERSION, callId, turnId,
      vadLatency,
    });
  }

  sendClientMetric(name: "audioChunkGap" | "reconnectLatency", milliseconds: number): boolean {
    return this.sendJson({
      type: "call_client_metric",
      protocolVersion: PROTOCOL_VERSION,
      name,
      milliseconds: Math.max(0, Math.min(60_000, milliseconds)),
    });
  }

  interrupt(callId: string): boolean {
    return this.sendJson({
      type: "call_interrupt", protocolVersion: PROTOCOL_VERSION, callId,
    });
  }

  endCall(callId: string): boolean {
    return this.sendJson({
      type: "call_end", protocolVersion: PROTOCOL_VERSION, callId,
    });
  }

  disconnect(): void {
    this.disposed = true;
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
    this.reconnectTimer = null;
    this.socket?.close();
    this.socket = null;
  }

  private sendJson(payload: Record<string, unknown>): boolean {
    if (!this.isConnected) return false;
    this.socket?.send(JSON.stringify(payload));
    return true;
  }

  private scheduleReconnect(): void {
    const delayMs = Math.min(1600, this.reconnectBaseMs * 2 ** this.reconnectAttempt);
    this.reconnectAttempt += 1;
    this.onReconnecting?.(delayMs);
    this.reconnectTimer = setTimeout(() => this.connect(), delayMs);
  }

  private handleMessage(data: unknown): void {
    if (data instanceof ArrayBuffer) {
      const meta = this.pendingAudioMeta;
      this.pendingAudioMeta = null;
      if (meta && meta.byteLength === data.byteLength) this.onAudioChunk?.(meta, data);
      else if (meta) this.onError?.("CALL_AUDIO_FRAME_INVALID", "通话音频帧不完整");
      return;
    }
    if (typeof data !== "string") return;
    let payload: Record<string, unknown>;
    try {
      payload = JSON.parse(data) as Record<string, unknown>;
    } catch (error) {
      log.warn("Ignored malformed call event", error);
      return;
    }
    const type = String(payload.type ?? "");
    if (type === "call_started") {
      this.onStarted?.(payload as unknown as CallStartedPayload);
    } else if (type === "call_state") {
      this.onState?.(String(payload.state) as "listening" | "thinking" | "speaking");
    } else if (type === "call_stt_partial" || type === "call_stt_final") {
      this.onTranscript?.(String(payload.text ?? ""), type === "call_stt_final");
    } else if (type === "call_ai_sentence") {
      this.onAiSentence?.(String(payload.text ?? ""), Number(payload.index ?? 0));
    } else if (type === "call_billing_status") {
      this.onBillingStatus?.(payload as unknown as CallBillingStatusPayload);
    } else if (type === "call_audio_chunk_start") {
      this.pendingAudioMeta = payload as unknown as CallAudioChunkMeta;
    } else if (type === "call_interrupted") {
      this.onInterrupted?.();
    } else if (type === "call_ended") {
      this.onEnded?.();
    } else if (type === "call_error") {
      this.onError?.(String(payload.errorCode ?? "CALL_ERROR"), String(payload.message ?? "通话失败"));
    }
  }
}
