import express, { Request, Response } from "express";
import twilio from "twilio";
import { storage } from "../storage";
import { prisma as db } from "@repo/db/client";
import { runReminderGraph } from "../ai/reminder-graph";
import { runNewPatientStep } from "../ai/new-patient-graph";
import {
  runRescheduleStep,
  parseDateOnlyFromMessage,
  parseTime,
  isOfficeDayOpen,
  isWithinOfficeHours,
  getOfficeHoursDisplay,
  timeLabel,
} from "../ai/reschedule-graph";
import { getLlm, resolveAiProvider } from "../ai/llm-factory";
import { runEligibilityProcessor } from "../queue/processors/eligibilityProcessor";
import {
  getHandoff, getAfterHoursHandoff,
  getStage, setStage,
  setPendingReschedule, getPendingReschedule, clearPendingReschedule,
  type ConversationStage,
} from "../ai/aiHandoffStore";

const router = express.Router();

// ── Helpers ───────────────────────────────────────────────────────────────────

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

/** Send multiple SMS messages in guaranteed order via a single TwiML response. */
function twimlMessages(...texts: string[]): string {
  const body = texts.map(t => `<Message>${escapeXml(t)}</Message>`).join("");
  return `<?xml version="1.0" encoding="UTF-8"?><Response>${body}</Response>`;
}

function twimlReply(text: string): string {
  return `<?xml version="1.0" encoding="UTF-8"?><Response><Message>${escapeXml(text)}</Message></Response>`;
}

function empty(): string {
  return "<Response></Response>";
}

/** Get the patient's next scheduled appointment as a human-readable string. */
async function getAppointmentDatetime(patientId: number): Promise<string> {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const appt = await db.appointment.findFirst({
    where: { patientId, status: "scheduled", date: { gte: today } },
    orderBy: { date: "asc" },
  });
  if (!appt) return "";
  const months = ["January","February","March","April","May","June",
                  "July","August","September","October","November","December"];
  const d = new Date(appt.date);
  return `${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()} at ${appt.startTime}`;
}

/** Check if right now is outside office hours for the given user. */
async function isAfterHours(userId: number): Promise<boolean> {
  const record = await storage.getOfficeHours(userId);
  if (!record?.data) return false; // no hours configured → treat as in-hours

  const data = record.data as any;
  const now  = new Date();
  const days = ["sunday","monday","tuesday","wednesday","thursday","friday","saturday"];
  const day  = days[now.getDay()];

  const slot = data.doctors?.[day];
  if (!slot?.enabled) return true; // office closed today

  const hhmm = `${String(now.getHours()).padStart(2,"0")}:${String(now.getMinutes()).padStart(2,"0")}`;
  if (hhmm >= slot.amStart && hhmm <= slot.amEnd) return false;
  if (hhmm >= slot.pmStart && hhmm <= slot.pmEnd) return false;
  return true;
}

/** Substitute {officeName} in a template string. */
function applyOfficeName(template: string, name: string): string {
  return template.replace(/\{officeName\}/g, name || "our dental office");
}

/** Save an outbound message and return the text. */
async function saveOutbound(patientId: number, body: string): Promise<void> {
  await storage.createCommunication({
    patientId, channel: "sms", direction: "outbound", status: "sent", body,
  });
}

/**
 * Extract MassHealth Member ID and date of birth from a free-text SMS.
 * Tries regex first, falls back to LLM extraction.
 */
/** Normalize a DOB string to zero-padded MM/DD/YYYY required by MassHealth. */
function normalizeDob(raw: string): string {
  const parts = raw.split(/[\/\-\.]/);
  if (parts.length !== 3) return raw;
  const [m, d, y] = parts;
  const mm   = String(parseInt(m!,  10)).padStart(2, "0");
  const dd   = String(parseInt(d!,  10)).padStart(2, "0");
  const yyyy = y!.length === 2 ? `20${y}` : y!;
  return `${mm}/${dd}/${yyyy}`;
}

async function parseMassHealthInfo(
  message: string,
  apiKey: string,
  provider: import("../ai/llm-factory").AiProvider = "google",
  model?: string
): Promise<{ memberId: string | null; dob: string | null }> {
  // Regex: member IDs are typically 8-12 digits; DOB as MM/DD/YYYY or similar
  const idMatch  = message.match(/\b(\d{8,12})\b/);
  const dobMatch = message.match(/\b(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})\b/);

  if (idMatch && dobMatch) {
    const [, m, d, y] = dobMatch;
    const year = y!.length === 2 ? `20${y}` : y;
    return { memberId: idMatch[1]!, dob: normalizeDob(`${m}/${d}/${year}`) };
  }

  // Fall back to LLM structured extraction
  try {
    const llm = getLlm(provider, apiKey, model);
    const res = await llm.invoke([
      {
        role: "system",
        content:
          'Extract the insurance member ID and date of birth from the patient message. ' +
          'Return ONLY valid JSON: {"memberId":"...","dob":"MM/DD/YYYY"}. Use null for missing fields.',
      },
      { role: "user", content: message },
    ]);
    const raw  = String(res.content).replace(/```json|```/g, "").trim();
    const json = JSON.parse(raw);
    const dob  = json.dob ? normalizeDob(String(json.dob)) : null;
    return { memberId: json.memberId ?? null, dob };
  } catch {
    return { memberId: null, dob: null };
  }
}

/**
 * Run MassHealth eligibility check in the background (after replying to patient)
 * and send the result as a follow-up SMS.
 */
