/**
 * Internal chat workflow — deterministic orchestration after LLM classification.
 *
 * Steps (no LLM involved here):
 *  1. Resolve patient from DB (by name or memberId)
 *  2. Determine insurance siteKey from patient record OR message hint
 *  3. Map procedure names → CDT codes via keyword lookup (check_and_claim only)
 *  4. Return a typed ChatResponse the route handler sends to the frontend
 */
import { ChatClassification } from "./internal-chat-graph";
import { lookupCdtCodes } from "./cdt-lookup";
import insuranceAliases from "../data/insuranceAliases.json";

// Phrases the user may write to mean "attach the uploaded file" — not CDT procedures
const ATTACHMENT_PHRASES = /^(with\s+)?(the\s+)?(x[\s-]?ray[s]?|xray[s]?|radiograph[s]?|image[s]?|film[s]?|attachment[s]?|attach|file[s]?|photo[s]?|picture[s]?|scan[s]?|doc(ument)?[s]?)$/i;

function stripAttachmentRefs(names: string[]): string[] {
  return names.filter((n) => !ATTACHMENT_PHRASES.test(n.trim()));
}

// ─── Types ────────────────────────────────────────────────────────────────────

export interface ResolvedPatient {
  id: number;
  firstName: string | null;
  lastName: string | null;
  insuranceId: string | null;
  insuranceProvider: string | null;
  dateOfBirth: string | null;
}

export interface CdtResult {
  code: string | null;
  description: string;
  input: string;
  toothNumber?: string;
  toothSurface?: string;
  quad?: string;
}

export interface ChatResponse {
  reply: string;
  action?:
    | "navigate"
    | "show_patient"
    | "check_eligibility_prefill"
    | "eligibility_id_ready"
    | "batch_eligibility_ready"
    | "batch_claim_ready"
    | "batch_check_and_claim_ready"
    | "check_and_claim_ready"
    | "need_insurance_clarification"
    | "appointment_created"
    | "claim_only_ready"
    | "preauth_ready"
    | "need_appointment_selection"
    | "need_cdt_clarification";
  actionData?: Record<string, any>;
}

// ─── insuranceProvider → siteKey mapping (mirrors claims.ts batchColumnDeriveSiteKey) ─

export function deriveSiteKey(provider: string): string {
  const p = (provider ?? "").toLowerCase().trim();
  if (!p) return "MH";
  for (const { keyword, siteKey } of insuranceAliases) {
    if (p.includes(keyword.toLowerCase())) return siteKey;
  }
  return "MH";
}

// Returns true when the user said "united" without a specific qualifier
// (e.g. "united sco", "united healthcare") — needs clarification.
function isAmbiguousUnited(hint: string | null | undefined): boolean {
  const h = (hint ?? "").toLowerCase().trim();
  return h === "united";
}

// siteKey → autoCheck value used by the insurance-status page prefill.
// For MH, pass the patient's DOB so under-21 patients route to CMSP.
export function siteKeyToAutoCheck(siteKey: string, dob?: string | Date | null): string {
  switch (siteKey) {
    case "CCA":        return "cca";
    case "DDMA":       return "ddma";
    case "DELTA_INS":  return "delta-ins";
    case "TUFTS_SCO":  return "tufts-sco";
    case "UNITED_SCO": return "united-sco";
    default: {
      // MassHealth: patients under 21 use the CMSP button
      if (dob) {
        try {
          const birth = dob instanceof Date ? dob : new Date(dob);
          const today = new Date();
          let age = today.getFullYear() - birth.getFullYear();
          const m = today.getMonth() - birth.getMonth();
          if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
          if (age < 21) return "cmsp";
        } catch {}
      }
      return "mh";
    }
  }
}

// ─── Storage interface (duck-typed, matches the real storage object) ──────────

interface StorageLike {
  searchPatients(opts: {
    filters: any;
    limit: number;
    offset: number;
  }): Promise<any[] | null>;
  getPatientByInsuranceId(id: string): Promise<any | null>;
  getPatientByInsuranceIdAndDob(id: string, dob: Date): Promise<any | null>;
  createAppointment(appointment: any): Promise<any>;
  getAppointmentsByDateForUser(dateStr: string, userId: number): Promise<any[]>;
  getOfficeHours(userId: number): Promise<any | null>;
  getAppointmentsByPatientId(patientId: number): Promise<any[]>;
}

// ─── Shared helpers ───────────────────────────────────────────────────────────

function calcAge(dob: string | null | undefined, on?: string | null): number | null {
  if (!dob) return null;
  try {
    const birth = new Date(dob);
    const ref = on ? new Date(on) : new Date();
    let age = ref.getFullYear() - birth.getFullYear();
    const m = ref.getMonth() - birth.getMonth();
    if (m < 0 || (m === 0 && ref.getDate() < birth.getDate())) age--;
    return age;
  } catch { return null; }
}

