import type {
  AudioFormat,
  AudioFrame,
  RingCentralCallMetadata,
  VoiceCallAuditRecord,
  VoiceCallToolAuditEntry,
  VoiceCallTranscriptEntry,
  VoiceProvider,
  VoiceProviderKind,
  VoiceProviderSession,
} from "./types.ts";
import { VoicePolicyProxy } from "./policy-proxy.ts";

export interface RingCentralInvite {
  callId: string;
  callerNumber?: string;
  calledNumber?: string;
  extensionId?: string;
  deviceId?: string;
  sessionId?: string;
  telephonySessionId?: string;
}

export interface RingCentralMediaCall {
  answer(): Promise<void>;
  sendAudio(frame: AudioFrame): Promise<void>;
  transfer(destination: string): Promise<void>;
  hangup(): Promise<void>;
  onAudio(handler: (frame: AudioFrame) => void | Promise<void>): void;
  onDtmf(handler: (digit: string) => void | Promise<void>): void;
  onClose(handler: () => void | Promise<void>): void;
}

export interface RingCentralMediaClient {
  onInvite(handler: (invite: RingCentralInvite, call: RingCentralMediaCall) => Promise<void>): void;
}

export interface RingCentralAudioTransform {
  toProviderInput(frame: AudioFrame): Promise<AudioFrame> | AudioFrame;
  toTelephonyOutput(frame: AudioFrame): Promise<AudioFrame> | AudioFrame;
}

export interface RingCentralBridgeOptions {
  voiceProvider: VoiceProvider;
  policyProxy: VoicePolicyProxy | ((invite: RingCentralInvite) => VoicePolicyProxy);
  humanTransferDestination: string;
  instructions: string;
  voice?: string;
  providerInputFormat?: AudioFormat;
  providerOutputFormat?: AudioFormat;
  audioTransform?: RingCentralAudioTransform;
  initialPrompt?: string;
  safetyIdentifierSecret?: string;
  onAudit?: (record: VoiceCallAuditRecord) => void | Promise<void>;
}

export class RingCentralBridge {
  constructor(
    private readonly client: RingCentralMediaClient,
    private readonly opts: RingCentralBridgeOptions,
  ) {}

  start(): void {
    this.client.onInvite(async (invite, call) => this.handleInvite(invite, call));
  }

  private async handleInvite(invite: RingCentralInvite, call: RingCentralMediaCall): Promise<void> {
    await call.answer();
    const startedAt = new Date().toISOString();
    const metadata: RingCentralCallMetadata = {
      extensionId: invite.extensionId,
      deviceId: invite.deviceId,
      sessionId: invite.sessionId,
      telephonySessionId: invite.telephonySessionId,
      callerNumber: invite.callerNumber,
      calledNumber: invite.calledNumber,
    };
    let disposition: VoiceCallAuditRecord["disposition"] = "resolved_by_ai";
    const transcripts: VoiceCallTranscriptEntry[] = [];
    const toolCalls: VoiceCallToolAuditEntry[] = [];
    let activeVoiceProvider = this.opts.voiceProvider.kind;
    let audited = false;
    const policyProxy =
      typeof this.opts.policyProxy === "function"
        ? this.opts.policyProxy(invite)
        : this.opts.policyProxy;
    const audioTransform = this.opts.audioTransform ?? passthroughAudioTransform;
    const auditOnce = async (notes?: string[]) => {
      if (audited) return;
      audited = true;
      await this.audit(
        invite.callId,
        activeVoiceProvider,
        metadata,
        disposition,
        startedAt,
        transcripts,
        toolCalls,
        notes,
      );
    };

    let voiceSession: VoiceProviderSession;
    try {
      voiceSession = await this.opts.voiceProvider.connect({
        callId: invite.callId,
        instructions: this.opts.instructions,
        voice: this.opts.voice,
        inputFormat:
          this.opts.providerInputFormat ?? { codec: "pcm16", sampleRateHz: 24000, channels: 1 },
        outputFormat:
          this.opts.providerOutputFormat ?? { codec: "pcm16", sampleRateHz: 24000, channels: 1 },
        tools: policyProxy.toolDefinitions(),
        safetyIdentifier:
          invite.callerNumber && this.opts.safetyIdentifierSecret
            ? await privacyPreservingIdentifier(invite.callerNumber, this.opts.safetyIdentifierSecret)
            : undefined,
        events: {
          onAudio: async (frame) => call.sendAudio(await audioTransform.toTelephonyOutput(frame)),
          onTranscript: (event) => {
            transcripts.push({ at: new Date().toISOString(), ...event });
          },
          onToolCall: async (toolCall) => {
            const result = await policyProxy.handleToolCall(toolCall);
            toolCalls.push({
              at: new Date().toISOString(),
              id: toolCall.id,
              name: toolCall.name,
              arguments: toolCall.arguments,
              result,
            });
            await voiceSession.sendToolResult(toolCall.id, result);
            if (result.action === "transfer_to_staff") {
              disposition = "transferred";
              metadata.transferDestination = this.opts.humanTransferDestination;
              await call.transfer(this.opts.humanTransferDestination);
            } else if (result.action === "callback_required") {
              disposition = "callback_required";
            } else if (result.action === "approval_required") {
              disposition = "amd_write_failed";
            }
          },
          onError: async () => {
            disposition = "rc_media_failed";
            metadata.transferDestination = this.opts.humanTransferDestination;
            await voiceSession.close("voice provider error").catch(() => undefined);
            await auditOnce(["Voice provider session reported an error."]);
            await call.transfer(this.opts.humanTransferDestination);
          },
        },
      });
      activeVoiceProvider = voiceSession.provider;
      if (this.opts.initialPrompt) await voiceSession.sendText(this.opts.initialPrompt);
    } catch {
      disposition = "voice_provider_failed";
      metadata.transferDestination = this.opts.humanTransferDestination;
      await auditOnce(["Voice provider connection failed before audio streaming started."]);
      await call.transfer(this.opts.humanTransferDestination);
      return;
    }

    call.onAudio(async (frame) => voiceSession.sendAudio(await audioTransform.toProviderInput(frame)));
    call.onDtmf((digit) => voiceSession.sendText(`Caller pressed DTMF ${digit}`));
    call.onClose(async () => {
      await voiceSession.close("RingCentral call closed");
      await auditOnce();
    });
  }

  private async audit(
    callId: string,
    voiceProvider: VoiceProviderKind,
    ringCentral: RingCentralCallMetadata,
    disposition: VoiceCallAuditRecord["disposition"],
    startedAt: string,
    transcripts: VoiceCallTranscriptEntry[],
    toolCalls: VoiceCallToolAuditEntry[],
    notes?: string[],
  ): Promise<void> {
    await this.opts.onAudit?.({
      callId,
      telephonyProvider: "ringcentral",
      voiceProvider,
      ringCentral,
      disposition,
      startedAt,
      endedAt: new Date().toISOString(),
      transcripts,
      toolCalls,
      notes,
    });
  }
}

const passthroughAudioTransform: RingCentralAudioTransform = {
  toProviderInput: (frame) => frame,
  toTelephonyOutput: (frame) => frame,
};

async function privacyPreservingIdentifier(value: string, secret: string): Promise<string> {
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(value));
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
