import { NextRequest, NextResponse } from 'next/server';
import { initDatabase } from '@server/db/init';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { generateResponse, TranscriptEntry } from '@server/services/voice/llm.service';
import { synthesizeSpeech } from '@server/services/voice/tts.service';
import { validateTwilioSignature } from '@server/services/voice/twilio-signature';

interface AgentRecord {
  id: string;
  system_prompt: string | null;
  greeting_script: string | null;
  closing_script: string | null;
  llm_model_id: string | null;
  voice_model_id: string | null;
  voice_language_code: string | null;
  realtime_model: string | null;
  voice_provider_name: string | null;
  fallback_rules: Array<{ trigger: string; action: string; priority?: string }> | null;
  capabilities: Record<string, boolean> | null;
}

interface PhoneRecord {
  id: string;
  restaurant_id: string;
  assigned_agent_id: string | null;
}

interface SessionRecord {
  id: string;
  agent_id: string | null;
  restaurant_id: string | null;
  sip_call_id: string | null;
  transcript: TranscriptEntry[];
}

const AGENT_COLS = `a.id, a.system_prompt, a.greeting_script, a.closing_script,
  a.llm_model_id, a.voice_model_id, a.voice_language_code, a.realtime_model,
  a.fallback_rules, a.capabilities,
  vp.name AS voice_provider_name`;

function twimlResponse(xml: string): NextResponse {
  return new NextResponse(xml, { headers: { 'Content-Type': 'text/xml' } });
}

function escapeXml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function agentLanguageForGather(agent: AgentRecord | null): string {
  const code = agent?.voice_language_code || 'hi-IN';
  const map: Record<string, string> = {
    'hi-IN': 'hi-IN', 'en-IN': 'en-IN', 'ta-IN': 'ta-IN', 'te-IN': 'te-IN',
    'gu-IN': 'gu-IN', 'kn-IN': 'kn-IN', 'ml-IN': 'ml-IN', 'mr-IN': 'mr-IN',
    'pa-IN': 'pa-IN', 'od-IN': 'en-IN', 'bn-IN': 'bn-IN',
  };
  return map[code] || code;
}

const DEFAULT_PHONE_AGENT_PROMPT = `You are a friendly and professional AI phone agent for a restaurant. Your job is to assist callers efficiently and warmly over the phone.

Guidelines:
- Greet callers warmly and make them feel welcome.
- Help callers place food orders: ask for items, quantities, delivery or pickup, and confirm the order clearly.
- Assist with table reservations: ask for date, time, party size, and caller name; confirm the booking.
- Answer common questions about the restaurant such as opening hours, location, menu items, pricing, and allergens — use any information provided to you.
- If a caller is upset or the situation is complex, offer to connect them with a staff member.
- Keep responses concise — callers are on the phone, so avoid long paragraphs.
- Speak naturally and confirm back important details (order items, reservation time, etc.) before finalising.
- Do not make up menu items, prices, or hours you are not certain about — say you will check and offer to call back if needed.
- Always be polite, patient, and professional, even if the caller repeats themselves or is difficult to understand.`;

interface BranchHoursSlot { open: string; close: string; closed?: boolean }

const DAY_NAMES: Record<string, string> = {
  monday: 'Monday', tuesday: 'Tuesday', wednesday: 'Wednesday',
  thursday: 'Thursday', friday: 'Friday', saturday: 'Saturday', sunday: 'Sunday',
};

function formatBranchHoursForPrompt(
  hours: Record<string, BranchHoursSlot> | null,
  timezone: string | null
): string | null {
  if (!hours || typeof hours !== 'object') return null;
  const lines: string[] = [];
  for (const day of ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']) {
    const slot = hours[day];
    if (!slot) continue;
    if (slot.closed) {
      lines.push(`${DAY_NAMES[day]}: Closed`);
    } else if (slot.open && slot.close) {
      lines.push(`${DAY_NAMES[day]}: ${slot.open} – ${slot.close}`);
    }
  }
  if (lines.length === 0) return null;
  const tzNote = timezone ? ` (${timezone})` : '';
  return `Working Hours${tzNote}:\n${lines.join('\n')}`;
}