function checkD1120Age(
  matched: CdtResult[],
  patientName: string,
  dob: string | null | undefined,
  serviceDate?: string | null
): ChatResponse | null {
  if (!matched.some((r) => r.code === "D1120")) return null;
  const age = calcAge(dob, serviceDate);
  if (age === null || age < 14) return null;
  return {
    reply: `${patientName} is ${age} years old. D1120 (child prophy) is only for patients under 14 — I recommend using D1110 (adult prophy) instead. If you still need D1120, please claim manually for this patient.`,
  };
}

function patientToResult(p: any): ResolvedPatient {
  return {
    id: p.id,
    firstName: p.firstName ?? null,
    lastName: p.lastName ?? null,
    insuranceId: p.insuranceId ?? null,
    insuranceProvider: p.insuranceProvider ?? null,
    dateOfBirth: p.dateOfBirth
      ? (p.dateOfBirth instanceof Date
          ? p.dateOfBirth.toISOString().split("T")[0]
          : String(p.dateOfBirth).split("T")[0])
      : null,
  };
}

/** Look up by memberId, preferring the insuranceId+DOB combo when DOB is available. */
async function findPatientByMemberId(
  memberId: string,
  dob: string | null | undefined,
  storage: StorageLike
): Promise<any | null> {
  const dobDate = dob ? new Date(dob) : null;
  if (dobDate && !isNaN(dobDate.getTime())) {
    const byCombo = await storage.getPatientByInsuranceIdAndDob(memberId, dobDate);
    if (byCombo) return byCombo;
    // DOB provided but no record with that combo → this is a new family member not yet in DB
    return null;
  }
  return storage.getPatientByInsuranceId(memberId);
}

async function findPatientByName(
  name: string,
  storage: StorageLike
): Promise<any | null> {
  const patients = await storage.searchPatients({
    filters: {
      OR: [
        { firstName: { contains: name, mode: "insensitive" } },
        { lastName: { contains: name, mode: "insensitive" } },
        {
          AND: name.split(/\s+/).map((part: string) => ({
            OR: [
              { firstName: { contains: part, mode: "insensitive" } },
              { lastName: { contains: part, mode: "insensitive" } },
            ],
          })),
        },
      ],
    },
    limit: 5,
    offset: 0,
  });
  return patients?.[0] ?? null;
}

// ─── Workflow entry point ─────────────────────────────────────────────────────

export async function runInternalChatWorkflow(
  classification: ChatClassification,
  _userId: number,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[] = []
): Promise<ChatResponse> {
  const { intent } = classification;

  // ── Navigation ──────────────────────────────────────────────────────────────

  if (intent === "navigate_claims") {
    return {
      reply: classification.fallbackReply,
      action: "navigate",
      actionData: { url: "/claims" },
    };
  }
  if (intent === "navigate_schedule") {
    return {
      reply: classification.fallbackReply,
      action: "navigate",
      actionData: { url: "/appointments" },
    };
  }
  if (intent === "navigate_eligibility") {
    return {
      reply: classification.fallbackReply ?? "Opening the eligibility page...",
      action: "navigate",
      actionData: { url: "/insurance-status" },
    };
  }

  // ── Find patient (record only, no eligibility) ──────────────────────────────

  if (intent === "find_patient") {
    const name = classification.patientName?.trim();
    if (!name) {
      return { reply: "Please include the patient's name in your message." };
    }
    const raw = await findPatientByName(name, storage);
    if (!raw) {
      return {
        reply: `No patient found matching "${name}". Try a different spelling or search on the Patients page.`,
      };
    }
    const patient = patientToResult(raw);
    const ins = patient.insuranceProvider ? ` · ${patient.insuranceProvider}` : "";
    const id = patient.insuranceId ? ` (ID: ${patient.insuranceId})` : "";
    return {
      reply: `Found: ${patient.firstName ?? ""} ${patient.lastName ?? ""}${ins}${id}`.trim(),
      action: "show_patient",
      actionData: { patient },
    };
  }

  // ── Check eligibility by patient name ──────────────────────────────────────

  if (intent === "check_eligibility") {
    const name = classification.patientName?.trim();
    if (!name) {
      return { reply: "Please include the patient's name in your message." };
    }
    const raw = await findPatientByName(name, storage);
    if (!raw) {
      return {
        reply: `No patient found matching "${name}". Try a different spelling or search on the Patients page.`,
      };
    }
    const patient = patientToResult(raw);
    const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();

    if (!patient.insuranceId) {
      return {
        reply: `Found ${fullName} but no Member ID is on file. Please add their insurance info first.`,
        action: "show_patient",
        actionData: { patient },
      };
    }

    // If patient has DOB + known insurance, route through eligibility_id_ready so the
    // correct provider button (DDMA, CCA, etc.) auto-triggers instead of always MH.
    const resolvedDob = patient.dateOfBirth ?? null;
    const siteKey = resolveSiteKey(
      patient.insuranceProvider ?? null,
      classification.insuranceHint ?? null
    );
    if (resolvedDob && siteKey) {
      return {
        reply: `Found ${fullName}. Ready to check eligibility.`,
        action: "eligibility_id_ready",
        actionData: {
          patient,
          memberId: patient.insuranceId,
          dob: resolvedDob,
          siteKey,
          autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
        },
      };
    }

    return {
      reply: `Found ${fullName}. Ready to check eligibility.`,
      action: "check_eligibility_prefill",
      actionData: { patient },
    };
  }

  // ── Eligibility by explicit member ID + DOB ────────────────────────────────

  if (intent === "eligibility_by_id") {
    return await handleEligibilityById(classification, storage);
  }

  // ── Batch eligibility (multiple patients) ─────────────────────────────────

  if (intent === "batch_eligibility") {
    return await handleBatchEligibility(classification, storage);
  }

  if (intent === "batch_eligibility_by_name") {
    return await handleBatchEligibilityByName(classification, storage);
  }

  // ── Check eligibility + claim procedures ──────────────────────────────────

  if (intent === "check_and_claim") {
    return await handleCheckAndClaim(classification, storage, customAliases);
  }

  // ── Batch check & claim (eligibility + claim for multiple patients) ────────

  if (intent === "batch_check_and_claim") {
    return await handleBatchCheckAndClaim(classification, storage, customAliases);
  }

  // ── Batch claim (same procedures for multiple patients) ───────────────────

  if (intent === "batch_claim") {
    return await handleBatchClaim(classification, storage, customAliases);
  }

  // ── Claim only (no eligibility check) ─────────────────────────────────────

  if (intent === "claim_only") {
    return await handleClaimOnly(classification, storage, customAliases);
  }

  // ── Pre-authorization ──────────────────────────────────────────────────────

  if (intent === "preauth") {
    return await handlePreauth(classification, storage, customAliases);
  }

  // ── Schedule appointment ───────────────────────────────────────────────────

  if (intent === "schedule_appointment") {
    return await handleScheduleAppointment(classification, _userId, storage);
  }

  // ── General ────────────────────────────────────────────────────────────────
  return { reply: classification.fallbackReply };
}

