import {
  AdvancedMdMcpReadGateway,
  type AdvancedMdReadGateway,
} from "./advancedmd-gateway.ts";
import { ReadOnlyAdvancedMdSchedulingPort } from "./advancedmd-scheduling-port.ts";
import {
  loadVoiceAgentConfig,
  type McpEndpointConfig,
  type VoiceAgentConfig,
} from "./config.ts";
import { createPcm16ResamplerTransform } from "./audio-transform.ts";
import { McpToolClient, type McpToolCaller } from "./mcp-tool-client.ts";
import { VoicePolicyProxy, type SchedulingToolPort } from "./policy-proxy.ts";
import { createVoiceProvider } from "./provider-factory.ts";
import {
  RingCentralBridge,
  type RingCentralInvite,
  type RingCentralMediaClient,
} from "./ringcentral-bridge.ts";
import { RingCentralSoftphoneAdapter } from "./ringcentral-softphone-adapter.ts";
import type { ApprovalToken, VoiceCallAuditRecord, VoiceProvider } from "./types.ts";

export const EXULT_VOICE_AGENT_INSTRUCTIONS = [
  "You are Exult Healthcare's backup scheduling voice agent.",
  "Handle overflow and after-hours rescheduling only.",
  'Your first spoken message must be exactly: "Hi, this is Exult Healthcare. How can I help you?"',
  "If the caller asks to reschedule an appointment, say that calls may be recorded for quality and continuity of care, then ask for their legal name and date of birth.",
  "Before discussing appointment details, verify the caller through the verify_patient_identity tool using legal name, DOB, and the incoming caller phone already available to the server.",
  "When verification returns a next_appointment, say that you see their next appointment is on that date/time with that provider, then ask when they would like to reschedule.",
  "If no next_appointment is returned, offer a staff transfer or callback instead of guessing appointment details.",
  "If verify_patient_identity returns no_candidates, name_mismatch, or dob_mismatch, do not transfer immediately. First confirm what you heard by saying: \"I don't see you in our system. I have your name as [name] and DOB as [Month Day, Year]. Is that right?\" If the caller confirms both values were heard correctly, then transfer to staff with: \"Let me get a staff member to help look you up.\" If the caller corrects either the name or DOB, re-run verify_patient_identity with the corrected values. After 2 total verify_patient_identity attempts, transfer to staff regardless with: \"Let me get a staff member to help — they can look you up directly.\" Do not silently transfer.",
  "If verify_patient_identity returns multiple_candidates, ask the caller to confirm their address or phone number to narrow it down, then transfer to staff if still ambiguous.",
  "Escalate urgent symptoms, controlled-substance/refill requests, billing, insurance, guardian issues, multiple matches, or failed verification.",
  "Never claim an appointment changed until the server returns a verified AdvancedMD write result.",
  "Keep responses brief, calm, and natural for a phone call.",
].join(" ");

export const EXULT_VOICE_AGENT_START_PROMPT =
  'Say exactly: "Hi, this is Exult Healthcare. How can I help you?"';

export interface VoiceAgentRuntimeOptions {
  config?: VoiceAgentConfig;
  voiceProvider?: VoiceProvider;
  mediaClient?: RingCentralMediaClient;
  scheduling?: SchedulingToolPort | ((invite: RingCentralInvite) => SchedulingToolPort);
  advancedMdReads?: AdvancedMdReadGateway;
  mcpClientFactory?: (endpoint: McpEndpointConfig) => McpToolCaller;
  humanTransferDestination?: string;
  instructions?: string;
  initialPrompt?: string;
  writeApproval?: ApprovalToken;
  onAudit?: (record: VoiceCallAuditRecord) => void | Promise<void>;
}

export interface VoiceAgentRuntimeStartResult {
  provider: string;
  media: "injected" | "ringcentral_softphone";
  advancedMd: "injected" | "mcp" | "not_used";
}

export class VoiceAgentRuntime {
  private readonly mcpClients: McpToolCaller[] = [];
  private bridge?: RingCentralBridge;
  private mediaClient?: RingCentralMediaClient;

  constructor(private readonly opts: VoiceAgentRuntimeOptions = {}) {}