async function fetchBranchHoursContext(restaurantId: string): Promise<string | null> {
  try {
    /* raw: SELECT name, hours, timezone FROM branches WHERE restaurant_id = $1 AND is_active = true LIMIT 5 */
    const { rows } = await db.execute(sql`
      SELECT name, hours, timezone FROM branches WHERE restaurant_id = ${restaurantId} AND is_active = true LIMIT 5
    `);
    if (!rows || rows.length === 0) return null;
    const parts: string[] = [];
    for (const b of rows as Array<{ name: string; hours: Record<string, BranchHoursSlot> | null; timezone: string | null }>) {
      const hoursStr = formatBranchHoursForPrompt(b.hours, b.timezone);
      if (hoursStr) {
        parts.push(`Branch "${b.name}":\n${hoursStr}`);
      }
    }
    if (parts.length === 0) return null;
    const now = new Date().toLocaleString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' });
    return `\n\n--- Restaurant Operating Hours ---\n${parts.join('\n\n')}\nCurrent date/time: ${now}\nIf the restaurant is currently closed, politely inform the caller of the next opening time and offer to take their order or reservation for when the restaurant reopens.\n--- End Operating Hours ---`;
  } catch {
    return null;
  }
}

function buildSystemPrompt(agent: AgentRecord | null, branchHoursContext?: string | null): string {
  const parts: string[] = [];
  parts.push(agent?.system_prompt?.trim() || DEFAULT_PHONE_AGENT_PROMPT);

  const capLabels: Record<string, string> = {
    order_taking: 'taking food orders',
    reservation_booking: 'booking table reservations',
    faq_answering: 'answering frequently asked questions',
    escalation: 'escalating to a human agent when needed',
  };
  const enabledCaps = Object.entries(agent?.capabilities || {})
    .filter(([, enabled]) => enabled)
    .map(([key]) => capLabels[key] || key.replace(/_/g, ' '));
  if (enabledCaps.length > 0) {
    parts.push(`\n\nYou are enabled to assist with: ${enabledCaps.join(', ')}.`);
  }

  const rules = (agent?.fallback_rules || []).filter(r => r.trigger?.trim() && r.action?.trim());
  if (rules.length > 0) {
    const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
    const sorted = [...rules].sort((a, b) =>
      (priorityOrder[a.priority || 'medium'] ?? 1) - (priorityOrder[b.priority || 'medium'] ?? 1)
    );
    const ruleLines = sorted.map(r => `- If ${r.trigger}: ${r.action}`).join('\n');
    parts.push(`\n\nFallback rules to follow:\n${ruleLines}`);
  }

  if (branchHoursContext) {
    parts.push(branchHoursContext);
  }

  return parts.join('');
}

async function resolveTokenByRestaurantId(restaurantId: string): Promise<string | null> {
  /* raw: SELECT twilio_auth_token, twilio_connected FROM telephone_settings WHERE restaurant_id = $1 */
  const { rows } = await db.execute(sql`
    SELECT twilio_auth_token, twilio_connected FROM telephone_settings WHERE restaurant_id = ${restaurantId}
  `);
  const settingsRow = rows[0] as { twilio_auth_token: string | null; twilio_connected: boolean } | undefined;
  if (settingsRow?.twilio_connected && settingsRow.twilio_auth_token) {
    try {
      const { decrypt } = await import('@server/utils/crypto');
      return decrypt(settingsRow.twilio_auth_token);
    } catch {
      return settingsRow.twilio_auth_token;
    }
  }
  return null;
}

async function resolveAuthToken(opts: { callSid?: string; sessionId?: string; fromNumber?: string; toNumber?: string }): Promise<string | null> {
  const envToken = process.env.TWILIO_AUTH_TOKEN;

  if (opts.sessionId) {
    try {
      const { rows } = await db.execute(sql`SELECT restaurant_id FROM sip_call_sessions WHERE id = ${opts.sessionId}`);
      const session = rows[0] as { restaurant_id: string } | undefined;
      if (session) {
        const token = await resolveTokenByRestaurantId(session.restaurant_id);
        if (token) return token;
      }
    } catch { /* fall through */ }
  }

  if (opts.callSid) {
    try {
      const { rows } = await db.execute(sql`SELECT restaurant_id FROM sip_call_sessions WHERE sip_call_id = ${opts.callSid}`);
      const session = rows[0] as { restaurant_id: string } | undefined;
      if (session) {
        const token = await resolveTokenByRestaurantId(session.restaurant_id);
        if (token) return token;
      }
    } catch { /* fall through */ }
  }

  const numbers = [opts.toNumber, opts.fromNumber].filter(Boolean);
  for (const num of numbers) {
    if (!num) continue;
    try {
      const didNumber = num.replace(/^\+/, '');
      const { rows } = await db.execute(sql`
        SELECT restaurant_id FROM phone_numbers WHERE number IN (${sql.join([num, didNumber].map(n => sql`${n}`), sql`, `)}) AND is_active = true LIMIT 1
      `);
      const phoneRow = rows[0] as { restaurant_id: string } | undefined;
      if (phoneRow) {
        const token = await resolveTokenByRestaurantId(phoneRow.restaurant_id);
        if (token) return token;
      }
    } catch { /* fall through */ }
  }

  return envToken || null;
}