// ─── eligibility_by_id ────────────────────────────────────────────────────────

async function handleEligibilityById(
  c: ChatClassification,
  storage: StorageLike
): Promise<ChatResponse> {
  const memberId = c.memberId?.trim();
  const dob = c.dob?.trim();

  if (!memberId) {
    return {
      reply: "Please provide a Member ID to run the eligibility check.",
    };
  }

  // User explicitly chose "Other United" — it's not in the app
  if ((c.insuranceHint ?? "").toLowerCase().includes("other united")) {
    return {
      reply: "That United plan is not currently supported in the app. Please check it manually.",
    };
  }

  // Try to resolve existing patient for name display + insurance (use DOB to pick the right family member)
  const existingPatient = await findPatientByMemberId(memberId, dob, storage);
  const patient: ResolvedPatient | null = existingPatient
    ? patientToResult(existingPatient)
    : null;

  // Use stored DOB if not provided in the message
  const resolvedDob = dob ?? patient?.dateOfBirth ?? null;
  if (!resolvedDob) {
    return {
      reply: "Please provide a Date of Birth (MM/DD/YYYY) to run the eligibility check.",
    };
  }

  // "united" alone is ambiguous — there's United SCO (Dental Hub) in the app
  // and other United plans that are not supported.
  if (isAmbiguousUnited(c.insuranceHint) && !patient?.insuranceProvider) {
    const label = patient
      ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
      : `Member ID ${memberId}`;
    return {
      reply: `Which United plan is ${label}?`,
      action: "need_insurance_clarification",
      actionData: {
        memberId,
        dob: resolvedDob,
        patient,
        options: ["United SCO / Dental Hub", "Other United (not in app)"],
      },
    };
  }

  // Determine siteKey
  const siteKey = resolveSiteKey(
    patient?.insuranceProvider ?? null,
    c.insuranceHint ?? null
  );

  if (!siteKey) {
    const name = patient
      ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
      : `Member ID ${memberId}`;
    return {
      reply: `Found ${name} but couldn't determine the insurance type. Which insurance should I use?`,
      action: "need_insurance_clarification",
      actionData: {
        memberId,
        dob: resolvedDob,
        patient,
        options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United SCO / Dental Hub"],
      },
    };
  }

  const label = patient
    ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
    : `Member ID ${memberId}`;

  return {
    reply: `Ready to check eligibility for ${label}.`,
    action: "eligibility_id_ready",
    actionData: {
      patient,
      memberId,
      dob: resolvedDob,
      siteKey,
      autoCheck: siteKeyToAutoCheck(siteKey),
      appointmentDate: c.appointmentDate ?? null,
    },
  };
}

// ─── batch_eligibility ───────────────────────────────────────────────────────