  async start(): Promise<VoiceAgentRuntimeStartResult> {
    const config = this.opts.config ?? loadVoiceAgentConfig();
    const provider =
      this.opts.voiceProvider ?? createVoiceProvider(config.defaultVoiceProvider, config);
    const media = this.opts.mediaClient ?? createSoftphoneClient(config);
    const transferDestination =
      this.opts.humanTransferDestination ?? process.env.HUMAN_TRANSFER_DESTINATION;
    if (!transferDestination) {
      throw new Error("HUMAN_TRANSFER_DESTINATION is required for staff transfers.");
    }
    const reads = await this.resolveAdvancedMdReads(config);

    this.bridge = new RingCentralBridge(media, {
      voiceProvider: provider,
      policyProxy: (invite) =>
        new VoicePolicyProxy(
          this.resolveSchedulingPort(invite, config, reads),
          this.opts.writeApproval,
        ),
      humanTransferDestination: transferDestination,
      instructions: this.opts.instructions ?? EXULT_VOICE_AGENT_INSTRUCTIONS,
      initialPrompt: this.opts.initialPrompt ?? EXULT_VOICE_AGENT_START_PROMPT,
      providerInputFormat: config.audio.providerFormat,
      providerOutputFormat: config.audio.providerFormat,
      audioTransform: createPcm16ResamplerTransform({
        telephonyFormat: config.audio.ringCentralFormat,
        providerFormat: config.audio.providerFormat,
      }),
      safetyIdentifierSecret: config.openai.safetyIdentifierSecret,
      onAudit: this.opts.onAudit,
    });
    this.bridge.start();
    this.mediaClient = media;

    if (hasRegister(media)) await media.register();

    return {
      provider: provider.kind,
      media: this.opts.mediaClient ? "injected" : "ringcentral_softphone",
      advancedMd: this.opts.advancedMdReads ? "injected" : reads ? "mcp" : "not_used",
    };
  }

  async stop(): Promise<void> {
    for (const client of this.mcpClients.splice(0)) {
      await client.close();
    }
  }

  private async resolveAdvancedMdReads(
    config: VoiceAgentConfig,
  ): Promise<AdvancedMdReadGateway | undefined> {
    if (this.opts.advancedMdReads) return this.opts.advancedMdReads;
    if (this.opts.scheduling) return undefined;

    if (!config.mcp.advancedmd.bearerToken) {
      throw new Error(
        "MCP_BEARER_TOKEN_ADVANCEDMD (or legacy MCP_BEARER_TOKEN) is required for live AdvancedMD read tools.",
      );
    }

    const client =
      this.opts.mcpClientFactory?.(config.mcp.advancedmd) ??
      new McpToolClient({
        name: config.mcp.advancedmd.name,
        mcpUrl: config.mcp.advancedmd.mcpUrl,
        bearerToken: config.mcp.advancedmd.bearerToken,
      });
    this.mcpClients.push(client);
    return new AdvancedMdMcpReadGateway(client);
  }

  private resolveSchedulingPort(
    invite: RingCentralInvite,
    config: VoiceAgentConfig,
    reads?: AdvancedMdReadGateway,
  ): SchedulingToolPort {
    if (typeof this.opts.scheduling === "function") return this.opts.scheduling(invite);
    if (this.opts.scheduling) return this.opts.scheduling;
    if (!reads) {
      throw new Error("AdvancedMD reads are required when a scheduling port is not injected.");
    }
    return new ReadOnlyAdvancedMdSchedulingPort({
      reads,
      callerNumber: invite.callerNumber,
      lookupTimeoutMs: config.scheduling.amdLookupTimeoutMs,
    });
  }
}

function createSoftphoneClient(config: VoiceAgentConfig): RingCentralSoftphoneAdapter {
  if (!config.ringCentralSip) {
    throw new Error("RingCentral SIP credentials are required for live media.");
  }
  return new RingCentralSoftphoneAdapter({ sip: config.ringCentralSip });
}

function hasRegister(
  mediaClient: RingCentralMediaClient,
): mediaClient is RingCentralMediaClient & { register: () => Promise<void> } {
  return "register" in mediaClient && typeof mediaClient.register === "function";
}