async function verifyRequest(req: NextRequest, params: Record<string, string>): Promise<boolean> {
  const signature = req.headers.get('x-twilio-signature');
  const callSid = params['CallSid'] || '';
  const sessionId = params['sessionId'] || req.nextUrl.searchParams.get('sessionId') || '';
  const authToken = await resolveAuthToken({
    callSid, sessionId,
    fromNumber: params['From'] || '',
    toNumber: params['To'] || '',
  });

  if (process.env.NODE_ENV === 'production' && !authToken) {
    console.error('[Twilio Webhook] No auth token found in production');
    return false;
  }

  if (!signature) {
    if (process.env.NODE_ENV === 'production') {
      console.warn('[Twilio Webhook] Missing X-Twilio-Signature in production');
      return false;
    }
    return true;
  }

  if (!authToken) return true;

  const url = `https://${req.headers.get('host')}${req.nextUrl.pathname}${req.nextUrl.search}`;
  return validateTwilioSignature(authToken, signature, url, params);
}

async function extractPostParams(req: NextRequest): Promise<Record<string, string>> {
  const params: Record<string, string> = {};
  if (req.method === 'POST') {
    try {
      const formData = await req.formData();
      formData.forEach((val, key) => { params[key] = String(val); });
    } catch (formErr: unknown) {
      console.warn(`[Twilio Webhook] Failed to parse POST body: ${formErr}`);
    }
  }
  return params;
}

async function handleVoiceWebhook(req: NextRequest): Promise<NextResponse> {
  await initDatabase();

  const postParams = await extractPostParams(req);

  if (!(await verifyRequest(req, postParams))) {
    return new NextResponse('Forbidden', { status: 403 });
  }

  const qp = req.nextUrl.searchParams;

  const transferTwiml = qp.get('transferTwiml') || '';
  if (transferTwiml) {
    return twimlResponse(decodeURIComponent(transferTwiml));
  }

  const conferenceName = qp.get('conferenceName') || '';
  if (conferenceName) {
    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>You are joining the conference.</Say>
  <Dial><Conference>${escapeXml(conferenceName)}</Conference></Dial>
</Response>`);
  }

  const callSid = postParams['CallSid'] || '';
  const from = postParams['From'] || '';
  const to = postParams['To'] || '';
  const speechResult = postParams['SpeechResult'] || '';
  const callStatus = postParams['CallStatus'] || '';
  const sessionId = postParams['sessionId'] || qp.get('sessionId') || '';

  if (callStatus === 'completed' || callStatus === 'failed' ||
      callStatus === 'busy' || callStatus === 'no-answer') {
    return handleCallEnd(callSid, callStatus, postParams['CallDuration'] || '0');
  }

  const resolvedHost =
    req.headers.get('host') ||
    process.env.WEBHOOK_BASE_URL?.replace(/^https?:\/\//, '');
  if (!resolvedHost) {
    console.error('[Twilio Webhook] Cannot determine base URL: set WEBHOOK_BASE_URL to your public HTTPS app URL');
    return new NextResponse('Service misconfiguration', { status: 503 });
  }
  const baseUrl = `https://${resolvedHost}`.replace(/\/+$/, '');

  if (!speechResult && !sessionId) {
    return handleNewCall(callSid, from, to, baseUrl);
  }

  if (sessionId) {
    return handleSpeechInput(sessionId, speechResult, baseUrl);
  }

  return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Thank you for calling. Goodbye!</Say>
  <Hangup/>
</Response>`);
}

export async function POST(req: NextRequest) {
  return handleVoiceWebhook(req);
}

export async function GET(req: NextRequest) {
  return handleVoiceWebhook(req);
}

async function handleCallEnd(callSid: string, status: string, durationStr: string) {
  const duration = parseInt(durationStr, 10) || 0;
  const dbStatus = status === 'completed' ? 'completed' : 'failed';

  /* raw: UPDATE sip_call_sessions SET status = $1, ended_at = NOW(), duration_seconds = $2 WHERE sip_call_id = $3 AND status = 'active' */
  await db.execute(sql`
    UPDATE sip_call_sessions SET status = ${dbStatus}, ended_at = NOW(), duration_seconds = ${duration}
    WHERE sip_call_id = ${callSid} AND status = 'active'
  `);

  return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>\n<Response/>`);
}