async function handleBatchEligibility(
  c: ChatClassification,
  storage: StorageLike
): Promise<ChatResponse> {
  const pairs = c.patients ?? [];
  if (pairs.length < 2) {
    // Fallback to single if somehow only 1 pair
    if (pairs.length === 1) {
      return await handleEligibilityById(
        { ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "eligibility_by_id" },
        storage
      );
    }
    return { reply: "Please provide at least two member ID + DOB pairs to batch-check." };
  }

  const resolved: {
    memberId: string;
    dob: string;
    siteKey: string;
    autoCheck: string;
    patient: ResolvedPatient | null;
  }[] = [];

  for (const { memberId, dob } of pairs) {
    const id = memberId?.trim();
    const d = dob?.trim();
    if (!id || !d) continue;

    const existing = await findPatientByMemberId(id, d, storage);
    const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
    const resolvedDob = d ?? patient?.dateOfBirth ?? null;
    if (!resolvedDob) continue;

    const siteKey = resolveSiteKey(
      patient?.insuranceProvider ?? null,
      c.insuranceHint ?? null
    ) ?? "MH";

    resolved.push({
      memberId: id,
      dob: resolvedDob,
      siteKey,
      autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
      patient,
    });
  }

  if (resolved.length === 0) {
    return { reply: "Could not parse any valid member ID + DOB pairs." };
  }

  const labels = resolved.map((r) => {
    const name = r.patient
      ? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
      : `ID ${r.memberId}`;
    return name;
  });

  return {
    reply: `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`,
    action: "batch_eligibility_ready",
    actionData: { queue: resolved },
  };
}

// ─── batch_eligibility_by_name ───────────────────────────────────────────────

async function handleBatchEligibilityByName(
  c: ChatClassification,
  storage: StorageLike
): Promise<ChatResponse> {
  const names = c.patientNames ?? [];
  if (names.length < 2) {
    return { reply: "Please include at least two patient names to batch-check eligibility." };
  }

  const resolved: {
    memberId: string;
    dob: string;
    siteKey: string;
    autoCheck: string;
    patient: ResolvedPatient | null;
  }[] = [];
  const notFound: string[] = [];
  const noInsurance: string[] = [];

  for (const name of names) {
    const trimmed = name.trim();
    if (!trimmed) continue;
    const raw = await findPatientByName(trimmed, storage);
    if (!raw) {
      notFound.push(trimmed);
      continue;
    }
    const patient = patientToResult(raw);
    const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();

    if (!patient.insuranceId) {
      noInsurance.push(fullName);
      continue;
    }

    const resolvedDob = patient.dateOfBirth ?? null;
    if (!resolvedDob) {
      noInsurance.push(fullName);
      continue;
    }

    const siteKey = resolveSiteKey(
      patient.insuranceProvider ?? null,
      c.insuranceHint ?? null
    ) ?? "MH";

    resolved.push({
      memberId: patient.insuranceId,
      dob: resolvedDob,
      siteKey,
      autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
      patient,
    });
  }

  if (resolved.length === 0) {
    if (notFound.length > 0) {
      return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
    }
    if (noInsurance.length > 0) {
      return { reply: `Found ${noInsurance.join(", ")} but they have no Member ID or DOB on file. Please add their insurance info first.` };
    }
    return { reply: "Could not resolve any patients for eligibility check." };
  }

  const labels = resolved.map((r) => {
    const name = r.patient
      ? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
      : `ID ${r.memberId}`;
    return name;
  });

  let reply = `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`;
  if (notFound.length > 0) {
    reply += ` Could not find: ${notFound.join(", ")}.`;
  }
  if (noInsurance.length > 0) {
    reply += ` Missing insurance info: ${noInsurance.join(", ")}.`;
  }

  return {
    reply,
    action: "batch_eligibility_ready",
    actionData: { queue: resolved },
  };
}

// ─── batch_check_and_claim ────────────────────────────────────────────────────

async function handleBatchCheckAndClaim(
  c: ChatClassification,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
  const pairs = c.patients ?? [];
  if (pairs.length < 2) {
    if (pairs.length === 1) {
      return await handleCheckAndClaim(
        { ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "check_and_claim" },
        storage,
        customAliases
      );
    }
    return { reply: "Please provide at least two member ID + DOB pairs." };
  }

  const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
  if (procedureNames.length === 0) {
    return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
  }

  const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
  const matched = cdtResults.filter((r) => r.code !== null);
  const unmatched = cdtResults.filter((r) => r.code === null);

  if (unmatched.length > 0) {
    const phrases = unmatched.map((r) => r.input);
    return {
      reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
      action: "need_cdt_clarification",
      actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
    };
  }

  const resolved: {
    memberId: string;
    dob: string;
    siteKey: string;
    autoCheck: string;
    patient: ResolvedPatient | null;
  }[] = [];

  for (const { memberId, dob } of pairs) {
    const id = memberId?.trim();
    const d = dob?.trim();
    if (!id || !d) continue;

    const existing = await findPatientByMemberId(id, d, storage);
    const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
    const resolvedDob = d ?? patient?.dateOfBirth ?? null;
    if (!resolvedDob) continue;

    const siteKey = resolveSiteKey(
      patient?.insuranceProvider ?? null,
      c.insuranceHint ?? null
    ) ?? "MH";

    resolved.push({
      memberId: id,
      dob: resolvedDob,
      siteKey,
      autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
      patient,
    });
  }

  if (resolved.length === 0) {
    return { reply: "Could not parse any valid member ID + DOB pairs." };
  }

  const labels = resolved.map((r) => {
    const name = r.patient
      ? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
      : `ID ${r.memberId}`;
    return name;
  });
  const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");

  return {
    reply: `Ready to check eligibility and claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`,
    action: "batch_check_and_claim_ready",
    actionData: {
      queue: resolved,
      matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
      renderingProvider: c.renderingProvider ?? null,
    },
  };
}