async function runMassHealthCheckAndNotify(
  patient: { id: number; userId: number; phone: string | null; preferredLanguage: string | null },
  memberId: string,
  dob: string,
  apiKey: string,
  isExistingPatient = false
): Promise<void> {
  try {
    const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(patient.userId, "MH");
    if (!credentials) return;

    const twilioSettings = await storage.getTwilioSettings(patient.userId);
    if (!twilioSettings || !patient.phone) return;

    // Run Selenium eligibility check directly via the processor
    await runEligibilityProcessor({
      userId: patient.userId,
      insuranceId: memberId,
      formDob: dob,
      enrichedPayload: {
        memberId,
        dateOfBirth: dob,
        insuranceSiteKey: "MH",
        massdhpUsername: credentials.username,
        massdhpPassword: credentials.password,
      },
    });

    // Re-fetch updated patient status
    const updated = await db.patient.findUnique({
      where: { id: patient.id },
      select: { status: true, firstName: true },
    });

    const lang   = patient.preferredLanguage || "English";
    const active = updated?.status === "ACTIVE";

    // ── ACTIVE ────────────────────────────────────────────────────────────────
    const activeMessages: Record<string, string> = {
      English:          "Great news! Your MassHealth coverage is active. We can schedule an appointment for you! What date and time would you prefer?",
      Spanish:          "¡Buenas noticias! Su cobertura de MassHealth está activa. ¡Podemos programar una cita para usted! ¿Qué fecha y hora prefiere?",
      Portuguese:       "Ótimas notícias! Sua cobertura MassHealth está ativa. Podemos agendar uma consulta para você! Qual data e horário prefere?",
      Mandarin:         "好消息！您的MassHealth保险有效。我们可以为您安排预约！您希望什么日期和时间？",
      Cantonese:        "好消息！您的MassHealth保險有效。我們可以為您安排預約！您希望什麼日期和時間？",
      Arabic:           "أخبار رائعة! تغطيتك من MassHealth نشطة. يمكننا تحديد موعد لك! ما التاريخ والوقت المفضل لديك؟",
      "Haitian Creole": "Bon nouvèl! Asirans MassHealth ou aktif. Nou ka planifye yon randevou pou ou! Ki dat ak lè ou prefere?",
    };

    // ── INACTIVE: new patient → ask other insurance; existing → ask self-pay ──
    const inactiveMessagesNew: Record<string, string> = {
      English:          "Unfortunately, your MassHealth coverage appears to be inactive. Do you have any other insurance?",
      Spanish:          "Lamentablemente, su cobertura de MassHealth parece estar inactiva. ¿Tiene algún otro seguro?",
      Portuguese:       "Infelizmente, sua cobertura MassHealth parece estar inativa. Você tem algum outro plano de saúde?",
      Mandarin:         "很遗憾，您的MassHealth保险似乎无效。您还有其他保险吗？",
      Cantonese:        "很遺憾，您的MassHealth保險似乎無效。您還有其他保險嗎？",
      Arabic:           "للأسف، تغطيتك من MassHealth تبدو غير نشطة. هل لديك أي تأمين آخر؟",
      "Haitian Creole": "Malerezman, kouvèti MassHealth ou parèt inaktif. Èske ou gen yon lòt asirans?",
    };

    const inactiveMessagesExisting: Record<string, string> = {
      English:          "We checked your MassHealth coverage. Unfortunately the plan appears inactive or could not be verified. Would you still like to schedule an examination appointment as a self-pay patient?",
      Spanish:          "Verificamos su cobertura de MassHealth. Lamentablemente el plan aparece inactivo o no pudo ser verificado. ¿Le gustaría programar una cita de examen como paciente de pago particular?",
      Portuguese:       "Verificamos sua cobertura MassHealth. Infelizmente o plano parece inativo ou não pôde ser verificado. Gostaria de agendar uma consulta de exame como paciente particular?",
      Mandarin:         "我们查看了您的MassHealth保险。遗憾的是，保险似乎无效或无法验证。您仍然希望以自费方式预约检查吗？",
      Cantonese:        "我們查看了您的MassHealth保險。遺憾地，保險似乎無效或無法核實。您仍然希望以自費方式預約檢查嗎？",
      Arabic:           "تحققنا من تغطيتك من MassHealth. للأسف يبدو أن الخطة غير نشطة أو لا يمكن التحقق منها. هل تودّ تحديد موعد فحص كمريض يدفع من حسابه الخاص؟",
      "Haitian Creole": "Nou te verifye kouvèti MassHealth ou. Malerezman plan an sanble inaktif oswa pa ka verifye. Èske ou ta renmen pran yon randevou egzamen kòm pasyan ki peye poukont li?",
    };

    const resultText = active
      ? (activeMessages[lang] ?? activeMessages["English"]!)
      : isExistingPatient
        ? (inactiveMessagesExisting[lang] ?? inactiveMessagesExisting["English"]!)
        : (inactiveMessagesNew[lang]      ?? inactiveMessagesNew["English"]!);

    const nextStage: ConversationStage = active
      ? "asked_appointment_time"
      : isExistingPatient
        ? "asked_self_pay"
        : "asked_other_insurance_after_inactive";

    // Send follow-up question via Twilio
    const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
    await client.messages.create({
      body:  resultText,
      from:  twilioSettings.phoneNumber,
      to:    patient.phone,
    });

    // Persist and advance stage
    await saveOutbound(patient.id, resultText);
    await setStage(patient.userId, patient.id, nextStage);

  } catch {
    // Silent — don't crash the main request
  }
}

// ── Empathetic one-liner (instant keyword-based, no API latency) ─────────────

function getEmpatheticAck(message: string, language: string): string {
  const hasPain = /pain|hurt|ache|toothache|emergency|urgent|asap|bleeding|broke|crack|fell out|swollen|infection|abscess/i.test(message);

  const painMsgs: Record<string, string> = {
    English:          "Sorry to hear that! We are here to help you.",
    Spanish:          "¡Lo sentimos! Estamos aquí para ayudarle.",
    Portuguese:       "Lamentamos isso! Estamos aqui para ajudá-lo.",
    Mandarin:         "很遗憾听到这个消息！我们在这里帮助您。",
    Cantonese:        "很遺憾聽到這個消息！我們在這裡幫助您。",
    Arabic:           "آسف لسماع ذلك! نحن هنا لمساعدتك.",
    "Haitian Creole": "Nou regrèt tande sa! Nou la pou ede ou.",
  };
  const normalMsgs: Record<string, string> = {
    English:          "No problem!",
    Spanish:          "¡Sin problema!",
    Portuguese:       "Sem problema!",
    Mandarin:         "没问题！",
    Cantonese:        "沒問題！",
    Arabic:           "لا مشكلة!",
    "Haitian Creole": "Pa gen pwoblèm!",
  };

  return hasPain
    ? (painMsgs[language]   ?? painMsgs["English"]!)
    : (normalMsgs[language] ?? normalMsgs["English"]!);
}

// ── "New appointment or reschedule?" prompt (multilingual) ────────────────────

const NEW_OR_RESCHEDULE_Q: Record<string, string> = {
  English:          "Just to confirm — would you like to make a new appointment, or would you like to reschedule your current appointment?",
  Spanish:          "Solo para confirmar — ¿desea hacer una nueva cita o reprogramar su cita actual?",
  Portuguese:       "Só para confirmar — você gostaria de marcar uma nova consulta ou reagendar sua consulta atual?",
  Mandarin:         "请问您是想预约新的就诊，还是想更改现有预约？",
  Cantonese:        "請問您是想預約新的就診，還是想更改現有預約？",
  Arabic:           "فقط للتأكيد — هل تريد تحديد موعد جديد أم تعديل موعدك الحالي؟",
  "Haitian Creole": "Jis pou konfime — èske ou ta renmen pran yon nouvo randevou, oswa èske ou ta renmen reprogramè randevou aktyèl ou?",
};

// ── POST /api/twilio/webhook/sms ──────────────────────────────────────────────

const CONVO_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes

// In-memory conversation state for unknown phone numbers (no patient record yet).
// Keyed by normalized "From" number.
const unknownPhoneConvos = new Map<string, {
  userId: number;
  stage: ConversationStage;
  language: string;
  lastActivityAt: Date;
  pendingApptDate?: { date: Date; dateLabel: string };
}>();