const AGENT_JOIN_SQL = `ai_agents a
  LEFT JOIN voice_models vm ON vm.id = a.voice_model_id
  LEFT JOIN voice_providers vp ON vp.id = vm.voice_provider_id`;

async function resolveAgent(phoneRow: PhoneRecord): Promise<AgentRecord | null> {
  if (phoneRow.assigned_agent_id) {
    /* raw: SELECT agent cols FROM ai_agents a LEFT JOIN ... WHERE a.id = $1 AND a.is_active = true AND 'voice' = ANY(a.channels) */
    const { rows } = await db.execute(sql`
      SELECT ${sql.raw(AGENT_COLS)} FROM ${sql.raw(AGENT_JOIN_SQL)}
      WHERE a.id = ${phoneRow.assigned_agent_id} AND a.is_active = true AND 'voice' = ANY(a.channels)
    `);
    const agent = rows[0] as unknown as AgentRecord | undefined;
    if (agent) return agent;
  }
  const { rows } = await db.execute(sql`
    SELECT ${sql.raw(AGENT_COLS)} FROM ${sql.raw(AGENT_JOIN_SQL)}
    WHERE a.restaurant_id = ${phoneRow.restaurant_id} AND a.is_default = true AND a.is_active = true AND 'voice' = ANY(a.channels)
  `);
  return (rows[0] as unknown as AgentRecord | undefined) ?? null;
}

async function resolveAgentById(agentId: string): Promise<AgentRecord | null> {
  const { rows } = await db.execute(sql`
    SELECT ${sql.raw(AGENT_COLS)} FROM ${sql.raw(AGENT_JOIN_SQL)} WHERE a.id = ${agentId}
  `);
  return (rows[0] as unknown as AgentRecord | undefined) ?? null;
}

function buildGatherTwiml(
  webhookUrl: string,
  language: string,
  promptAudioUrl: string | null,
  promptText: string,
  timeoutMessage: string
): string {
  const gather = promptAudioUrl
    ? `<Play>${escapeXml(promptAudioUrl)}</Play>`
    : `<Say>${escapeXml(promptText)}</Say>`;

  return `<?xml version="1.0" encoding="UTF-8"?>
<Response>
  ${gather}
  <Gather input="speech" action="${escapeXml(webhookUrl)}" method="POST" speechTimeout="auto" language="${escapeXml(language)}">
  </Gather>
  <Say>${escapeXml(timeoutMessage)}</Say>
  <Hangup/>
</Response>`;
}

function buildRealtimeStreamUrl(baseUrl: string, sessionId: string): string {
  const customStreamUrl = process.env.TWILIO_REALTIME_STREAM_URL;
  if (customStreamUrl) {
    return `${customStreamUrl}/stream/${encodeURIComponent(sessionId)}`;
  }
  const host = baseUrl.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
  return `wss://${host}/stream/${encodeURIComponent(sessionId)}`;
}