// ─── batch_claim ─────────────────────────────────────────────────────────────

async function handleBatchClaim(
  c: ChatClassification,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
  const names = c.patientNames ?? [];
  if (names.length < 2) {
    if (names.length === 1) {
      return await handleClaimOnly(
        { ...c, patientName: names[0], intent: "claim_only" },
        storage,
        customAliases
      );
    }
    return { reply: "Please include at least two patient names to batch-claim." };
  }

  const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
  if (procedureNames.length === 0) {
    return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
  }

  const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
  const matched = cdtResults.filter((r) => r.code !== null);
  const unmatched = cdtResults.filter((r) => r.code === null);

  if (unmatched.length > 0) {
    const phrases = unmatched.map((r) => r.input);
    return {
      reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
      action: "need_cdt_clarification",
      actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
    };
  }

  const resolved: {
    patient: ResolvedPatient;
    siteKey: string;
    serviceDate: string | null;
    appointmentId: number | null;
  }[] = [];
  const notFound: string[] = [];

  for (const name of names) {
    const trimmed = name.trim();
    if (!trimmed) continue;
    const raw = await findPatientByName(trimmed, storage);
    if (!raw) {
      notFound.push(trimmed);
      continue;
    }
    const patient = patientToResult(raw);
    const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();

    const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
    if (d1120Warning) return d1120Warning;

    const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";

    let serviceDate: string | null = c.appointmentDate ?? null;
    let appointmentId: number | null = null;
    if (!serviceDate) {
      const now = new Date();
      serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
    }

    resolved.push({ patient, siteKey, serviceDate, appointmentId });
  }

  if (notFound.length > 0 && resolved.length === 0) {
    return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
  }

  const labels = resolved.map((r) =>
    `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
  );
  const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
  let reply = `Ready to claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`;
  if (notFound.length > 0) {
    reply += ` Could not find: ${notFound.join(", ")}.`;
  }

  return {
    reply,
    action: "batch_claim_ready",
    actionData: {
      queue: resolved.map((r) => ({
        patient: r.patient,
        siteKey: r.siteKey,
        serviceDate: r.serviceDate,
        appointmentId: r.appointmentId,
      })),
      matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
      renderingProvider: c.renderingProvider ?? null,
    },
  };
}

// ─── check_and_claim ─────────────────────────────────────────────────────────

async function handleCheckAndClaim(
  c: ChatClassification,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[] = []
): Promise<ChatResponse> {
  // User explicitly chose "Other United" — it's not in the app
  if ((c.insuranceHint ?? "").toLowerCase().includes("other united")) {
    return {
      reply: "That United plan is not currently supported in the app. Please check it manually.",
    };
  }

  // 1. Resolve patient
  let patient: ResolvedPatient | null = null;
  let memberId = c.memberId?.trim() ?? null;
  const dob = c.dob?.trim() ?? null;

  if (memberId) {
    const existing = await findPatientByMemberId(memberId, dob, storage);
    if (existing) patient = patientToResult(existing);
  } else if (c.patientName?.trim()) {
    const raw = await findPatientByName(c.patientName.trim(), storage);
    if (raw) {
      patient = patientToResult(raw);
      memberId = patient.insuranceId;
    }
  }

  if (!memberId) {
    return {
      reply: "Please include either a Member ID or a patient name so I can look up their record.",
    };
  }

  // Use stored DOB if not provided in the message
  const resolvedDob = dob ?? patient?.dateOfBirth ?? null;
  if (!resolvedDob) {
    return {
      reply: `I have the Member ID (${memberId}) but couldn't find a Date of Birth on file. Please provide it (MM/DD/YYYY).`,
    };
  }

  // "united" alone is ambiguous — clarify before proceeding
  if (isAmbiguousUnited(c.insuranceHint) && !patient?.insuranceProvider) {
    const label = patient
      ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
      : `Member ID ${memberId}`;
    return {
      reply: `Which United plan is ${label}?`,
      action: "need_insurance_clarification",
      actionData: {
        memberId,
        dob: resolvedDob,
        patient,
        procedureNames: c.procedureNames ?? [],
        options: ["United SCO / Dental Hub", "Other United (not in app)"],
      },
    };
  }

  // 2. Determine siteKey
  const siteKey = resolveSiteKey(
    patient?.insuranceProvider ?? null,
    c.insuranceHint ?? null
  );

  if (!siteKey) {
    const label = patient
      ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
      : `Member ID ${memberId}`;
    return {
      reply: `Found ${label} but couldn't determine the insurance type. Which insurance should I use?`,
      action: "need_insurance_clarification",
      actionData: {
        memberId,
        dob,
        patient,
        procedureNames: c.procedureNames ?? [],
        options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United SCO / Dental Hub"],
      },
    };
  }

  // 3. Map procedure names → CDT codes (custom aliases take priority)
  const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
  const cdtResults: CdtResult[] = procedureNames.length > 0
    ? lookupCdtCodes(procedureNames, customAliases)
    : [];

  const matched = cdtResults.filter((r) => r.code !== null);
  const unmatched = cdtResults.filter((r) => r.code === null);

  // Block if any term couldn't be mapped — ask instead of proceeding
  if (unmatched.length > 0) {
    const phrases = unmatched.map((r) => r.input);
    return {
      reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
      action: "need_cdt_clarification",
      actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
    };
  }

  const label = patient
    ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim()
    : `Member ID ${memberId}`;

  // D1120 age check — recommend D1110 for patients 14+
  const d1120Warning = checkD1120Age(matched, label, patient?.dateOfBirth ?? resolvedDob, c.appointmentDate);
  if (d1120Warning) return d1120Warning;

  const reply = `Ready to check eligibility for ${label} and claim: ${
    matched.map((r) => `${r.code} (${r.description})`).join(", ") || "no procedures mapped"
  }.`;

  return {
    reply,
    action: "check_and_claim_ready",
    actionData: {
      patient,
      memberId,
      dob: resolvedDob,
      siteKey,
      autoCheck: siteKeyToAutoCheck(siteKey),
      cdtResults,
      matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
      serviceDate: c.appointmentDate ?? null,
      renderingProvider: c.renderingProvider ?? null,
    },
  };
}