router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
  try {
    const { From, To, Body, MessageSid } = req.body;

    const normalizedFrom = (From || "").replace(/\D/g, "");
    const allPatients = await db.patient.findMany({
      select: { id: true, phone: true, userId: true, preferredLanguage: true },
    });
    const patient = allPatients.find(
      (p) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
    );

    if (!patient) {
      // Unknown number — look up office by the Twilio "To" number
      const normalizedTo = (To || "").replace(/\D/g, "");
      const twilioRow = await db.twilioSettings.findFirst({
        where: { phoneNumber: { contains: normalizedTo.slice(-10) } },
        select: { userId: true },
      });
      if (!twilioRow) {
        res.set("Content-Type", "text/xml");
        return res.send(empty());
      }

      const userId = twilioRow.userId;
      const openPhoneReply = await storage.getOpenPhoneReply(userId);
      if (!openPhoneReply) {
        res.set("Content-Type", "text/xml");
        return res.send(empty());
      }

      // Fetch required context for this office
      const aiSettings    = await storage.getAiSettings(userId);
      const activeAiU     = resolveAiProvider(aiSettings ?? {});
      if (!activeAiU) {
        res.set("Content-Type", "text/xml");
        return res.send(empty());
      }

      const chatTemplates  = await storage.getAiChatTemplates(userId);
      const officeContact  = await storage.getOfficeContact(userId);
      const officeName     = (officeContact as any)?.officeName?.trim() || "";
      const convo    = unknownPhoneConvos.get(normalizedFrom);
      let   stage    = convo?.stage ?? "initial";
      const language = convo?.language ?? "English";

      // Reset conversation if idle for more than 5 minutes
      if (
        stage !== "initial" && stage !== "done" &&
        convo?.lastActivityAt &&
        Date.now() - convo.lastActivityAt.getTime() > CONVO_TIMEOUT_MS
      ) {
        stage = "initial";
        unknownPhoneConvos.set(normalizedFrom, { userId, stage: "initial", language, lastActivityAt: new Date() });
      }

      const replyUnknown = (
        text: string,
        nextStage: ConversationStage,
        pendingApptDate?: { date: Date; dateLabel: string },
      ) => {
        unknownPhoneConvos.set(normalizedFrom, {
          userId, stage: nextStage, language,
          lastActivityAt: new Date(),
          ...(pendingApptDate ? { pendingApptDate } : { pendingApptDate: convo?.pendingApptDate }),
        });
        res.set("Content-Type", "text/xml");
        return res.send(twimlReply(text));
      };

      if (stage === "initial" || stage === "done") {
        // MSG 1: AI intro
        const rawGreeting = chatTemplates.newPatientGreeting ||
          `Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
        const introText = applyOfficeName(rawGreeting, officeName);

        // MSG 2: Empathetic acknowledgment
        const empatheticText = getEmpatheticAck(Body, language);

        // MSG 3: Detect intent from first message
        const isReschedule = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
        let msg3Unk: string;
        let nextStage3Unk: ConversationStage;

        if (isReschedule) {
          const notFoundMsgs: Record<string, string> = {
            English:          "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
            Spanish:          "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
            Portuguese:       "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
            Mandarin:         "我找不到与您号码关联的预约。请留下您的姓名和联系电话，我们的前台将尽快与您联系。",
            Cantonese:        "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話，我們的接待員將盡快與您聯絡。",
            Arabic:           "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
            "Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
          };
          msg3Unk = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
          nextStage3Unk = "collecting_contact_info";
        } else {
          const newOrExistingMsgs: Record<string, string> = {
            English:          "Can you please tell me if you are a new or existing patient?",
            Spanish:          "¿Puede decirme si es usted un paciente nuevo o existente?",
            Portuguese:       "Pode me dizer se você é um paciente novo ou existente?",
            Mandarin:         "请问您是新患者还是现有患者？",
            Cantonese:        "請問您是新病人還是現有病人？",
            Arabic:           "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
            "Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
          };
          msg3Unk = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
          nextStage3Unk = "asked_new_or_existing";
        }

        // Update store then send all 3 in one TwiML response (guaranteed order)
        unknownPhoneConvos.set(normalizedFrom, {
          userId, stage: nextStage3Unk, language, lastActivityAt: new Date(),
        });
        res.set("Content-Type", "text/xml");
        return res.send(twimlMessages(introText, empatheticText, msg3Unk));
      }

      // ── Unknown: asked_new_or_reschedule (fallback for in-progress conversations) ──
      if (stage === "asked_new_or_reschedule") {
        const isReschedule = /reschedule|rescheduler|change|modify|move|reprogramar|reagendar|cambiar|mudar/i.test(Body);
        if (isReschedule) {
          const notFoundMsgs: Record<string, string> = {
            English:          "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
            Spanish:          "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
            Portuguese:       "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
            Mandarin:         "我找不到与您号码关联的预约。请留下您的姓名和联系电话，我们的前台将尽快与您联系。",
            Cantonese:        "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話，我們的接待員將盡快與您聯絡。",
            Arabic:           "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
            "Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
          };
          return replyUnknown(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "collecting_contact_info");
        }
        const newOrExistingMsgs: Record<string, string> = {
          English:          "Can you please tell me if you are a new or existing patient?",
          Spanish:          "¿Puede decirme si es usted un paciente nuevo o existente?",
          Portuguese:       "Pode me dizer se você é um paciente novo ou existente?",
          Mandarin:         "请问您是新患者还是现有患者？",
          Cantonese:        "請問您是新病人還是現有病人？",
          Arabic:           "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
          "Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
        };
        return replyUnknown(newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!, "asked_new_or_existing");
      }

      // ── Unknown: asked_appointment_time → parse date, check office hours ──
      if (stage === "asked_appointment_time") {
        const parsedDate = await parseDateOnlyFromMessage(Body, activeAiU.key, activeAiU.provider, activeAiU.model);
        if (!parsedDate) {
          const msgs: Record<string, string> = {
            English:          "I didn't catch that. What day would you prefer? For example: 'May 28', 'next Monday', or '5/28'.",
            Spanish:          "No entendí. ¿Qué día prefiere? Por ejemplo: '28 de mayo', 'próximo lunes' o '28/5'.",
            Portuguese:       "Não entendi. Que dia você prefere? Por exemplo: '28 de maio', 'próxima segunda' ou '28/5'.",
            Mandarin:         "我没听清。您希望哪天？例如：'5月28日'、'下周一'或'5/28'。",
            Cantonese:        "我沒聽清。您希望哪天？例如：'5月28日'、'下週一'或'5/28'。",
            Arabic:           "لم أفهم. ما اليوم الذي تفضله؟ مثلاً: '28 مايو' أو '5/28'.",
            "Haitian Creole": "Mwen pa konprann. Ki jou ou prefere? Pa egzanp: '28 me', 'Lendi pwochèn', oswa '5/28'.",
          };
          return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
        }
        const { date, dateLabel } = parsedDate;
        const dayCheck = await isOfficeDayOpen(date, userId);
        if (!dayCheck.open) {
          const msgs: Record<string, string> = {
            English:          `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
            Spanish:          `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
            Portuguese:       `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
            Mandarin:         `我们诊所在 ${dateLabel}（${dayCheck.displayDay}）不开放。请您选择另一天好吗？`,
            Cantonese:        `我們診所在 ${dateLabel}（${dayCheck.displayDay}）不開放。請您選擇另一天好嗎？`,
            Arabic:           `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
            "Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
          };
          return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
        }
        const askTimeMsgs: Record<string, string> = {
          English:          `What time do you prefer on ${dateLabel}?`,
          Spanish:          `¿A qué hora prefiere el ${dateLabel}?`,
          Portuguese:       `Que horário você prefere em ${dateLabel}?`,
          Mandarin:         `您希望在 ${dateLabel} 几点？`,
          Cantonese:        `您希望在 ${dateLabel} 幾點？`,
          Arabic:           `ما الوقت الذي تفضله في ${dateLabel}؟`,
          "Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
        };
        return replyUnknown(
          askTimeMsgs[language] ?? askTimeMsgs["English"]!,
          "asked_new_appt_time_for_date",
          { date, dateLabel },
        );
      }

      // ── Unknown: asked_new_appt_time_for_date → parse time, check hours ───
      if (stage === "asked_new_appt_time_for_date") {
        const pendingApptDate = convo?.pendingApptDate;
        if (!pendingApptDate) {
          return replyUnknown(
            "I lost track of the date. What day would you prefer?",
            "asked_appointment_time",
          );
        }
        const startTime = await parseTime(Body, activeAiU.key, activeAiU.provider, activeAiU.model);
        if (!startTime) {
          const msgs: Record<string, string> = {
            English:          `I didn't catch the time. What time do you prefer on ${pendingApptDate.dateLabel}? For example: '10am' or '2pm'.`,
            Spanish:          `No entendí la hora. ¿Qué hora prefiere el ${pendingApptDate.dateLabel}? Por ejemplo: '10am' o '2pm'.`,
            Portuguese:       `Não entendi o horário. Que hora você prefere em ${pendingApptDate.dateLabel}? Por exemplo: '10h' ou '14h'.`,
            Mandarin:         `我没听清时间。您在 ${pendingApptDate.dateLabel} 几点？例如：上午10点或下午2点。`,
            Cantonese:        `我沒聽清時間。您在 ${pendingApptDate.dateLabel} 幾點？例如：上午10點或下午2點。`,
            Arabic:           `لم أفهم الوقت. ما الوقت الذي تفضله في ${pendingApptDate.dateLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
            "Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pendingApptDate.dateLabel}? Pa egzanp: 10am oswa 2pm.`,
          };
          return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
        }
        const withinHours = await isWithinOfficeHours(pendingApptDate.date, startTime, userId);
        if (!withinHours) {
          const hoursDisplay = await getOfficeHoursDisplay(pendingApptDate.date, userId);
          const timeLbl = timeLabel(startTime);
          const msgs: Record<string, string> = {
            English: hoursDisplay
              ? `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
              : `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. What other time do you prefer?`,
            Spanish: hoursDisplay
              ? `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
              : `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. ¿Qué otro horario prefiere?`,
            Portuguese: hoursDisplay
              ? `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
              : `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Que outro horário você prefere?`,
            Mandarin: hoursDisplay
              ? `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间？`
              : `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。您希望改什么时间？`,
            Cantonese: hoursDisplay
              ? `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間？`
              : `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。您希望改什麼時間？`,
            Arabic: hoursDisplay
              ? `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
              : `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ما وقت آخر تفضله؟`,
            "Haitian Creole": hoursDisplay
              ? `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
              : `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Ki lòt lè ou prefere?`,
          };
          return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
        }
        const apptLabel = `${pendingApptDate.dateLabel} at ${timeLabel(startTime)}`;
        const confirmMsgs: Record<string, string> = {
          English:          `Thank you! Your preferred appointment is ${apptLabel}. Our receptionist will confirm the details with you shortly.`,
          Spanish:          `¡Gracias! Su cita preferida es el ${apptLabel}. Nuestra recepcionista confirmará los detalles con usted en breve.`,
          Portuguese:       `Obrigado! Sua consulta preferida é em ${apptLabel}. Nossa recepcionista confirmará os detalhes em breve.`,
          Mandarin:         `谢谢！您的预约时间为 ${apptLabel}。我们的前台将很快与您确认详情。`,
          Cantonese:        `多謝！您的預約時間為 ${apptLabel}。我們的接待員將很快與您確認詳情。`,
          Arabic:           `شكراً! موعدك المفضل هو ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيد التفاصيل.`,
          "Haitian Creole": `Mèsi! Randevou ou a se ${apptLabel}. Resepsyonis nou an pral konfime detay yo ak ou byento.`,
        };
        return replyUnknown(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
      }

      // Multi-step new patient stages for unknown numbers
      const unknownNewPatientStages: ConversationStage[] = [
        "new_patient_greeted", "asked_new_or_existing",
        "asked_new_patient_insurance", "asked_insurance_type",
        "asked_masshealth_check_consent", "asked_existing_insurance",
        "asked_appointment_preference",
        "asked_self_pay", "asked_other_insurance_after_inactive",
        "collecting_contact_info",
      ];
      if (unknownNewPatientStages.includes(stage)) {
        const { reply: aiReply, nextStage } = await runNewPatientStep(
          Body, stage, language, activeAiU.key, chatTemplates.generalFallback, activeAiU.provider, activeAiU.model
        );
        return replyUnknown(aiReply, nextStage);
      }

      res.set("Content-Type", "text/xml");
      return res.send(empty());
    }

    // Save inbound message
    await storage.createCommunication({
      patientId: patient.id, channel: "sms", direction: "inbound",
      status: "delivered", body: Body, twilioSid: MessageSid,
    });

    // Per-patient handoff toggle must be ON
    if (!await getHandoff(patient.userId, patient.id)) {
      res.set("Content-Type", "text/xml");
      return res.send(empty());
    }

    const aiSettings = await storage.getAiSettings(patient.userId);
    const activeAi   = resolveAiProvider(aiSettings ?? {});
    if (!activeAi) {
      res.set("Content-Type", "text/xml");
      return res.send(empty());
    }

    const language      = patient.preferredLanguage || "English";
    let   stage         = await getStage(patient.userId, patient.id);
    const chatTemplates = await storage.getAiChatTemplates(patient.userId);

    // Reset conversation if idle for more than 5 minutes
    if (stage !== "initial" && stage !== "done") {
      const convRow = await db.patientConversation.findUnique({
        where:  { patientId: patient.id },
        select: { updatedAt: true },
      });
      if (convRow?.updatedAt && Date.now() - convRow.updatedAt.getTime() > CONVO_TIMEOUT_MS) {
        clearPendingReschedule(patient.userId, patient.id);
        await setStage(patient.userId, patient.id, "initial");
        stage = "initial";
      }
    }
    const officeContact = await storage.getOfficeContact(patient.userId);
    const officeName    = (officeContact as any)?.officeName?.trim() || "";

    // ── Helper: send reply + set stage ─────────────────────────────────────
    const reply = async (text: string, nextStage: ConversationStage) => {
      await saveOutbound(patient.id, text);
      await setStage(patient.userId, patient.id, nextStage);
      res.set("Content-Type", "text/xml");
      return res.send(twimlReply(text));
    };

    // ── Stage: reminder_initial → two messages: 1) AI intro, 2) intent response ──
    if (stage === "reminder_initial") {
      const rawGreeting = chatTemplates.reminderGreeting ||
        `Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can confirm or reschedule your appointment and answer general questions 24/7.`;
      const introText = applyOfficeName(rawGreeting, officeName);

      // Use Google AI (LangGraph) to read the patient's reply and classify yes/no
      const apptDatetime = await getAppointmentDatetime(patient.id);
      const { reply: intentReply, intent } = await runReminderGraph(
        Body, activeAi.key, language, apptDatetime,
        chatTemplates.rescheduleGreeting, chatTemplates.generalFallback, activeAi.provider, activeAi.model
      );

      if (intentReply) {
        let nextStage: ConversationStage;
        if (intent === "no")                    nextStage = "asked_reschedule_datetime";
        else if (intent === "wants_appointment") nextStage = "asked_new_or_existing";
        else                                    nextStage = "done";

        // Send message 1 (AI intro) via REST API — queued FIRST in Twilio so it arrives first
        const twilioSettings = await storage.getTwilioSettings(patient.userId);
        if (twilioSettings) {
          const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
          await client.messages.create({ body: introText, from: twilioSettings.phoneNumber, to: From });
          await saveOutbound(patient.id, introText);
        }

        // If patient said "no" but already included a date (e.g. "no, 5/18"),
        // skip "when to reschedule?" and go straight to date processing
        if (intent === "no") {
          const hasDateInMessage =
            /\b\d{1,2}[\/\-]\d{1,2}\b/.test(Body) ||
            /\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|next week)\b/i.test(Body);
          if (hasDateInMessage) {
            const { reply: rescheduleReply, nextStage: rescheduleNextStage } = await runRescheduleStep(
              Body, "asked_reschedule_datetime", language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
            );
            return reply(rescheduleReply, rescheduleNextStage);
          }
        }

        // Send message 2 (yes/no response) via TwiML — queued SECOND
        return reply(intentReply, nextStage);
      }

      // No clear intent detected — send only the intro and wait for next reply
      return reply(introText, "greeted");
    }

    // ── Stage: greeted → classify yes/no for appointment reminder ────────
    if (stage === "greeted") {
      const apptDatetime = await getAppointmentDatetime(patient.id);
      const { reply: aiReply, intent } = await runReminderGraph(
        Body, activeAi.key, language, apptDatetime,
        chatTemplates.rescheduleGreeting, chatTemplates.generalFallback, activeAi.provider, activeAi.model
      );
      if (aiReply) {
        let nextStage: ConversationStage;
        if (intent === "no")               nextStage = "asked_reschedule_datetime";
        else if (intent === "wants_appointment") nextStage = "asked_new_or_existing";
        else                               nextStage = "done";

        // If patient said "no" but already included a date, skip straight to date processing
        if (intent === "no") {
          const hasDateInMessage =
            /\b\d{1,2}[\/\-]\d{1,2}\b/.test(Body) ||
            /\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|next week)\b/i.test(Body);
          if (hasDateInMessage) {
            const { reply: rescheduleReply, nextStage: rescheduleNextStage } = await runRescheduleStep(
              Body, "asked_reschedule_datetime", language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
            );
            return reply(rescheduleReply, rescheduleNextStage);
          }
        }

        return reply(aiReply, nextStage);
      }
    }

    // ── Rescheduling flow stages ───────────────────────────────────────────
    const rescheduleStages: ConversationStage[] = [
      "asked_reschedule_confirm",          "asked_reschedule_preference",
      "asked_reschedule_asap",             "asked_reschedule_next_week",
      "asked_reschedule_time",             "asked_reschedule_datetime",
      "asked_reschedule_time_for_date",    "asked_reschedule_confirm_datetime",
    ];
    if (rescheduleStages.includes(stage)) {
      const { reply: aiReply, nextStage } = await runRescheduleStep(
        Body, stage, language, patient.id, activeAi.key, patient.userId, activeAi.provider, activeAi.model
      );
      return reply(aiReply, nextStage);
    }

    // ── Stage: awaiting MassHealth member ID + DOB ────────────────────────
    if (stage === "awaiting_masshealth_info") {
      const { memberId, dob } = await parseMassHealthInfo(Body, activeAi.key, activeAi.provider, activeAi.model);

      if (!memberId || !dob) {
        // Couldn't parse — ask again with a clearer format hint
        const retryMessages: Record<string, string> = {
          English:          "I couldn't read your Member ID and date of birth. Please reply in this format: Member ID: 12345678  DOB: 01/01/1990",
          Spanish:          "No pude leer su número de miembro y fecha de nacimiento. Por favor responda así: ID: 12345678  Fecha: 01/01/1990",
          Portuguese:       "Não consegui ler seu número de membro e data de nascimento. Por favor responda assim: ID: 12345678  Data: 01/01/1990",
          Mandarin:         "我无法读取您的会员ID和出生日期。请按以下格式回复：ID: 12345678  生日: 01/01/1990",
          Cantonese:        "我無法讀取您的會員ID和出生日期。請按以下格式回覆：ID: 12345678  生日: 01/01/1990",
          Arabic:           "لم أتمكن من قراءة رقم العضوية وتاريخ الميلاد. يرجى الرد بالصيغة التالية: ID: 12345678  DOB: 01/01/1990",
          "Haitian Creole": "Mwen pa t ka li ID manm ou ak dat nesans. Tanpri reponn konsa: ID: 12345678  DOB: 01/01/1990",
        };
        const retryMsg = retryMessages[language] ?? retryMessages["English"]!;
        return reply(retryMsg, "awaiting_masshealth_info");
      }

      // Immediately confirm to the patient and start the check in background
      const checkingMessages: Record<string, string> = {
        English:          "Thank you! I'm checking your MassHealth eligibility now. I'll send you the result in a moment.",
        Spanish:          "¡Gracias! Estoy verificando su elegibilidad de MassHealth ahora. Le enviaré el resultado en un momento.",
        Portuguese:       "Obrigado! Estou verificando sua elegibilidade MassHealth agora. Enviarei o resultado em instantes.",
        Mandarin:         "谢谢！我正在查询您的MassHealth资格。稍后我会发送结果给您。",
        Cantonese:        "多謝！我正在查詢您的MassHealth資格。稍後我會發送結果給您。",
        Arabic:           "شكراً! أقوم بالتحقق من أهليتك في MassHealth الآن. سأرسل لك النتيجة قريباً.",
        "Haitian Creole": "Mèsi! Mwen ap verifye kalifikasyon MassHealth ou kounye a. M ap voye rezilta a nan yon ti moman.",
      };
      const checkingMsg = checkingMessages[language] ?? checkingMessages["English"]!;

      // Reply now — Selenium runs in the background
      await saveOutbound(patient.id, checkingMsg);
      await setStage(patient.userId, patient.id, "done");
      res.set("Content-Type", "text/xml");
      res.send(twimlReply(checkingMsg));

      // Fire-and-forget: run check and send result SMS when complete
      runMassHealthCheckAndNotify(patient, memberId, dob, activeAi.key).catch(() => {});
      return;
    }

    // ── Stage: existing patient said YES to same insurance ───────────────
    // Special case: if they have MassHealth on file, run Selenium check
    // automatically (we already have their member ID + DOB in DB).
    if (stage === "asked_existing_insurance") {
      const saysYes = /yes|same|still have|haven't changed|no change|yep|yeah|sí|si|sim|好的|نعم|wi/i.test(Body);

      if (saysYes) {
        const patientRecord = await db.patient.findUnique({
          where: { id: patient.id },
          select: { insuranceProvider: true, insuranceId: true, dateOfBirth: true },
        });

        const isMassHealth = /masshealth|mass health|masscare|medicaid/i.test(
          patientRecord?.insuranceProvider ?? ""
        );

        if (isMassHealth && patientRecord?.insuranceId) {
          // Format DOB as MM/DD/YYYY for Selenium
          let dobStr = "";
          if (patientRecord.dateOfBirth) {
            const d = new Date(patientRecord.dateOfBirth);
            const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
            const dd = String(d.getUTCDate()).padStart(2, "0");
            const yy = d.getUTCFullYear();
            dobStr = `${mm}/${dd}/${yy}`;
          }

          const checkingMessages: Record<string, string> = {
            English:          "Please wait about 30-60 seconds! I'm double-checking your MassHealth coverage right now.",
            Spanish:          "¡Por favor espere unos 30-60 segundos! Estoy verificando su cobertura de MassHealth ahora mismo.",
            Portuguese:       "Por favor aguarde cerca de 30-60 segundos! Estou verificando sua cobertura MassHealth agora.",
            Mandarin:         "请等待约30-60秒！我现在正在为您核查MassHealth保险。",
            Cantonese:        "請等待約30-60秒！我現在正在為您核查MassHealth保險。",
            Arabic:           "يرجى الانتظار حوالي 30-60 ثانية! أقوم الآن بالتحقق من تغطيتك في MassHealth.",
            "Haitian Creole": "Tanpri tann anviwon 30-60 segonn! Mwen ap verifye kouvèti MassHealth ou kounye a.",
          };
          const checkingMsg = checkingMessages[language] ?? checkingMessages["English"]!;

          await saveOutbound(patient.id, checkingMsg);
          await setStage(patient.userId, patient.id, "done");
          res.set("Content-Type", "text/xml");
          res.send(twimlReply(checkingMsg));

          // Fire-and-forget Selenium check; existing patient gets simpler result
          runMassHealthCheckAndNotify(
            patient, patientRecord.insuranceId, dobStr, activeAi.key, true
          ).catch(() => {});
          return;
        }
      }
      // Not MassHealth or said NO — fall through to normal graph handling
    }

    // ── Stage: asked_new_or_reschedule ────────────────────────────────────
    if (stage === "asked_new_or_reschedule") {
      const isReschedule = /reschedule|rescheduler|change|modify|move|different|reprogramar|reagendar|cambiar|mudar|\b2\b/i.test(Body);
      const isNew = /new.*appoint|make.*appoint|book.*appoint|new patient|first.*time|nueva cita|nova consulta|\b1\b/i.test(Body);

      if (isReschedule) {
        const apptDatetime = await getAppointmentDatetime(patient.id);
        if (apptDatetime) {
          const foundMsgs: Record<string, string> = {
            English:          `Ok. Just to confirm, your current appointment is on ${apptDatetime}. When would you like to reschedule?`,
            Spanish:          `De acuerdo. Solo para confirmar, su cita actual es el ${apptDatetime}. ¿Cuándo le gustaría reprogramarla?`,
            Portuguese:       `Ok. Só para confirmar, sua consulta atual é em ${apptDatetime}. Quando você gostaria de reagendar?`,
            Mandarin:         `好的。请确认一下，您当前的预约是 ${apptDatetime}。您想重新安排到什么时候？`,
            Cantonese:        `好的。請確認一下，您目前的預約是 ${apptDatetime}。您想重新安排到什麼時候？`,
            Arabic:           `حسناً. فقط للتأكيد، موعدك الحالي في ${apptDatetime}. متى تريد إعادة الجدولة؟`,
            "Haitian Creole": `Oke. Jis pou konfime, randevou aktyèl ou a se ${apptDatetime}. Ki lè ou ta renmen reprogramè?`,
          };
          return reply(foundMsgs[language] ?? foundMsgs["English"]!, "asked_reschedule_datetime");
        } else {
          const notFoundMsgs: Record<string, string> = {
            English:          "Ok. I could not find your current appointment. Please tell me when you'd like to reschedule and our receptionist will contact you as soon as possible.",
            Spanish:          "De acuerdo. No pude encontrar su cita actual. Por favor díganos cuándo le gustaría reprogramar y nuestra recepcionista le contactará lo antes posible.",
            Portuguese:       "Ok. Não consegui encontrar sua consulta atual. Por favor diga-nos quando você gostaria de reagendar e nossa recepcionista entrará em contato o mais breve possível.",
            Mandarin:         "好的。我找不到您当前的预约。请告诉我您希望重新安排的时间，我们的前台将尽快与您联系。",
            Cantonese:        "好的。我找不到您目前的預約。請告訴我您希望重新安排的時間，我們的接待員將盡快與您聯絡。",
            Arabic:           "حسناً. لم أجد موعدك الحالي. يرجى إخبارنا بالوقت الذي تريد إعادة الجدولة إليه وسيتصل بك موظف الاستقبال في أقرب وقت ممكن.",
            "Haitian Creole": "Oke. Mwen pa t jwenn randevou aktyèl ou. Tanpri di nou ki lè ou ta renmen reprogramè epi resepsyonis nou an pral kontakte ou pi vit posib.",
          };
          return reply(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "done");
        }
      }

      // "new appointment" or ambiguous → ask new/existing patient
      const newApptMsgs: Record<string, string> = {
        English:          "No problem! To get started, are you an existing patient or a new patient?",
        Spanish:          "¡Sin problema! Para comenzar, ¿es usted un paciente existente o un paciente nuevo?",
        Portuguese:       "Sem problema! Para começar, você é um paciente existente ou um novo paciente?",
        Mandarin:         "没问题！请问您是现有患者还是新患者？",
        Cantonese:        "沒問題！請問您是現有病人還是新病人？",
        Arabic:           "لا مشكلة! للبدء، هل أنت مريض حالي أم مريض جديد؟",
        "Haitian Creole": "Pa gen pwoblèm! Pou kòmanse, èske ou se yon pasyan egzistan oswa yon nouvo pasyan?",
      };
      return reply(newApptMsgs[language] ?? newApptMsgs["English"]!, "asked_new_or_existing");
    }

    // ── Stage: asked_appointment_time → parse date, check office hours ───
    if (stage === "asked_appointment_time") {
      const parsedDate = await parseDateOnlyFromMessage(Body, activeAi.key, activeAi.provider, activeAi.model);
      if (!parsedDate) {
        const msgs: Record<string, string> = {
          English:          "I didn't catch that. What day would you prefer? For example: 'May 28', 'next Monday', or '5/28'.",
          Spanish:          "No entendí. ¿Qué día prefiere? Por ejemplo: '28 de mayo', 'próximo lunes' o '28/5'.",
          Portuguese:       "Não entendi. Que dia você prefere? Por exemplo: '28 de maio', 'próxima segunda' ou '28/5'.",
          Mandarin:         "我没听清。您希望哪天？例如：'5月28日'、'下周一'或'5/28'。",
          Cantonese:        "我沒聽清。您希望哪天？例如：'5月28日'、'下週一'或'5/28'。",
          Arabic:           "لم أفهم. ما اليوم الذي تفضله؟ مثلاً: '28 مايو' أو '5/28'.",
          "Haitian Creole": "Mwen pa konprann. Ki jou ou prefere? Pa egzanp: '28 me', 'Lendi pwochèn', oswa '5/28'.",
        };
        return reply(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
      }
      const { date, dateLabel } = parsedDate;
      const dayCheck = await isOfficeDayOpen(date, patient.userId);
      if (!dayCheck.open) {
        const msgs: Record<string, string> = {
          English:          `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
          Spanish:          `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
          Portuguese:       `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
          Mandarin:         `我们诊所在 ${dateLabel}（${dayCheck.displayDay}）不开放。请您选择另一天好吗？`,
          Cantonese:        `我們診所在 ${dateLabel}（${dayCheck.displayDay}）不開放。請您選擇另一天好嗎？`,
          Arabic:           `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
          "Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
        };
        return reply(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
      }
      // Day is open — save pending date and ask for preferred time
      setPendingReschedule(patient.userId, patient.id, { newDate: date, dayLabel: dateLabel });
      const askTimeMsgs: Record<string, string> = {
        English:          `What time do you prefer on ${dateLabel}?`,
        Spanish:          `¿A qué hora prefiere el ${dateLabel}?`,
        Portuguese:       `Que horário você prefere em ${dateLabel}?`,
        Mandarin:         `您希望在 ${dateLabel} 几点？`,
        Cantonese:        `您希望在 ${dateLabel} 幾點？`,
        Arabic:           `ما الوقت الذي تفضله في ${dateLabel}؟`,
        "Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
      };
      return reply(askTimeMsgs[language] ?? askTimeMsgs["English"]!, "asked_new_appt_time_for_date");
    }

    // ── Stage: asked_new_appt_time_for_date → parse time, check hours ────
    if (stage === "asked_new_appt_time_for_date") {
      const pending = getPendingReschedule(patient.userId, patient.id);
      if (!pending) {
        return reply("I lost track of the date. What day would you prefer?", "asked_appointment_time");
      }
      const startTime = await parseTime(Body, activeAi.key, activeAi.provider, activeAi.model);
      if (!startTime) {
        const msgs: Record<string, string> = {
          English:          `I didn't catch the time. What time do you prefer on ${pending.dayLabel}? For example: '10am' or '2pm'.`,
          Spanish:          `No entendí la hora. ¿Qué hora prefiere el ${pending.dayLabel}? Por ejemplo: '10am' o '2pm'.`,
          Portuguese:       `Não entendi o horário. Que hora você prefere em ${pending.dayLabel}? Por exemplo: '10h' ou '14h'.`,
          Mandarin:         `我没听清时间。您在 ${pending.dayLabel} 几点？例如：上午10点或下午2点。`,
          Cantonese:        `我沒聽清時間。您在 ${pending.dayLabel} 幾點？例如：上午10點或下午2點。`,
          Arabic:           `لم أفهم الوقت. ما الوقت الذي تفضله في ${pending.dayLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
          "Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pending.dayLabel}? Pa egzanp: 10am oswa 2pm.`,
        };
        return reply(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
      }
      const withinHours = await isWithinOfficeHours(pending.newDate, startTime, patient.userId);
      if (!withinHours) {
        const hoursDisplay = await getOfficeHoursDisplay(pending.newDate, patient.userId);
        const timeLbl = timeLabel(startTime);
        const msgs: Record<string, string> = {
          English: hoursDisplay
            ? `Our office is not available at ${timeLbl} on ${pending.dayLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
            : `Our office is not available at ${timeLbl} on ${pending.dayLabel}. What other time do you prefer?`,
          Spanish: hoursDisplay
            ? `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
            : `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. ¿Qué otro horario prefiere?`,
          Portuguese: hoursDisplay
            ? `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
            : `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Que outro horário você prefere?`,
          Mandarin: hoursDisplay
            ? `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间？`
            : `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。您希望改什么时间？`,
          Cantonese: hoursDisplay
            ? `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間？`
            : `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。您希望改什麼時間？`,
          Arabic: hoursDisplay
            ? `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
            : `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ما وقت آخر تفضله؟`,
          "Haitian Creole": hoursDisplay
            ? `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
            : `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Ki lòt lè ou prefere?`,
        };
        return reply(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
      }
      clearPendingReschedule(patient.userId, patient.id);
      const apptLabel = `${pending.dayLabel} at ${timeLabel(startTime)}`;

      // Calculate end time (60-minute default slot)
      const [nh, nm] = startTime.split(":").map(Number);
      const endTotal  = nh! * 60 + nm! + 60;
      const endTime   = `${String(Math.floor(endTotal / 60)).padStart(2, "0")}:${String(endTotal % 60).padStart(2, "0")}`;

      // Look up patient name + find first staff for this office
      const [patientRecord, firstStaff] = await Promise.all([
        db.patient.findUnique({
          where:  { id: patient.id },
          select: { firstName: true, lastName: true },
        }),
        db.staff.findFirst({
          where:   { userId: patient.userId },
          orderBy: { id: "asc" },
        }),
      ]);

      // Create the appointment if a staff member exists
      if (firstStaff) {
        try {
          await storage.createAppointment({
            patientId:  patient.id,
            userId:     patient.userId,
            staffId:    firstStaff.id,
            title:      `AI Scheduled - ${(patientRecord?.firstName ?? "") + " " + (patientRecord?.lastName ?? "")}`.trim(),
            date:       pending.newDate,
            startTime,
            endTime,
            type:       "checkup",
            status:     "scheduled",
            movedByAi:  true,
          } as any);
        } catch { /* silent — message still sent, staff can create manually */ }
      }

      const confirmMsgs: Record<string, string> = {
        English:          `Thank you! Your preferred appointment at ${apptLabel} was scheduled. Our receptionist will confirm it with you shortly.`,
        Spanish:          `¡Gracias! Su cita preferida el ${apptLabel} fue programada. Nuestra recepcionista lo confirmará con usted en breve.`,
        Portuguese:       `Obrigado! Sua consulta preferida em ${apptLabel} foi agendada. Nossa recepcionista confirmará com você em breve.`,
        Mandarin:         `谢谢！您在 ${apptLabel} 的预约已安排。我们的前台将很快与您确认。`,
        Cantonese:        `多謝！您在 ${apptLabel} 的預約已安排。我們的接待員將很快與您確認。`,
        Arabic:           `شكراً! تم جدولة موعدك في ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيده.`,
        "Haitian Creole": `Mèsi! Randevou ou a nan ${apptLabel} pwograme. Resepsyonis nou an pral konfime li ak ou byento.`,
      };
      return reply(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
    }

    // ── Stage: new_patient_greeted + multi-step new patient stages ────────
    const newPatientStages: ConversationStage[] = [
      "new_patient_greeted", "asked_new_or_existing",
      "asked_new_patient_insurance", "asked_insurance_type",
      "asked_masshealth_check_consent", "asked_existing_insurance",
      "asked_appointment_preference",
      "asked_self_pay", "asked_other_insurance_after_inactive",
      "collecting_contact_info",
    ];
    if (newPatientStages.includes(stage)) {
      const { reply: aiReply, nextStage } = await runNewPatientStep(
        Body, stage, language, activeAi.key, chatTemplates.generalFallback, activeAi.provider, activeAi.model
      );
      return reply(aiReply, nextStage);
    }

    // ── Stage: done → closing thank-you reply ────────────────────────────
    // When the patient sends a thank-you / acknowledgement after the conversation
    // is complete, reply warmly with their upcoming appointment time.
    if (stage === "done") {
      const isThanks = /\b(thank|thanks|thank you|ty|ok|okay|great|perfect|sounds good|got it|understood|alright|appreciate|wonderful|excellent|awesome|cool|nice|good)\b/i.test(Body);
      if (isThanks) {
        const apptDatetime = await getAppointmentDatetime(patient.id);
        const CLOSING: Record<string, string> = {
          English:          apptDatetime
            ? `Thank you for choosing our office! We look forward to seeing you on ${apptDatetime}.`
            : `Thank you for choosing our office! We look forward to seeing you soon.`,
          Spanish:          apptDatetime
            ? `¡Gracias por elegirnos! Le esperamos el ${apptDatetime}.`
            : `¡Gracias por elegirnos! Le esperamos pronto.`,
          Portuguese:       apptDatetime
            ? `Obrigado por nos escolher! Aguardamos sua visita em ${apptDatetime}.`
            : `Obrigado por nos escolher! Aguardamos sua visita em breve.`,
          Mandarin:         apptDatetime
            ? `感谢您选择我们！期待在 ${apptDatetime} 见到您。`
            : `感谢您选择我们！期待很快见到您。`,
          Cantonese:        apptDatetime
            ? `感謝您選擇我們！期待在 ${apptDatetime} 見到您。`
            : `感謝您選擇我們！期待很快見到您。`,
          Arabic:           apptDatetime
            ? `شكراً لاختيارك عيادتنا! نتطلع إلى رؤيتك في ${apptDatetime}.`
            : `شكراً لاختيارك عيادتنا! نتطلع إلى رؤيتك قريباً.`,
          "Haitian Creole": apptDatetime
            ? `Mèsi dèske ou chwazi nou! N'ap tann ou ${apptDatetime}.`
            : `Mèsi dèske ou chwazi nou! N'ap tann ou byento.`,
        };
        const fallback = CLOSING[language] ?? CLOSING["English"]!;
        if (apptDatetime) {
          try {
            const llm = getLlm(activeAi.provider, activeAi.key, activeAi.model);
            const res = await llm.invoke([
              {
                role: "system",
                content: `You are a friendly dental office AI assistant. The patient just said "${Body}" after completing a conversation. Reply warmly in ${language}, thanking them for choosing the office and reminding them of their upcoming appointment on ${apptDatetime}. 1-2 sentences, no formatting.`,
              },
              { role: "user", content: Body },
            ]);
            const aiMsg = String(res.content).trim();
            if (aiMsg) return reply(aiMsg, "done");
          } catch { /* fall through to fallback */ }
        }
        return reply(fallback, "done");
      }
    }

    // ── Stage: initial / done (patient texts in fresh) ───────────────────
    if (stage === "initial" || stage === "done") {
      const openPhoneReply    = await storage.getOpenPhoneReply(patient.userId);
      const afterHoursEnabled = await getAfterHoursHandoff(patient.userId);
      const outsideHours      = await isAfterHours(patient.userId);

      if (openPhoneReply || (afterHoursEnabled && outsideHours)) {
        // MSG 1: AI self-introduction
        const rawGreeting = chatTemplates.newPatientGreeting ||
          `Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
        const introText = applyOfficeName(rawGreeting, officeName);

        // MSG 2: Empathetic acknowledgment of the patient's message
        const empatheticText = getEmpatheticAck(Body, language);

        // MSG 3: Detect reschedule vs new appointment from initial text
        const isRescheduleIntent = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);

        let msg3: string;
        let nextStage3: ConversationStage;

        if (isRescheduleIntent) {
          const apptDatetime = await getAppointmentDatetime(patient.id);
          if (apptDatetime) {
            const foundMsgs: Record<string, string> = {
              English:          `Ok. Just to confirm, your current appointment is on ${apptDatetime}. When would you like to reschedule?`,
              Spanish:          `De acuerdo. Solo para confirmar, su cita actual es el ${apptDatetime}. ¿Cuándo le gustaría reprogramarla?`,
              Portuguese:       `Ok. Só para confirmar, sua consulta atual é em ${apptDatetime}. Quando você gostaria de reagendar?`,
              Mandarin:         `好的。请确认一下，您当前的预约是 ${apptDatetime}。您想重新安排到什么时候？`,
              Cantonese:        `好的。請確認一下，您目前的預約是 ${apptDatetime}。您想重新安排到什麼時候？`,
              Arabic:           `حسناً. فقط للتأكيد، موعدك الحالي في ${apptDatetime}. متى تريد إعادة الجدولة؟`,
              "Haitian Creole": `Oke. Jis pou konfime, randevou aktyèl ou a se ${apptDatetime}. Ki lè ou ta renmen reprogramè?`,
            };
            msg3 = foundMsgs[language] ?? foundMsgs["English"]!;
            nextStage3 = "asked_reschedule_datetime";
          } else {
            const notFoundMsgs: Record<string, string> = {
              English:          "Ok. I could not find your current appointment. Please tell me when you'd like to reschedule and our receptionist will contact you as soon as possible.",
              Spanish:          "De acuerdo. No pude encontrar su cita actual. Por favor díganos cuándo le gustaría reprogramar y nuestra recepcionista le contactará lo antes posible.",
              Portuguese:       "Ok. Não consegui encontrar sua consulta atual. Por favor, diga-nos quando você gostaria de reagendar e nossa recepcionista entrará em contato o mais breve possível.",
              Mandarin:         "好的。我找不到您当前的预约。请告诉我您希望重新安排的时间，我们的前台将尽快与您联系。",
              Cantonese:        "好的。我找不到您目前的預約。請告訴我您希望重新安排的時間，我們的接待員將盡快與您聯絡。",
              Arabic:           "حسناً. لم أجد موعدك الحالي. يرجى إخبارنا بالوقت الذي تريد إعادة الجدولة إليه وسيتصل بك موظف الاستقبال في أقرب وقت ممكن.",
              "Haitian Creole": "Oke. Mwen pa t jwenn randevou aktyèl ou. Tanpri di nou ki lè ou ta renmen reprogramè epi resepsyonis nou an pral kontakte ou pi vit posib.",
            };
            msg3 = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
            nextStage3 = "done";
          }
        } else {
          // New appointment (or any other intent) → ask new or existing patient
          const newOrExistingMsgs: Record<string, string> = {
            English:          "Can you please tell me if you are a new or existing patient?",
            Spanish:          "¿Puede decirme si es usted un paciente nuevo o existente?",
            Portuguese:       "Pode me dizer se você é um paciente novo ou existente?",
            Mandarin:         "请问您是新患者还是现有患者？",
            Cantonese:        "請問您是新病人還是現有病人？",
            Arabic:           "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
            "Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
          };
          msg3 = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
          nextStage3 = "asked_new_or_existing";
        }

        // Save all 3 outbound messages and set stage
        await saveOutbound(patient.id, introText);
        await saveOutbound(patient.id, empatheticText);
        await saveOutbound(patient.id, msg3);
        await setStage(patient.userId, patient.id, nextStage3);

        // Send all 3 in one TwiML response — Twilio guarantees delivery order
        res.set("Content-Type", "text/xml");
        return res.send(twimlMessages(introText, empatheticText, msg3));
      }
    }

    res.set("Content-Type", "text/xml");
    return res.send(empty());

  } catch (err) {
    res.set("Content-Type", "text/xml");
    return res.send(empty());
  }
});

// ── POST /api/twilio/webhook/voice ────────────────────────────────────────────

router.post("/webhook/voice", async (req: Request, res: Response): Promise<any> => {
  try {
    const { From, CallSid } = req.body;

    const normalizedFrom = (From || "").replace(/\D/g, "");
    const allPatients = await db.patient.findMany({ select: { id: true, phone: true, userId: true } });
    const patient = allPatients.find(
      (p: { id: number; phone: string | null; userId: number }) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
    );

    let greeting = "Thank you for calling. Please leave a message after the beep and we will get back to you shortly.";
    if (patient) {
      const settings = await storage.getTwilioSettings(patient.userId);
      if (settings?.greetingMessage?.trim()) greeting = settings.greetingMessage.trim();
    }

    if (patient) {
      await storage.createCommunication({
        patientId: patient.id, channel: "voice", direction: "inbound",
        status: "completed", body: "(Inbound call — voicemail below)", twilioSid: CallSid,
      });
    }

    const recordingCallbackUrl = `${process.env.BASE_URL || "https://communitydentistsoflowell.mydentalofficemanagement.com"}/api/twilio/webhook/voice-recording`;

    res.set("Content-Type", "text/xml");
    return res.send(`<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="alice">${greeting}</Say>
  <Record maxLength="120" action="${recordingCallbackUrl}" transcribeCallback="${recordingCallbackUrl}" playBeep="true"/>
  <Say voice="alice">We did not receive a recording. Goodbye.</Say>
</Response>`);
  } catch (err) {
    res.set("Content-Type", "text/xml");
    return res.send(`<?xml version="1.0" encoding="UTF-8"?><Response><Say>Thank you for calling. Please try again later.</Say></Response>`);
  }
});

// ── POST /api/twilio/webhook/voice-recording ──────────────────────────────────

router.post("/webhook/voice-recording", async (req: Request, res: Response): Promise<any> => {
  try {
    const { CallSid, RecordingUrl } = req.body;
    if (RecordingUrl && CallSid) {
      const comm = await db.communication.findFirst({ where: { twilioSid: CallSid } });
      if (comm) {
        await db.communication.update({
          where: { id: comm.id },
          data:  { body: `Voicemail: ${RecordingUrl}.mp3` },
        });
      }
    }
    res.set("Content-Type", "text/xml");
    return res.send(empty());
  } catch (err) {
    res.set("Content-Type", "text/xml");
    return res.send(empty());
  }
});

// POST /api/twilio/webhook/voice-browser
// Called by Twilio when a browser Device (Voice SDK) places an outbound call.
// Looks up the office's Twilio phone number by AccountSid and dials the patient.
router.post("/webhook/voice-browser", async (req: Request, res: Response): Promise<any> => {
  try {
    const { To, AccountSid } = req.body as { To?: string; AccountSid?: string };
    if (!To || !AccountSid) {
      res.set("Content-Type", "text/xml");
      return res.send("<Response><Say>Missing call parameters.</Say></Response>");
    }
    const settings = await db.twilioSettings.findFirst({ where: { accountSid: AccountSid } });
    const callerId = settings?.phoneNumber || "";
    res.set("Content-Type", "text/xml");
    return res.send(
      `<Response><Dial callerId="${escapeXml(callerId)}"><Number>${escapeXml(To)}</Number></Dial></Response>`
    );
  } catch {
    res.set("Content-Type", "text/xml");
    return res.send("<Response><Say>An error occurred. Please try again.</Say></Response>");
  }
});

export default router;