async function handleNewCall(callSid: string, from: string, to: string, baseUrl: string) {
  const didNumber = to.replace(/^\+/, '');

  /* raw: SELECT id, restaurant_id, assigned_agent_id FROM phone_numbers WHERE (number = $1 OR number = $2) AND is_active = true LIMIT 1 */
  const { rows: phoneRows } = await db.execute(sql`
    SELECT id, restaurant_id, assigned_agent_id FROM phone_numbers
    WHERE (number = ${to} OR number = ${didNumber}) AND is_active = true LIMIT 1
  `);
  const phoneRow = phoneRows[0] as unknown as PhoneRecord | undefined;

  if (!phoneRow) {
    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>This number is not configured. Please contact the restaurant directly.</Say>
  <Hangup/>
</Response>`);
  }

  const agent = await resolveAgent(phoneRow);

  if (!agent) {
    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>We're sorry, voice assistance is not available for this number right now. Please contact us through our website or try again later. Thank you.</Say>
  <Hangup/>
</Response>`);
  }

  /* raw: INSERT INTO sip_call_sessions (...) VALUES (...) RETURNING id */
  const { rows: sessionRows } = await db.execute(sql`
    INSERT INTO sip_call_sessions (restaurant_id, phone_number_id, agent_id, sip_call_id, caller_number, callee_number, direction, status, transcript)
    VALUES (${phoneRow.restaurant_id}, ${phoneRow.id}, ${agent.id}, ${callSid}, ${from}, ${to}, 'inbound', 'active', '[]'::jsonb)
    RETURNING id
  `);
  const session = sessionRows[0] as { id: string } | undefined;

  if (!session) {
    console.error(`[Twilio Webhook] Failed to create session for call ${callSid}`);
    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Sorry, there was a problem setting up your call. Please try again.</Say>
  <Hangup/>
</Response>`);
  }

  const streamUrl = buildRealtimeStreamUrl(baseUrl, session.id);
  console.log(`[Twilio Webhook] Routing call ${callSid} (session ${session.id}) → Realtime engine (${streamUrl})`);
  return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="${escapeXml(streamUrl)}" />
  </Connect>
</Response>`);
}

async function handleSpeechInput(sessionId: string, speechText: string, baseUrl: string) {
  /* raw: SELECT id, agent_id, restaurant_id, sip_call_id, transcript FROM sip_call_sessions WHERE id = $1 */
  const { rows: sessionRows } = await db.execute(sql`
    SELECT id, agent_id, restaurant_id, sip_call_id, transcript FROM sip_call_sessions WHERE id = ${sessionId}
  `);
  const session = sessionRows[0] as unknown as SessionRecord | undefined;

  if (!session) {
    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Sorry, there was an error. Goodbye!</Say>
  <Hangup/>
</Response>`);
  }

  const agent = session.agent_id ? await resolveAgentById(session.agent_id) : null;
  const language = agentLanguageForGather(agent);
  const langCode = agent?.voice_language_code || undefined;
  const webhookUrl = `${baseUrl}/api/webhooks/twilio/voice?sessionId=${sessionId}`;

  if (!speechText) {
    const transcript = Array.isArray(session.transcript) ? session.transcript : [];
    const isFirstInteraction = transcript.length === 0;

    if (isFirstInteraction) {
      if (!session.sip_call_id) {
        console.warn(`[Twilio Webhook] Outbound call session ${sessionId} has no sip_call_id — hanging up`);
        return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say>Sorry, there was a problem setting up the call. Goodbye!</Say>
  <Hangup/>
</Response>`);
      }
      const streamUrl = buildRealtimeStreamUrl(baseUrl, sessionId);
      console.log(`[Twilio Webhook] Routing outbound call ${session.sip_call_id} (session ${sessionId}) → Realtime engine (${streamUrl})`);
      return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Connect>
    <Stream url="${escapeXml(streamUrl)}" />
  </Connect>