// ─── claim_only ───────────────────────────────────────────────────────────────

async function handleClaimOnly(
  c: ChatClassification,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
  // Resolve patient — use insuranceId+DOB when DOB is available to distinguish family members
  let patient: ResolvedPatient | null = null;

  if (c.memberId?.trim()) {
    const existing = await findPatientByMemberId(c.memberId.trim(), c.dob, storage);
    if (existing) patient = patientToResult(existing);
  } else if (c.patientName?.trim()) {
    const raw = await findPatientByName(c.patientName.trim(), storage);
    if (raw) patient = patientToResult(raw);
  }

  if (!patient) {
    return {
      reply: "Please include a patient name or Member ID so I can look them up.",
    };
  }

  const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();

  // Map procedure names → CDT codes
  const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
  if (procedureNames.length === 0) {
    return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
  }

  const cdtResults = lookupCdtCodes(procedureNames, customAliases);
  const matched = cdtResults.filter((r) => r.code !== null);
  const unmatched = cdtResults.filter((r) => r.code === null);

  // Block if any term couldn't be mapped — ask instead of proceeding
  if (unmatched.length > 0) {
    const phrases = unmatched.map((r) => r.input);
    return {
      reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
      action: "need_cdt_clarification",
      actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
    };
  }

  // D1120 age check — recommend D1110 for patients 14+
  const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
  if (d1120Warning) return d1120Warning;

  // Resolve service date: use explicit date from message, then latest appointment, then ask
  let serviceDate: string | null = c.appointmentDate ?? null;
  let appointmentId: number | null = null;

  if (!serviceDate) {
    const now = new Date();
    const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;

    const appts = await storage.getAppointmentsByPatientId(patient.id);
    const sorted = appts.sort((a: any, b: any) =>
      new Date(b.date).getTime() - new Date(a.date).getTime()
    );

    if (sorted.length > 0) {
      const rawDate = new Date(sorted[0].date);
      const latestStr = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;

      if (latestStr === todayStr) {
        serviceDate = todayStr;
        appointmentId = sorted[0].id ?? null;
      } else {
        const fmtUTC = (a: any) => {
          const d = new Date(a.date);
          return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
        };
        const todayLabel = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}/${now.getFullYear()}`;
        return {
          reply: `Which service date for ${fullName}?`,
          action: "need_appointment_selection",
          actionData: {
            patient,
            siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
            matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
            options: [
              { label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: latestStr },
              { label: `${todayLabel} (Today)`, appointmentId: null, serviceDate: todayStr },
            ],
          },
        };
      }
    } else {
      serviceDate = todayStr;
    }
  }

  const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";

  const [sy, sm, sd] = serviceDate.split("-");
  const dateLabel = `${sm}/${sd}/${sy}`;

  let reply = `Opening claim for ${fullName} (service date ${dateLabel}): ${
    matched.map((r) => `${r.code} (${r.description})`).join(", ") || "no procedures matched"
  }.`;
  if (unmatched.length > 0) {
    reply += ` Could not map: ${unmatched.map((r) => `"${r.input}"`).join(", ")} — verify manually.`;
  }

  return {
    reply,
    action: "claim_only_ready",
    actionData: {
      patient,
      siteKey,
      serviceDate,
      appointmentId,
      matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
      renderingProvider: c.renderingProvider ?? null,
    },
  };
}

// ─── preauth ──────────────────────────────────────────────────────────────────

async function handlePreauth(
  c: ChatClassification,
  storage: StorageLike,
  customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
  let patient: ResolvedPatient | null = null;

  if (c.memberId?.trim()) {
    const existing = await findPatientByMemberId(c.memberId.trim(), c.dob, storage);
    if (existing) patient = patientToResult(existing);
  } else if (c.patientName?.trim()) {
    const raw = await findPatientByName(c.patientName.trim(), storage);
    if (raw) patient = patientToResult(raw);
  }

  if (!patient) {
    return { reply: "Please include a patient name or Member ID so I can look them up." };
  }

  const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
  if (procedureNames.length === 0) {
    return { reply: "Please specify which procedures to pre-authorize (e.g. rct, post, crown)." };
  }

  const cdtResults = lookupCdtCodes(procedureNames, customAliases);
  const matched = cdtResults.filter((r) => r.code !== null);
  const unmatched = cdtResults.filter((r) => r.code === null);

  if (unmatched.length > 0) {
    const phrases = unmatched.map((r) => r.input);
    return {
      reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D3320)`,
      action: "need_cdt_clarification",
      actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
    };
  }

  // Use explicit date from message; otherwise today+3 (pre-auth is for a future appointment)
  let serviceDate: string = c.appointmentDate ?? (() => {
    const d = new Date();
    d.setDate(d.getDate() + 3);
    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
  })();

  const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
  const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
  const [sy, sm, sd] = serviceDate.split("-");
  const dateLabel = `${sm}/${sd}/${sy}`;

  return {
    reply: `Opening pre-auth for ${fullName} (tentative date ${dateLabel}): ${matched.map((r) => `${r.code} (${r.description})`).join(", ")}.`,
    action: "preauth_ready",
    actionData: {
      patient,
      siteKey,
      serviceDate,
      matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
      renderingProvider: c.renderingProvider ?? null,
    },
  };
}

