import type { RingCentralSipConfig } from "./config.ts";
import { Buffer } from "node:buffer";
import type {
  RingCentralInvite,
  RingCentralMediaCall,
  RingCentralMediaClient,
} from "./ringcentral-bridge.ts";
import type { AudioFrame } from "./types.ts";

type SoftphoneConstructor = new (config: {
  domain: string;
  outboundProxy: string;
  username: string;
  password: string;
  authorizationId: string;
  codec?: "OPUS/16000" | "OPUS/48000/2" | "PCMU/8000";
}) => SoftphoneLike;

interface SoftphoneLike {
  enableDebugMode?: () => void;
  register: () => Promise<void>;
  answer: (inviteMessage: unknown) => Promise<CallSessionLike>;
  on: (event: "invite", handler: (inviteMessage: unknown) => void | Promise<void>) => void;
}

interface CallSessionLike {
  callId?: string | (() => string | undefined);
  streamAudio?: (audio: Buffer) => StreamerLike | Promise<StreamerLike> | void;
  transfer?: (destination: string) => Promise<void> | void;
  hangup?: () => Promise<void> | void;
  on?: (
    event: "audioPacket" | "dtmf" | "disposed",
    handler: (payload?: unknown) => void | Promise<void>,
  ) => void;
}

interface StreamerLike {
  once?: (event: "finished", handler: () => void) => void;
}

export interface RingCentralSoftphoneAdapterOptions {
  sip: RingCentralSipConfig;
  softphoneConstructor?: SoftphoneConstructor;
}

export class RingCentralSoftphoneAdapter implements RingCentralMediaClient {
  private inviteHandler?: (invite: RingCentralInvite, call: RingCentralMediaCall) => Promise<void>;
  private softphone?: SoftphoneLike;

  constructor(private readonly opts: RingCentralSoftphoneAdapterOptions) {}

  onInvite(handler: (invite: RingCentralInvite, call: RingCentralMediaCall) => Promise<void>): void {
    this.inviteHandler = handler;
  }

  async register(): Promise<void> {
    const Softphone = this.opts.softphoneConstructor ?? (await loadSoftphoneConstructor());
    this.softphone = new Softphone({
      domain: this.opts.sip.domain,
      outboundProxy: this.opts.sip.outboundProxy,
      username: this.opts.sip.username,
      password: this.opts.sip.password,
      authorizationId: this.opts.sip.authorizationId,
      codec: this.opts.sip.codec,
    });

    if (this.opts.sip.debug) this.softphone.enableDebugMode?.();

    this.softphone.on("invite", async (inviteMessage) => {
      if (!this.inviteHandler || !this.softphone) return;
      const callSession = await this.softphone.answer(inviteMessage);
      const call = new RingCentralSoftphoneCall(callSession, Boolean(this.opts.sip.debug));
      await this.inviteHandler(
        {
          callId: callIdFromSession(callSession) ?? `rc-${Date.now()}`,
          callerNumber: phoneNumberFromInviteHeader(inviteMessage, "From"),
          calledNumber: phoneNumberFromInviteHeader(inviteMessage, "To"),
        },
        call,
      );
    });

    await this.softphone.register();
  }
}

class RingCentralSoftphoneCall implements RingCentralMediaCall {
  private audioHandlers: Array<(frame: AudioFrame) => void | Promise<void>> = [];
  private dtmfHandlers: Array<(digit: string) => void | Promise<void>> = [];
  private closeHandlers: Array<() => void | Promise<void>> = [];
  private outputQueue: Promise<void> = Promise.resolve();

  constructor(
    private readonly session: CallSessionLike,
    private readonly debug: boolean,
  ) {
    this.session.on?.("audioPacket", (payload) => {
      const frame = audioFrameFromSoftphonePayload(payload);
      for (const handler of this.audioHandlers) void handler(frame);
    });
    this.session.on?.("dtmf", (payload) => {
      const digit = typeof payload === "string" ? payload : String((payload as { digit?: string })?.digit ?? "");
      for (const handler of this.dtmfHandlers) void handler(digit);
    });
    const close = () => {
      for (const handler of this.closeHandlers) void handler();
    };
    this.session.on?.("disposed", close);
  }

  async answer(): Promise<void> {
    // The adapter answers before creating this wrapper because the SDK returns
    // the call session from softphone.answer(inviteMessage).
  }

  async sendAudio(frame: AudioFrame): Promise<void> {
    if (frame.data.length === 0 || !this.session.streamAudio) return;
    const current = this.outputQueue.catch(() => undefined).then(() => this.streamFrame(frame));
    this.outputQueue = current.catch((err) => this.logOutputError(err));
    await this.outputQueue;
  }

  private async streamFrame(frame: AudioFrame): Promise<void> {
    const result = this.session.streamAudio?.(Buffer.from(frame.data));
    const streamer = isPromiseLike(result) ? await result : result;
    if (streamer?.once) {
      await new Promise<void>((resolve) => streamer.once!("finished", resolve));
    }
  }

  private logOutputError(err: unknown): void {
    if (this.debug) console.debug("RingCentral streamAudio frame failed; continuing.", err);
  }

  async transfer(destination: string): Promise<void> {
    await this.session.transfer?.(destination);
  }

  async hangup(): Promise<void> {
    await this.session.hangup?.();
  }

  onAudio(handler: (frame: AudioFrame) => void | Promise<void>): void {
    this.audioHandlers.push(handler);
  }

  onDtmf(handler: (digit: string) => void | Promise<void>): void {
    this.dtmfHandlers.push(handler);
  }

  onClose(handler: () => void | Promise<void>): void {
    this.closeHandlers.push(handler);
  }
}

function isPromiseLike(value: unknown): value is PromiseLike<StreamerLike | void> {
  return Boolean(value && typeof (value as { then?: unknown }).then === "function");
}

async function loadSoftphoneConstructor(): Promise<SoftphoneConstructor> {
  try {
    const moduleName = "ringcentral-softphone";
    const mod = (await import(moduleName)) as {
      default?: SoftphoneConstructor;
    };
    if (!mod.default) throw new Error("default export missing");
    return mod.default;
  } catch (err) {
    throw new Error(
      `ringcentral-softphone is required for live RingCentral media. Install it after SIP credentials are available. Cause: ${
        err instanceof Error ? err.message : String(err)
      }`,
    );
  }
}

function callIdFromSession(session: CallSessionLike): string | undefined {
  if (typeof session.callId === "function") return session.callId();
  return session.callId;
}

function phoneNumberFromInviteHeader(inviteMessage: unknown, headerName: string): string | undefined {
  const header =
    inviteMessage &&
    typeof inviteMessage === "object" &&
    typeof (inviteMessage as { getHeader?: unknown }).getHeader === "function"
      ? (inviteMessage as { getHeader: (name: string) => unknown }).getHeader(headerName)
      : undefined;
  if (typeof header !== "string") return undefined;
  const match = header.match(/(?:tel:|sip:)(\+?[0-9]+)/i);
  return match?.[1];
}

function audioFrameFromSoftphonePayload(payload: unknown): AudioFrame {
  const bytes =
    payload instanceof Uint8Array
      ? payload
      : payload &&
          typeof payload === "object" &&
          "payload" in payload &&
          (payload as { payload?: unknown }).payload instanceof Uint8Array
        ? (payload as { payload: Uint8Array }).payload
        : new Uint8Array();

  return {
    data: bytes,
    format: { codec: "pcm16", sampleRateHz: 16000, channels: 1 },
  };
}