</Response>`);
    }

    const silenceText = 'I did not catch that. Could you please repeat?';
    const silenceAudioUrl = await generateTtsAudioUrl(
      silenceText, agent?.voice_model_id || null, baseUrl, sessionId, 'silence',
      session.restaurant_id, langCode
    );
    const timeoutMsg = agent?.closing_script?.trim() || 'Thank you for calling. Goodbye!';
    return twimlResponse(buildGatherTwiml(webhookUrl, language, silenceAudioUrl, silenceText, timeoutMsg));
  }

  const branchHoursContext = session.restaurant_id ? await fetchBranchHoursContext(session.restaurant_id) : null;
  const systemPrompt = buildSystemPrompt(agent, branchHoursContext);
  const transcript = Array.isArray(session.transcript) ? session.transcript : [];

  const llmResponse = await generateResponse(
    agent?.llm_model_id || null,
    systemPrompt,
    transcript,
    speechText,
    session.restaurant_id
  );

  transcript.push(
    { role: 'user', text: speechText, ts: new Date().toISOString() },
    { role: 'assistant', text: llmResponse.text, ts: new Date().toISOString() }
  );

  /* raw: UPDATE sip_call_sessions SET transcript = $1 WHERE id = $2 */
  await db.execute(sql`UPDATE sip_call_sessions SET transcript = ${JSON.stringify(transcript)}::jsonb WHERE id = ${sessionId}`);

  if (llmResponse.shouldEscalate) {
    /* raw: UPDATE sip_call_sessions SET escalated_to_human = true, status = 'completed', ended_at = NOW(), ... WHERE id = $1 */
    await db.execute(sql`
      UPDATE sip_call_sessions SET escalated_to_human = true,
       status = 'completed', ended_at = NOW(),
       duration_seconds = EXTRACT(EPOCH FROM (NOW() - started_at))::INTEGER
       WHERE id = ${sessionId}
    `);

    const closingText = agent?.closing_script?.trim() || 'Thank you for calling. Goodbye!';
    const replyAudioUrl = await generateTtsAudioUrl(
      llmResponse.text, agent?.voice_model_id || null, baseUrl, sessionId, 'escalate',
      session.restaurant_id, langCode
    );
    const closingAudioUrl = await generateTtsAudioUrl(
      closingText, agent?.voice_model_id || null, baseUrl, sessionId, 'closing',
      session.restaurant_id, langCode
    );

    /* raw: SELECT pn.fallback_number FROM sip_call_sessions s JOIN phone_numbers pn ON pn.id = s.phone_number_id WHERE s.id = $1 */
    const { rows: fallbackRows } = await db.execute(sql`
      SELECT pn.fallback_number FROM sip_call_sessions s
      JOIN phone_numbers pn ON pn.id = s.phone_number_id
      WHERE s.id = ${sessionId}
    `);
    const fallbackRow = fallbackRows[0] as { fallback_number: string | null } | undefined;

    const replyPart = replyAudioUrl
      ? `<Play>${escapeXml(replyAudioUrl)}</Play>`
      : `<Say>${escapeXml(llmResponse.text)}</Say>`;
    const closingPart = closingAudioUrl
      ? `<Play>${escapeXml(closingAudioUrl)}</Play>`
      : `<Say>${escapeXml(closingText)}</Say>`;

    if (fallbackRow?.fallback_number) {
      return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  ${replyPart}
  ${closingPart}
  <Dial>${escapeXml(fallbackRow.fallback_number)}</Dial>
</Response>`);
    }

    return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  ${replyPart}
  ${closingPart}
  <Hangup/>
</Response>`);
  }

  const replyAudioUrl = await generateTtsAudioUrl(
    llmResponse.text, agent?.voice_model_id || null, baseUrl, sessionId, 'response',
    session.restaurant_id, langCode
  );

  const timeoutMsg = agent?.closing_script?.trim() || 'Thank you for calling. Goodbye!';

  const replyPart = replyAudioUrl
    ? `<Play>${escapeXml(replyAudioUrl)}</Play>`
    : `<Say>${escapeXml(llmResponse.text)}</Say>`;

  return twimlResponse(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  ${replyPart}
  <Gather input="speech" action="${escapeXml(webhookUrl)}" method="POST" speechTimeout="auto" language="${escapeXml(language)}">
  </Gather>
  <Say>${escapeXml(timeoutMsg)}</Say>
  <Hangup/>
</Response>`);
}

async function generateTtsAudioUrl(
  text: string,
  voiceModelId: string | null,
  baseUrl: string,
  sessionId: string,
  label: string,
  restaurantId?: string | null,
  languageCode?: string
): Promise<string | null> {
  try {
    const ttsResult = await synthesizeSpeech(text, voiceModelId, restaurantId, languageCode);
    const fs = await import('fs');
    const path = await import('path');
    const audioDir = path.join(process.cwd(), 'public', 'tts-audio');
    if (!fs.existsSync(audioDir)) fs.mkdirSync(audioDir, { recursive: true });

    const filename = `${sessionId}_${label}_${Date.now()}.wav`;
    const filePath = path.join(audioDir, filename);
    fs.writeFileSync(filePath, ttsResult.audioBuffer);

    setTimeout(() => {
      try { fs.unlinkSync(filePath); } catch (cleanErr: unknown) {
        console.warn(`[TTS] Temp cleanup: ${cleanErr}`);
      }
    }, 300000);

    return `${baseUrl}/tts-audio/${filename}`;
  } catch (err: unknown) {
    const msg = err instanceof Error ? err.message : String(err);
    console.warn(`[TTS] Failed to generate audio (model=${voiceModelId ?? 'none'}, label=${label}): ${msg}`);
    return null;
  }
}