// ─── schedule_appointment ─────────────────────────────────────────────────────

const DEFAULT_STAFF_ID = 1; // Column A
const SLOT_DURATION = 15;   // minutes

/** Convert "HH:MM" to total minutes since midnight */
function toMinutes(t: string): number {
  const [h, m] = t.split(":").map(Number);
  return (h ?? 0) * 60 + (m ?? 0);
}

/** Convert total minutes since midnight to "HH:MM" */
function fromMinutes(m: number): string {
  return `${String(Math.floor(m / 60)).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
}

/**
 * Build the list of valid start minutes for a day using office hours.
 * Skips the lunch gap (amEnd → pmStart).
 */
function buildSlots(dayHours: { amStart: string; amEnd: string; pmStart: string; pmEnd: string }): number[] {
  const slots: number[] = [];
  const ranges = [
    [toMinutes(dayHours.amStart), toMinutes(dayHours.amEnd)],
    [toMinutes(dayHours.pmStart), toMinutes(dayHours.pmEnd)],
  ];
  for (const [start, end] of ranges) {
    for (let t = start!; t + SLOT_DURATION <= end!; t += SLOT_DURATION) {
      slots.push(t);
    }
  }
  return slots;
}

async function handleScheduleAppointment(
  c: ChatClassification,
  userId: number,
  storage: StorageLike
): Promise<ChatResponse> {
  const name = c.patientName?.trim();
  if (!name) {
    return { reply: "Please include the patient's name so I can find them." };
  }

  const raw = await findPatientByName(name, storage);
  if (!raw) {
    return {
      reply: `No patient found matching "${name}". Please check the spelling or add them on the Patients page first.`,
    };
  }

  const patient = patientToResult(raw);
  const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();

  // Resolve date
  const today = new Date();
  const localDate = c.appointmentDate
    ? new Date(c.appointmentDate + "T00:00:00")
    : new Date(today.getFullYear(), today.getMonth(), today.getDate());
  const dateStr = `${localDate.getFullYear()}-${String(localDate.getMonth() + 1).padStart(2, "0")}-${String(localDate.getDate()).padStart(2, "0")}`;
  const dateLabel = localDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });

  // If time was explicitly provided, use it directly
  if (c.appointmentTime) {
    const startTime = c.appointmentTime;
    const startMin = toMinutes(startTime);
    const endTime = fromMinutes(startMin + SLOT_DURATION);
    await storage.createAppointment({
      patientId: patient.id,
      userId,
      staffId: DEFAULT_STAFF_ID,
      title: dateLabel,
      date: localDate,
      startTime,
      endTime,
      type: "recall",
      status: "scheduled",
      movedByAi: true,
    });
    return {
      reply: `Scheduled ${fullName} on ${dateLabel} at ${startTime} (Column A).`,
      action: "appointment_created",
      actionData: { patient, date: dateStr, startTime, endTime },
    };
  }

  // No time specified — find earliest available slot in Column A
  const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
  const dayName = dayNames[localDate.getDay()]!;

  // Load office hours (fall back to 9–12 / 13–17 if not configured)
  const officeHours = await storage.getOfficeHours(userId);
  const dayHours = officeHours?.data?.doctors?.[dayName] ?? {
    amStart: "09:00", amEnd: "12:00", pmStart: "13:00", pmEnd: "17:00", enabled: true,
  };

  if (!dayHours.enabled) {
    return { reply: `The office is closed on ${dayName}. Please choose a different day.` };
  }

  const allSlots = buildSlots(dayHours);

  // Fetch all Column A appointments for that day
  const existing = (await storage.getAppointmentsByDateForUser(dateStr, userId))
    .filter((a: any) => a.staffId === DEFAULT_STAFF_ID);

  // Find first slot that doesn't overlap any existing appointment
  const booked = existing.map((a: any) => ({
    start: toMinutes(a.startTime),
    end: toMinutes(a.endTime),
  }));

  const availableStart = allSlots.find((slotStart) => {
    const slotEnd = slotStart + SLOT_DURATION;
    return !booked.some((b) => slotStart < b.end && slotEnd > b.start);
  });

  if (availableStart === undefined) {
    return {
      reply: `Column A is fully booked on ${dateLabel}. Please pick a different date or time.`,
    };
  }

  const startTime = fromMinutes(availableStart);
  const endTime = fromMinutes(availableStart + SLOT_DURATION);

  await storage.createAppointment({
    patientId: patient.id,
    userId,
    staffId: DEFAULT_STAFF_ID,
    title: dateLabel,
    date: localDate,
    startTime,
    endTime,
    type: "recall",
    status: "scheduled",
    movedByAi: true,
  });

  return {
    reply: `Scheduled ${fullName} on ${dateLabel} at ${startTime} (Column A) — earliest available slot.`,
    action: "appointment_created",
    actionData: { patient, date: dateStr, startTime, endTime },
  };
}

// ─── Standalone helper: create today's appointment for a known patientId ─────

export async function createAppointmentToday(
  patientId: number,
  userId: number,
  storage: StorageLike,
  targetDate?: string   // YYYY-MM-DD; defaults to today
): Promise<{ startTime: string; endTime: string; dateStr: string; dateLabel: string; column: string } | { error: string }> {
  const base = targetDate ? new Date(targetDate + "T00:00:00") : new Date();
  const dateStr = `${base.getFullYear()}-${String(base.getMonth() + 1).padStart(2, "0")}-${String(base.getDate()).padStart(2, "0")}`;
  const dateLabel = base.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
  const localDate = new Date(base.getFullYear(), base.getMonth(), base.getDate());

  const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
  const dayName = dayNames[base.getDay()]!;

  const officeHours = await storage.getOfficeHours(userId);
  const dayHours = officeHours?.data?.doctors?.[dayName] ?? {
    amStart: "09:00", amEnd: "12:00", pmStart: "13:00", pmEnd: "17:00", enabled: true,
  };

  if (!dayHours.enabled) {
    return { error: `The office is closed on ${dayName} (${dateLabel}). Cannot create appointment.` };
  }

  const allSlots = buildSlots(dayHours);
  const allAppointments = await storage.getAppointmentsByDateForUser(dateStr, userId);

  for (const { staffId, label } of [
    { staffId: DEFAULT_STAFF_ID, label: "Column A" },
    { staffId: 2, label: "Column B" },
  ]) {
    const booked = allAppointments
      .filter((a: any) => a.staffId === staffId)
      .map((a: any) => ({ start: toMinutes(a.startTime), end: toMinutes(a.endTime) }));

    const availableStart = allSlots.find((slotStart) => {
      const slotEnd = slotStart + SLOT_DURATION;
      return !booked.some((b) => slotStart < b.end && slotEnd > b.start);
    });

    if (availableStart !== undefined) {
      const startTime = fromMinutes(availableStart);
      const endTime = fromMinutes(availableStart + SLOT_DURATION);
      await storage.createAppointment({
        patientId,
        userId,
        staffId,
        title: dateLabel,
        date: localDate,
        startTime,
        endTime,
        type: "recall",
        status: "scheduled",
        movedByAi: true,
      });
      return { startTime, endTime, dateStr, dateLabel, column: label };
    }
  }

  return { error: `Both Column A and Column B are fully booked today. Please add the appointment manually.` };
}

// ─── Insurance resolution helper ──────────────────────────────────────────────

/**
 * Determine siteKey from:
 *  1. Patient's stored insuranceProvider (most authoritative)
 *  2. Insurance hint from the chat message
 *  3. null → caller must ask for clarification
 */
function resolveSiteKey(
  storedProvider: string | null,
  hint: string | null
): string | null {
  if (storedProvider) return deriveSiteKey(storedProvider);
  if (hint) return deriveSiteKey(hint);
  return null;
}
