import { Router } from "express";
import { Request, Response } from "express";
import { storage } from "../storage";
import { z } from "zod";
import multer from "multer";
import { forwardToSeleniumClaimAgent } from "../services/seleniumClaimClient";
import path from "path";
import fs from "fs";
import axios from "axios";
import archiver from "archiver";
import { seleniumQueue } from "../queue/queues";
import { enqueueSeleniumJob } from "../queue/jobRunner";
import { Prisma } from "@repo/db/generated/prisma";
import { Decimal } from "decimal.js";
import {
  ExtendedClaimSchema,
  InputServiceLine,
  updateClaimSchema,
} from "@repo/db/types";
import { forwardToSeleniumClaimPreAuthAgent } from "../services/seleniumInsuranceClaimPreAuthClient";
import { formatDobForAgent } from "../utils/dateUtils";

const router = Router();

// ── Appointment-type inference (mirrors frontend appointmentTypeUtils.ts) ──────
function codeToApptType(raw: string): string | null {
  const c = raw.replace(/\s/g, "").toUpperCase();
  if (c === "D1351" || c === "D2930" || c === "D3220") return "pedo";
  if (c === "D9110") return "emergency";
  if (c === "D9310") return "consultation";
  if (/^D3/.test(c)) return "endo";
  if (/^D6/.test(c)) return "implant";
  if (/^D2[78]/.test(c)) return "crown";
  if (/^D5[1-8]/.test(c)) return "dentures";
  if (/^D71/.test(c)) return "extraction";
  if (/^D4[3-9]/.test(c)) return "perio";
  if (/^D2/.test(c)) return "filling";
  if (/^D8/.test(c)) return "ortho";
  if (/^D[01]/.test(c)) return "recall";
  return null;
}

function inferApptTypeFromCodes(codes: string[]): string | null {
  const priority = ["endo","implant","crown","pedo","dentures","extraction","perio","filling","ortho","recall","consultation","emergency"];
  const scores: Record<string, number> = {};
  for (const code of codes) {
    const t = codeToApptType(code);
    if (t) scores[t] = (scores[t] ?? 0) + 1;
  }
  let best: string | null = null, bestCount = 0;
  for (const t of priority) {
    const n = scores[t] ?? 0;
    if (n > bestCount) { best = t; bestCount = n; }
  }
  return best;
}

/** Update appointment.type if the inferred type differs and the user has not locked it. */
async function syncAppointmentType(appointmentId: number | null | undefined, codes: string[]): Promise<void> {
  if (!appointmentId || !codes.length) return;
  const inferred = inferApptTypeFromCodes(codes);
  if (!inferred) return;
  try {
    const apt = await storage.getAppointment(appointmentId);
    if (apt && !(apt as any).typeLocked && apt.type !== inferred) {
      await storage.updateAppointment(appointmentId, { type: inferred } as any);
    }
  } catch {
    // Non-fatal — don't break claim creation if type sync fails
  }
}

// Routes
const multerStorage = multer.memoryStorage(); // NO DISK
const upload = multer({
  storage: multerStorage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit per file
  fileFilter: (req, file, cb) => {
    const allowed = [
      "application/pdf",
      "image/jpeg",
      "image/png",
      "image/webp",
    ];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error("Unsupported file type"));
    }
  },
});


// POST /api/claims/upload-to-cloud
// Saves uploaded files to both:
//   - Patient Documents (visible on Documents page)
//   - Cloud Storage patient folder (visible on Cloud Storage page)
router.post(
  "/upload-to-cloud",
  upload.array("files", 10),
  async (req: Request, res: Response): Promise<any> => {
    if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });

    const files = req.files as Express.Multer.File[];
    if (!files?.length) return res.status(400).json({ error: "No files uploaded" });

    const patientId = Number(req.body.patientId);
    const patientName = String(req.body.patientName || "unknown").replace(/[/\\?%*:|"<>]/g, "-").trim();

    if (!patientId || isNaN(patientId)) {
      return res.status(400).json({ error: "Invalid patientId" });
    }

    try {
      const folder = await storage.getOrCreatePatientFolder(req.user.id, patientId, patientName);
      const result: { filename: string; mimeType: string; filePath: string }[] = [];

      for (const file of files) {
        // Save to patient-documents (Documents page)
        await storage.createPatientDocument(
          patientId,
          file.originalname,
          file.originalname,
          file.mimetype,
          file.size,
          file.buffer
        );

        // Save to cloud storage patient folder (Cloud Storage page)
        const cloudFile = await storage.initializeFileUpload(
          req.user.id,
          file.originalname,
          file.mimetype,
          BigInt(file.size),
          1,
          (folder as any).id
        );
        await storage.appendFileChunk((cloudFile as any).id, 0, file.buffer);
        const finalized = await storage.finalizeFileUpload((cloudFile as any).id);

        result.push({
          filename: file.originalname,
          mimeType: file.mimetype,
          filePath: finalized.diskPath,
        });
      }

      return res.json({ error: false, data: result });
    } catch (err: any) {
      console.error("[upload-to-cloud]", err);
      return res.status(500).json({ error: "Failed to upload files", message: err?.message });
    }
  }
);

router.post(
  "/mh-provider-login",
  async (req: Request, res: Response): Promise<any> => {
    if (!req.user || !req.user.id) {
      return res.status(401).json({ error: "Unauthorized: user info missing" });
    }

    try {
      const { memberId, dateOfBirth, submissionDate, firstName, lastName, procedureCode, toothNumber, toothSurface, insuranceSiteKey } = req.body;
      if (!memberId || !dateOfBirth || !submissionDate || !firstName || !lastName || !procedureCode || !insuranceSiteKey) {
        return res.status(400).json({ error: "Missing required fields: memberId, dateOfBirth, submissionDate, firstName, lastName, procedureCode, insuranceSiteKey" });
      }

      const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
        req.user.id,
        insuranceSiteKey
      );
      if (!credentials) {
        return res.status(404).json({
          error:
            "No insurance credentials found for this provider. Kindly Update this at Settings Page.",
        });
      }

      const enrichedData = {
        data: {
          memberId,
          dateOfBirth,
          submissionDate,
          firstName,
          lastName,
          procedureCode,
          toothNumber,
          toothSurface,
          insuranceSiteKey,
          massdhpUsername: credentials.username,
          massdhpPassword: credentials.password,
        },
      };

      const seleniumRes = await axios.post(
        "http://localhost:5002/claims-login",
        enrichedData
      );

      const result = seleniumRes.data;
      if (result?.status !== "success") {
        return res.status(502).json({ error: result?.message || "Selenium service error" });
      }

      return res.json({ status: "success", message: "Claims automation completed. Browser remains open." });
    } catch (err: any) {
      console.error(err);
      return res.status(500).json({
        error: err?.message || "Failed to contact selenium service",
      });
    }
  }
);

router.post(
  "/selenium-claim",
  upload.fields([
    { name: "pdfs", maxCount: 10 },
    { name: "images", maxCount: 10 },
  ]),
  async (req: Request, res: Response): Promise<any> => {
    if (!req.files || !req.body.data) {
      return res
        .status(400)
        .json({ error: "Missing files or claim data for selenium" });
    }

    if (!req.user || !req.user.id) {
      return res.status(401).json({ error: "Unauthorized: user info missing" });
    }

    try {
      const claimData = JSON.parse(req.body.data);
      const pdfs =
        (req.files as Record<string, Express.Multer.File[]>).pdfs ?? [];
      const images =
        (req.files as Record<string, Express.Multer.File[]>).images ?? [];

      const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
        req.user.id,
        claimData.insuranceSiteKey
      );
      if (!credentials) {
        return res.status(404).json({
          error:
            "No insurance credentials found for this provider. Kindly Update this at Settings Page.",
        });
      }

      const enrichedData = {
        ...claimData,
        massdhpUsername: credentials.username,
        massdhpPassword: credentials.password,
      };

      // Encode file buffers as base64 so they can be stored in Redis
      const filesForQueue = [...pdfs, ...images].map((f) => ({
        originalname: f.originalname,
        bufferBase64: f.buffer.toString("base64"),
        mimetype: f.mimetype,
      }));

      const job = await seleniumQueue.add("claim-submit", {
        jobType: "claim-submit",
        userId: req.user.id,
        socketId: req.body.socketId,
        enrichedPayload: enrichedData,
        files: filesForQueue,
        claimId: claimData.claimId,
      });

      return res.json({ jobId: job.id, status: "queued" });
    } catch (err: any) {
      console.error(err);
      return res.status(500).json({
        error: err.message || "Failed to enqueue selenium claim job",
      });
    }
  }
);

router.post(
  "/selenium/fetchpdf",
  async (req: Request, res: Response): Promise<any> => {
    function sendError(res: Response, message: string, status = 400) {
      console.error("Error:", message);
      return res.status(status).json({ error: message });
    }

    try {
      if (!req.user || !req.user.id) {
        return sendError(res, "Unauthorized: user info missing", 401);
      }

      const { patientId, pdf_url, groupTitleKey } = req.body;

      if (!pdf_url) {
        return sendError(res, "Missing pdf_url");
      }

      if (!patientId) {
        return sendError(res, "Missing Patient Id");
      }

      const parsedPatientId = parseInt(patientId);

      console.log("Fetching PDF from URL:", pdf_url);
      const filename = path.basename(new URL(pdf_url).pathname);
      console.log("Extracted filename:", filename);

      // Always fetch from localhost regardless of what hostname is in the pdf_url,
      // since both backend and selenium service run on the same machine.
      const seleniumPort = process.env.SELENIUM_PORT || "5002";
      const localPdfUrl = `http://localhost:${seleniumPort}/downloads/${filename}`;
      console.log("Fetching PDF from local URL:", localPdfUrl);

      const pdfResponse = await axios.get(localPdfUrl, {
        responseType: "arraybuffer",
        timeout: 15000,
      });
      console.log("PDF fetched successfully, size:", pdfResponse.data.length);

      // Allowed keys as a literal tuple to derive a union type
      const allowedKeys = [
        "INSURANCE_CLAIM",
        "INSURANCE_CLAIM_PREAUTH",
      ] as const;
      type GroupKey = (typeof allowedKeys)[number];
      const isGroupKey = (v: any): v is GroupKey =>
        (allowedKeys as readonly string[]).includes(v);

      if (!isGroupKey(groupTitleKey)) {
        return sendError(
          res,
          `Invalid groupTitleKey. Must be one of: ${allowedKeys.join(", ")}`
        );
      }

      const GROUP_TITLES: Record<GroupKey, string> = {
        INSURANCE_CLAIM: "Claims",
        INSURANCE_CLAIM_PREAUTH: "Preauth",
      };
      const groupTitle = GROUP_TITLES[groupTitleKey];

      // ✅ Find or create PDF group for this claim
      let group = await storage.findPdfGroupByPatientTitleKey(
        parsedPatientId,
        groupTitleKey
      );

      if (!group) {
        group = await storage.createPdfGroup(
          parsedPatientId,
          groupTitle,
          groupTitleKey
        );
      }

      // ✅ Save PDF file into that group
      const created = await storage.createPdfFile(group.id!, filename, pdfResponse.data);
      
      // Extract the PDF file ID for opening the viewer
      let pdfFileId: number | null = null;
      if (created && typeof created === "object" && "id" in created) {
        pdfFileId = Number(created.id);
      } else if (typeof created === "number") {
        pdfFileId = created;
      }

      return res.json({
        success: true,
        pdfPath: `/temp/${filename}`,
        pdf_url,
        fileName: filename,
        pdfFileId,
        // pdfFilename: filename,
      });
    } catch (err: any) {
      console.error("Error in /selenium/fetchpdf:", err);
      console.error("Error details:", {
        message: err.message,
        code: err.code,
        response: err.response?.status,
        responseData: err.response?.data,
      });
      const errorMsg = err.response?.data || err.message || "Failed to Fetch and Download the pdf";
      return sendError(res, `Failed to Fetch and Download the pdf: ${errorMsg}`, 500);
    }
  }
);

router.post(
  "/selenium-claim-pre-auth",
  upload.fields([
    { name: "pdfs", maxCount: 10 },
    { name: "images", maxCount: 10 },
  ]),
  async (req: Request, res: Response): Promise<any> => {
    if (!req.files || !req.body.data) {
      return res
        .status(400)
        .json({ error: "Missing files or claim data for selenium" });
    }

    if (!req.user || !req.user.id) {
      return res.status(401).json({ error: "Unauthorized: user info missing" });
    }

    try {
      const claimData = JSON.parse(req.body.data);
      const pdfs =
        (req.files as Record<string, Express.Multer.File[]>).pdfs ?? [];
      const images =
        (req.files as Record<string, Express.Multer.File[]>).images ?? [];

      const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
        req.user.id,
        claimData.insuranceSiteKey
      );
      if (!credentials) {
        return res.status(404).json({
          error:
            "No insurance credentials found for this provider. Kindly Update this at Settings Page.",
        });
      }

      const enrichedData = {
        ...claimData,
        massdhpUsername: credentials.username,
        massdhpPassword: credentials.password,
      };

      // Create a minimal PREAUTH claim record so the preauth number can be stored and shown
      let preAuthClaimId: number | undefined;
      try {
        const serviceDate = claimData.serviceDate
          ? new Date(claimData.serviceDate)
          : new Date();
        const dob = claimData.dateOfBirth
          ? new Date(claimData.dateOfBirth)
          : new Date("2000-01-01");

        const preAuthRecord = await storage.createClaim({
          patientId: Number(claimData.patientId),
          appointmentId: claimData.appointmentId ? Number(claimData.appointmentId) : null,
          userId: req.user.id,
          staffId: Number(claimData.staffId) || 1,
          patientName: claimData.patientName || "",
          memberId: claimData.memberId || "",
          dateOfBirth: dob,
          remarks: claimData.remarks || "",
          missingTeethStatus: claimData.missingTeethStatus || "No_missing",
          serviceDate,
          insuranceProvider: "MassHealth",
          status: "PREAUTH",
        } as any);
        preAuthClaimId = preAuthRecord.id;
      } catch (e: any) {
        console.error("[preauth] failed to create preauth record:", e?.message);
      }

      const filesForQueue = [...pdfs, ...images].map((f) => ({
        originalname: f.originalname,
        bufferBase64: f.buffer.toString("base64"),
        mimetype: f.mimetype,
      }));

      const job = await seleniumQueue.add("claim-pre-auth", {
        jobType: "claim-pre-auth",
        userId: req.user.id,
        socketId: req.body.socketId,
        enrichedPayload: enrichedData,
        files: filesForQueue,
        claimId: preAuthClaimId ?? claimData.claimId,
      });

      return res.json({ jobId: job.id, status: "queued" });
    } catch (err: any) {
      console.error(err);
      return res.status(500).json({
        error: err.message || "Failed to enqueue selenium pre-auth job",
      });
    }
  }
);

// Maps patient.insuranceProvider free-text → the siteKey stored in InsuranceCredential
function batchColumnDeriveSiteKey(provider: string): string {
  const p = provider.toLowerCase().trim();
  if (!p || p.includes("masshealth") || p === "mh" || p === "mass health") return "MH";
  if (p.includes("commonwealth care alliance") || p === "cca") return "CCA";
  if (p.includes("ddma") || p.includes("delta dental ma")) return "DDMA";
  if (p.includes("tufts") || p.includes("dentaquest") || p === "tuftssco") return "TUFTS_SCO";
  if ((p.includes("united") && p.includes("sco")) || p.includes("dentalhub") || p === "united_sco") return "UNITED_SCO";
  return "MH"; // default fallback
}

// Returns the canonical insuranceProvider name stored in the Claim record
function batchColumnCanonicalName(siteKey: string): string {
  switch (siteKey) {
    case "CCA": return "CCA";
    case "DDMA": return "Delta Dental MA";
    case "TUFTS_SCO": return "Tufts SCO";
    case "UNITED_SCO": return "United/DentalHub";
    default: return "MassHealth";
  }
}

// POST /api/claims/batch-column
// Query params: date=YYYY-MM-DD (required), staffIds=1,2 (required)
// For each appointment in the selected staff columns:
//   - skip if no saved AppointmentProcedure records
//   - otherwise create a Claim + Payment, enqueue selenium claim-submit job
router.post(
  "/batch-column",
  async (req: Request, res: Response): Promise<any> => {
    const date = String(req.query.date ?? "").trim();
    const staffIdsRaw = String(req.query.staffIds ?? "").trim();

    if (!date) return res.status(400).json({ error: "Missing date query param" });
    if (!staffIdsRaw) return res.status(400).json({ error: "Missing staffIds query param" });
    if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });

    const staffIdFilter = new Set(
      staffIdsRaw.split(",").map(Number).filter((n) => Number.isFinite(n) && n > 0)
    );

    try {
      const allAppointments = await storage.getAppointmentsByDateForUser(date, req.user.id);
      const appointments = allAppointments.filter((a) => staffIdFilter.has(Number(a.staffId)));

      const results: Array<any> = [];

      for (const apt of appointments) {
        const resultItem: any = {
          appointmentId: apt.id,
          patientId: apt.patientId ?? null,
          processed: false,
          skipped: false,
          error: null,
          jobId: null,
          claimId: null,
        };

        try {
          // Fetch active claim for this appointment (includes service lines from draft saves)
          const activeClaim = await storage.getActiveClaimByAppointmentId(Number(apt.id));

          // Skip if claim was voided via the "Void" button in Select Procedures.
          if (activeClaim?.status === "VOID") {
            resultItem.skipped = true;
            resultItem.error = "Voided";
            results.push(resultItem);
            continue;
          }

          // Skip appointments whose claim was already submitted (has claimNumber or REVIEW/APPROVED).
          // The "Update & Resubmit" button resets the claim to PENDING so it is picked up again.
          const alreadySubmitted =
            activeClaim &&
            ((activeClaim.claimNumber != null && String(activeClaim.claimNumber).trim() !== "") ||
              activeClaim.status === "REVIEW" ||
              activeClaim.status === "APPROVED");

          if (alreadySubmitted) {
            resultItem.skipped = true;
            resultItem.error = "Already submitted";
            results.push(resultItem);
            continue;
          }

          // Build service lines: prefer draft claim's saved lines, fall back to AppointmentProcedure
          const serviceDate = formatDobForAgent(
            apt.date instanceof Date ? apt.date : new Date(apt.date as any)
          ) ?? date;

          let serviceLines: Array<{
            procedureCode: string;
            procedureDate: string | Date;
            toothNumber?: string;
            toothSurface?: string;
            totalBilled: number;
            totalAdjusted: number;
            totalPaid: number;
            totalDue: number;
          }>;

          // Fetch AppointmentProcedure records once (used for service lines fallback + npiProviderId)
          const apptProcedures = await storage.getByAppointmentId(Number(apt.id));

          const safeProcedureDate = new Date(serviceDate);

          if (activeClaim?.serviceLines?.length) {
            // Use saved service lines from the draft claim
            serviceLines = activeClaim.serviceLines
              .filter((sl) => sl.procedureCode)
              .map((sl) => ({
                procedureCode: sl.procedureCode,
                procedureDate: safeProcedureDate,
                toothNumber: sl.toothNumber ?? undefined,
                toothSurface: sl.toothSurface ?? undefined,
                totalBilled: Math.max(0, Number(sl.totalBilled ?? 0) || 0),
                totalAdjusted: 0,
                totalPaid: 0,
                totalDue: Math.max(0, Number(sl.totalBilled ?? 0) || 0),
              }));
          } else {
            // Fall back to AppointmentProcedure records (saved via Select Procedures)
            serviceLines = apptProcedures.map((proc) => ({
              procedureCode: proc.procedureCode,
              procedureDate: safeProcedureDate,
              toothNumber: proc.toothNumber ?? undefined,
              toothSurface: proc.toothSurface ?? undefined,
              totalBilled: Math.max(0, Number(proc.fee ?? 0) || 0),
              totalAdjusted: 0,
              totalPaid: 0,
              totalDue: Math.max(0, Number(proc.fee ?? 0) || 0),
            }));
          }

          if (!serviceLines.length) {
            resultItem.skipped = true;
            results.push(resultItem);
            continue;
          }

          // Patient
          const patient = apt.patientId ? await storage.getPatient(apt.patientId) : null;
          if (!patient) {
            resultItem.error = "Patient not found";
            results.push(resultItem);
            continue;
          }

          const memberId = String(patient.insuranceId ?? "").trim();
          if (!memberId) {
            resultItem.error = "Missing insurance ID";
            results.push(resultItem);
            continue;
          }

          const dobStr = formatDobForAgent(patient.dateOfBirth);
          if (!dobStr) {
            resultItem.error = "Missing or invalid date of birth";
            results.push(resultItem);
            continue;
          }

          // Always derive siteKey from the patient record — claim.insuranceProvider
          // may be stale (e.g. previously set to "MassHealth" by the old hardcoded path)
          const rawInsuranceProvider = (patient.insuranceProvider ?? "").trim();
          const siteKey = batchColumnDeriveSiteKey(rawInsuranceProvider);
          const canonicalInsuranceProvider = batchColumnCanonicalName(siteKey);

          const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, siteKey);
          if (!credentials) {
            resultItem.error = `No ${canonicalInsuranceProvider} credentials found — check Settings`;
            results.push(resultItem);
            continue;
          }

          const npiProviders = await storage.getNpiProvidersByUser(req.user.id);
          const npiProvider = npiProviders[0] ?? null;

          // procNpiProviderId = user's explicit choice saved via "Select Procedures" form
          // This ALWAYS wins over any previously stored claim npiProviderId
          const procNpiProviderId = (apptProcedures as any[]).find((p) => p.npiProviderId)?.npiProviderId ?? null;

          // Priority: Select Procedures choice > existing claim > first provider
          const claimNpiProviderId = procNpiProviderId ?? activeClaim?.npiProviderId ?? npiProvider?.id ?? null;

          console.log(`[batch-column] apt=${apt.id} siteKey=${siteKey} procNpiId=${procNpiProviderId} claimNpiId=${activeClaim?.npiProviderId} resolved=${claimNpiProviderId}`);

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

          // Reuse the existing draft claim, or create a new one
          let claimId: number;
          if (activeClaim?.id) {
            claimId = activeClaim.id;
            if (procNpiProviderId && activeClaim.npiProviderId !== procNpiProviderId) {
              await storage.updateClaim(claimId, { npiProviderId: procNpiProviderId });
            }
          } else {
            // Validate required integer fields before sending to Prisma
            const safeStaffId = apt.staffId ? Number(apt.staffId) : null;
            if (!safeStaffId || !Number.isFinite(safeStaffId)) {
              resultItem.error = "Appointment has no valid staff column (staffId missing)";
              results.push(resultItem);
              continue;
            }

            const safeNpiId = claimNpiProviderId && Number.isFinite(Number(claimNpiProviderId))
              ? Number(claimNpiProviderId)
              : null;

            console.log(`[batch-column] creating claim: patientId=${patient.id} aptId=${apt.id} staffId=${safeStaffId} npiProviderId=${safeNpiId} serviceDate=${serviceDate} dobStr=${dobStr} lines=${serviceLines.length} insurance=${canonicalInsuranceProvider}`);

            const newClaim = await storage.createClaim({
              patientId: Number(patient.id),
              appointmentId: Number(apt.id),
              userId: req.user.id,
              staffId: safeStaffId,
              patientName,
              memberId,
              dateOfBirth: new Date(dobStr),
              serviceDate: new Date(serviceDate),
              insuranceProvider: canonicalInsuranceProvider,
              remarks: "",
              missingTeethStatus: "No_missing",
              missingTeeth: {},
              status: "PENDING",
              serviceLines: { create: serviceLines },
              ...(safeNpiId ? { npiProviderId: safeNpiId } : {}),
            } as any);
            claimId = newClaim.id;
          }
          resultItem.claimId = claimId;

          // Create Payment only if one doesn't already exist (prevents duplicates on retry)
          const existingPayment = await storage.getPaymentsByClaimId(claimId);
          if (!existingPayment) {
            const totalBilled = serviceLines.reduce((sum, l) => sum + Number(l.totalBilled), 0);
            await storage.createPayment({
              claimId,
              patientId: Number(patient.id),
              userId: req.user.id,
              totalBilled: new Decimal(totalBilled),
              totalPaid: new Decimal(0),
              totalDue: new Decimal(totalBilled),
              status: "PENDING",
              notes: "",
            } as any);
          }

          // Resolve NPI provider object for selenium payload
          let resolvedNpiProvider = npiProvider;
          if (claimNpiProviderId) {
            const saved = npiProviders.find((p) => p.id === Number(claimNpiProviderId));
            if (saved) resolvedNpiProvider = saved;
          }

          // Collect attachments: appointment-level files + claim-level files
          const apptFiles = await storage.getAppointmentFiles(Number(apt.id));
          const claimFiles = (activeClaim as any)?.claimFiles ?? [];
          const allFileMeta = [
            ...apptFiles,
            ...claimFiles,
          ] as Array<{ filename: string; mimeType?: string | null; filePath?: string | null }>;

          const filesForQueue = allFileMeta.flatMap((f) => {
            if (!f.filePath) return [];
            const absPath = path.join(process.cwd(), f.filePath);
            if (!fs.existsSync(absPath)) {
              console.warn(`[batch-column] attachment not found on disk: ${absPath}`);
              return [];
            }
            const bufferBase64 = fs.readFileSync(absPath).toString("base64");
            return [{ originalname: f.filename, bufferBase64, mimetype: f.mimeType ?? "application/octet-stream" }];
          });

          // Base claim data shared across all insurance pathways
          const baseClaimPayload: any = {
            patientId: Number(patient.id),
            appointmentId: Number(apt.id),
            userId: req.user.id,
            staffId: Number(apt.staffId),
            patientName,
            memberId,
            dateOfBirth: dobStr,
            serviceDate,
            missingTeethStatus: activeClaim?.missingTeethStatus ?? "No_missing",
            missingTeeth: activeClaim?.missingTeeth ?? {},
            remarks: activeClaim?.remarks ?? "",
            serviceLines,
            claimId,
            ...(resolvedNpiProvider ? {
              npiProvider: {
                npiNumber: resolvedNpiProvider.npiNumber,
                providerName: resolvedNpiProvider.providerName,
              },
            } : {}),
          };

          // Enqueue to the correct insurance pathway
          let jobId: string;
          if (siteKey === "MH") {
            const enrichedPayload = {
              ...baseClaimPayload,
              insuranceProvider: "MassHealth",
              insuranceSiteKey: "MH",
              massdhpUsername: credentials.username,
              massdhpPassword: credentials.password,
            };
            const job = await seleniumQueue.add("claim-submit", {
              jobType: "claim-submit",
              userId: req.user.id,
              enrichedPayload,
              files: filesForQueue,
              claimId,
            });
            jobId = String(job.id);
          } else if (siteKey === "CCA") {
            const enrichedPayload = {
              claim: {
                ...baseClaimPayload,
                insuranceProvider: "CCA",
                insuranceSiteKey: "CCA",
                cca_username: credentials.username,
                cca_password: credentials.password,
              },
              files: filesForQueue,
            };
            jobId = enqueueSeleniumJob({
              jobType: "cca-claim-submit",
              userId: req.user.id,
              enrichedPayload,
              claimId,
            });
          } else if (siteKey === "DDMA") {
            const enrichedPayload = {
              claim: {
                ...baseClaimPayload,
                insuranceProvider: "Delta Dental MA",
                insuranceSiteKey: "DDMA",
                massddmaUsername: credentials.username,
                massddmaPassword: credentials.password,
                providerName: resolvedNpiProvider?.providerName ?? "",
              },
            };
            jobId = enqueueSeleniumJob({
              jobType: "ddma-claim-submit",
              userId: req.user.id,
              enrichedPayload,
              claimId,
            });
          } else if (siteKey === "TUFTS_SCO") {
            const enrichedPayload = {
              claim: {
                ...baseClaimPayload,
                insuranceProvider: "Tufts SCO",
                insuranceSiteKey: "TuftsSCO",
                dentaquestUsername: credentials.username,
                dentaquestPassword: credentials.password,
              },
            };
            jobId = enqueueSeleniumJob({
              jobType: "tuftssco-claim-submit",
              userId: req.user.id,
              enrichedPayload,
              claimId,
            });
          } else if (siteKey === "UNITED_SCO") {
            const enrichedPayload = {
              claim: {
                ...baseClaimPayload,
                insuranceProvider: "United/DentalHub",
                insuranceSiteKey: "UNITED_SCO",
                uniteddhUsername: credentials.username,
                uniteddhPassword: credentials.password,
              },
            };
            jobId = enqueueSeleniumJob({
              jobType: "uniteddh-claim-submit",
              userId: req.user.id,
              enrichedPayload,
              claimId,
            });
          } else {
            resultItem.error = `Unsupported insurance type: ${siteKey}`;
            results.push(resultItem);
            continue;
          }

          resultItem.processed = true;
          resultItem.jobId = jobId;

        } catch (aptErr: any) {
          console.error("[batch-column] apt error:", aptErr);
          resultItem.error = aptErr?.message ?? String(aptErr);
        }

        results.push(resultItem);
      }

      return res.json({ results });

    } catch (err: any) {
      console.error("[batch-column claims] error", err);
      return res.status(500).json({ error: err?.message ?? "Batch claim failed" });
    }
  }
);

// GET /api/claims/batch-pdf
// Query params: date=YYYY-MM-DD (required), staffIds=1,2 (required)
// Returns a ZIP archive of all INSURANCE_CLAIM PdfFile records for patients
// scheduled on that date in the given staff columns.
router.get(
  "/batch-pdf",
  async (req: Request, res: Response): Promise<any> => {
    const date = String(req.query.date ?? "").trim();
    const staffIdsRaw = String(req.query.staffIds ?? "").trim();

    if (!date) return res.status(400).json({ error: "Missing date query param" });
    if (!staffIdsRaw) return res.status(400).json({ error: "Missing staffIds query param" });
    if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });

    const staffIdFilter = new Set(
      staffIdsRaw.split(",").map(Number).filter((n) => Number.isFinite(n) && n > 0)
    );

    try {
      const allAppointments = await storage.getAppointmentsByDateForUser(date, req.user.id);
      const appointments = allAppointments.filter((a) => staffIdFilter.has(Number(a.staffId)));

      type PdfEntry = { filename: string; data: Buffer };
      const pdfEntries: PdfEntry[] = [];

      for (const apt of appointments) {
        const patientId = apt.patientId;
        if (!patientId) continue;

        // Claim PDFs from Selenium are stored in PdfGroup (titleKey=INSURANCE_CLAIM) → PdfFile (binary pdfData)
        const group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM");
        if (!group?.id) continue;

        const raw = await storage.getPdfFilesByGroupId(group.id);
        const files = (Array.isArray(raw) ? raw : (raw as any).data ?? []) as Array<{ filename: string; pdfData: unknown }>;

        for (const f of files) {
          if (!f.pdfData) continue;
          // Prisma Bytes → always convert to Buffer to satisfy archiver
          const buf = Buffer.isBuffer(f.pdfData)
            ? f.pdfData
            : Buffer.from(f.pdfData as any);
          if (!buf.length) continue;
          // Sanitize filename: strip path components, keep safe chars
          const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf";
          pdfEntries.push({ filename: safeName, data: buf });
        }
      }

      if (pdfEntries.length === 0) {
        return res.status(404).json({ error: "No claim PDFs found for the selected columns and date" });
      }

      const zipFilename = `claims_${date}.zip`;
      res.setHeader("Content-Type", "application/zip");
      res.setHeader("Content-Disposition", `attachment; filename="${zipFilename}"`);

      const archive = archiver("zip", { zlib: { level: 6 } });

      // Capture archiver errors before any data is written
      let archiveError: Error | null = null;
      archive.on("error", (err) => {
        archiveError = err;
        console.error("[batch-pdf] archiver error:", err);
        if (!res.headersSent) {
          res.status(500).json({ error: `ZIP creation failed: ${err.message}` });
        }
      });

      archive.pipe(res);

      const seenNames = new Map<string, number>();
      for (const entry of pdfEntries) {
        if (archiveError) break;
        const count = seenNames.get(entry.filename) ?? 0;
        seenNames.set(entry.filename, count + 1);
        const archiveName =
          count === 0
            ? entry.filename
            : `${path.basename(entry.filename, ".pdf")}_${count}.pdf`;
        archive.append(entry.data, { name: archiveName });
      }

      if (!archiveError) {
        await archive.finalize();
      }
    } catch (err: any) {
      console.error("[batch-pdf] error:", err);
      if (!res.headersSent) {
        return res.status(500).json({ error: err?.message ?? "Batch PDF download failed" });
      }
    }
  }
);

// GET /api/claims/by-appointment/:appointmentId
// Returns the most recent active (non-cancelled/void) claim with service lines and files
router.get(
  "/by-appointment/:appointmentId",
  async (req: Request, res: Response): Promise<any> => {
    const aptId = parseInt(req.params.appointmentId, 10);
    if (isNaN(aptId)) return res.status(400).json({ error: "Invalid appointmentId" });

    const claim = await storage.getActiveClaimByAppointmentId(aptId);
    if (!claim) return res.status(404).json({ message: "No active claim found" });
    return res.json(claim);
  }
);

// GET /api/claims/recent
router.get("/recent", async (req: Request, res: Response) => {
  try {
    const limit = parseInt(req.query.limit as string) || 10;
    const offset = parseInt(req.query.offset as string) || 0;

    const [claims, totalCount] = await Promise.all([
      storage.getRecentClaims(limit, offset),
      storage.getTotalClaimCount(),
    ]);

    res.json({ claims, totalCount });
  } catch (error) {
    console.error("Failed to retrieve recent claims:", error);
    res.status(500).json({ message: "Failed to retrieve recent claims" });
  }
});

// GET /api/claims/patient/:patientId
router.get(
  "/patient/:patientId",
  async (req: Request, res: Response): Promise<any> => {
    try {
      const patientIdParam = Array.isArray(req.params.patientId) ? req.params.patientId[0] : req.params.patientId;
      if (!patientIdParam) {
        return res.status(400).json({ message: "Missing patientId" });
      }
      const patientId = parseInt(patientIdParam);
      if (isNaN(patientId)) {
        return res.status(400).json({ message: "Invalid patientId" });
      }
      const limit = parseInt(req.query.limit as string) || 10;
      const offset = parseInt(req.query.offset as string) || 0;

      if (isNaN(patientId)) {
        return res.status(400).json({ message: "Invalid patient ID" });
      }

      const [claims, totalCount] = await Promise.all([
        storage.getRecentClaimsByPatientId(patientId, limit, offset),
        storage.getTotalClaimCountByPatient(patientId),
      ]);

      res.json({ claims, totalCount });
    } catch (error) {
      console.error("Failed to retrieve claims for patient:", error);
      res.status(500).json({ message: "Failed to retrieve patient claims" });
    }
  }
);

// Get all claims count.
router.get("/all", async (req: Request, res: Response) => {
  try {
    const claims = await storage.getTotalClaimCount();
    res.json(claims);
  } catch (error) {
    res.status(500).json({ message: "Failed to retrieve claims count" });
  }
});

// Get a single claim by ID
router.get("/:id", async (req: Request, res: Response): Promise<any> => {
  try {
    const idParam = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
    if (!idParam) {
      return res.status(400).json({ error: "Missing claim ID" });
    }
    const claimId = parseInt(idParam, 10);
    if (isNaN(claimId)) {
      return res.status(400).json({ error: "Invalid claim ID" });
    }

    const claim = await storage.getClaim(claimId);
    if (!claim) {
      return res.status(404).json({ message: "Claim not found" });
    }

    res.json(claim);
  } catch (error) {
    res.status(500).json({ message: "Failed to retrieve claim" });
  }
});

// Links AppointmentFile records to a claim as ClaimFile records (dedup by filename).
// Called after claim create or update so files saved in the procedures form always appear.
async function autoLinkAppointmentFiles(claimId: number, appointmentId: number | null | undefined) {
  if (!appointmentId) return;
  const { prisma: db } = await import("@repo/db/client");
  const aptFiles = await db.appointmentFile.findMany({ where: { appointmentId } });
  if (!aptFiles.length) return;
  const existing = await db.claimFile.findMany({ where: { claimId }, select: { filename: true } });
  const existingNames = new Set(existing.map((f: any) => f.filename));
  const toLink = aptFiles.filter((f: any) => f.filename && !existingNames.has(f.filename));
  if (!toLink.length) return;
  await db.claimFile.createMany({
    data: toLink.map((f: any) => ({
      claimId,
      filename: f.filename,
      mimeType: f.mimeType ?? "",
      ...(f.filePath ? { filePath: String(f.filePath) } : {}),
    })),
  });
}

// Create a new claim
router.post("/", async (req: Request, res: Response): Promise<any> => {
  try {
    // --- TRANSFORM claimFiles (if provided) into Prisma nested-create shape
    if (Array.isArray(req.body.claimFiles)) {
      // each item expected: { filename: string, mimeType: string }
      req.body.claimFiles = {
        create: req.body.claimFiles.map((f: any) => ({
          filename: String(f.filename),
          mimeType: String(f.mimeType || f.mime || ""),
          ...(f.filePath ? { filePath: String(f.filePath) } : {}),
        })),
      };
    }

    // --- TRANSFORM serviceLines
    if (
      !Array.isArray(req.body.serviceLines) ||
      req.body.serviceLines.length === 0
    ) {
      return res.status(400).json({
        message: "At least one service line is required to create a claim",
      });
    }

    if (Array.isArray(req.body.serviceLines)) {
      req.body.serviceLines = req.body.serviceLines.map(
        (line: InputServiceLine) => ({
          procedureCode: String(line.procedureCode ?? ""),
          procedureDate: line.procedureDate,
          quad: line.quad ?? null,
          arch: line.arch ?? null,
          toothNumber: line.toothNumber ?? null,
          toothSurface: line.toothSurface ?? null,
          totalBilled: Number(line.totalBilled),
          totalAdjusted: 0,
          totalPaid: 0,
          totalDue: Number(line.totalBilled),
        })
      );
      req.body.serviceLines = { create: req.body.serviceLines };
    }

    // Strip unknown keys so strict() doesn't reject any extra properties the frontend may send
    const parsedClaim = (ExtendedClaimSchema as unknown as z.ZodObject<any>).strip().parse({
      patientId: req.body.patientId,
      appointmentId: req.body.appointmentId ?? null,
      staffId: req.body.staffId,
      patientName: req.body.patientName,
      memberId: req.body.memberId,
      dateOfBirth: req.body.dateOfBirth,
      remarks: req.body.remarks ?? "",
      missingTeethStatus: req.body.missingTeethStatus,
      missingTeeth: req.body.missingTeeth,
      serviceDate: req.body.serviceDate,
      insuranceProvider: req.body.insuranceProvider,
      status: req.body.status,
      serviceLines: req.body.serviceLines,
      claimFiles: req.body.claimFiles,
      ...(req.body.npiProviderId ? { npiProviderId: Number(req.body.npiProviderId) } : {}),
      userId: req.user!.id,
    });

    // Step 1: Calculate total billed from service lines
    const serviceLinesCreateInput = (
      parsedClaim.serviceLines as Prisma.ServiceLineCreateNestedManyWithoutClaimInput
    )?.create;
    const lines = Array.isArray(serviceLinesCreateInput)
      ? (serviceLinesCreateInput as unknown as {
          totalBilled: number | string;
        }[])
      : [];
    const totalBilled = lines.reduce(
      (sum, line) => sum + Number(line.totalBilled ?? 0),
      0
    );

    // Step 2: Create claim (with service lines)
    const claim = await storage.createClaim(parsedClaim);

    // Step 2b: Link any AppointmentFile records saved via procedures form
    await autoLinkAppointmentFiles(claim.id, claim.appointmentId);

    // Step 2c: Sync appointment type from final CDT codes
    const rawCodes = lines.map((l: any) => String(l.procedureCode ?? "")).filter(Boolean);
    await syncAppointmentType(claim.appointmentId, rawCodes);

    // Step 3: Create payment only for real submissions (not draft saves)
    const isDraft = req.query.draft === "true";
    if (!isDraft) {
      await storage.createPayment({
        claimId: claim.id,
        patientId: claim.patientId,
        userId: req.user!.id,
        totalBilled: new Decimal(totalBilled),
        totalPaid: new Decimal(0),
        totalDue: new Decimal(totalBilled),
        status: "PENDING",
        notes: "",
      });
    }

    res.status(201).json(claim);
  } catch (error) {
    if (error instanceof z.ZodError) {
      const firstIssue = error.issues[0];
      const fieldPath = firstIssue?.path?.join(".") ?? "unknown";
      const fieldMsg = firstIssue?.message ?? "invalid";
      console.error("❌ Claim validation error:", error.issues);
      return res.status(400).json({
        message: `Validation error on field "${fieldPath}": ${fieldMsg}`,
        errors: error.format(),
      });
    }

    console.error("❌ Failed to create claim:", error);

    // Send more detailed info to the client (for dev only)
    return res.status(500).json({
      message: "Failed to create claim",
      error: error instanceof Error ? error.message : String(error),
    });
  }
});

// Update a claim
router.put("/:id", async (req: Request, res: Response): Promise<any> => {
  try {
    const idParam = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
    if (!idParam) {
      return res.status(400).json({ error: "Missing claim ID" });
    }

    const claimId = parseInt(idParam, 10);
    if (isNaN(claimId)) {
      return res.status(400).json({ error: "Invalid claim ID" });
    }

    const existingClaim = await storage.getClaim(claimId);
    if (!existingClaim) {
      return res.status(404).json({ message: "Claim not found" });
    }

    // If service lines are provided, replace them (delete old, create new)
    if (Array.isArray(req.body.serviceLines) && req.body.serviceLines.length > 0) {
      const { prisma: db } = await import("@repo/db/client");
      await db.serviceLine.deleteMany({ where: { claimId } });
      await db.serviceLine.createMany({
        data: req.body.serviceLines.map((line: InputServiceLine) => ({
          claimId,
          procedureCode: String(line.procedureCode ?? ""),
          procedureDate: new Date(line.procedureDate),
          quad: line.quad || null,
          arch: line.arch || null,
          toothNumber: line.toothNumber || null,
          toothSurface: line.toothSurface || null,
          totalBilled: Number(line.totalBilled),
          totalAdjusted: 0,
          totalPaid: 0,
          totalDue: Number(line.totalBilled),
        })),
      });

      // Sync appointment type from updated CDT codes
      const updatedCodes = req.body.serviceLines
        .map((l: InputServiceLine) => String(l.procedureCode ?? ""))
        .filter(Boolean);
      await syncAppointmentType(existingClaim.appointmentId, updatedCodes);
    }

    // If new claimFiles are provided, append them (don't delete existing ones)
    if (Array.isArray(req.body.claimFiles) && req.body.claimFiles.length > 0) {
      const { prisma: db } = await import("@repo/db/client");
      await db.claimFile.createMany({
        data: req.body.claimFiles.map((f: any) => ({
          claimId,
          filename: String(f.filename || ""),
          mimeType: String(f.mimeType || f.mime || ""),
          ...(f.filePath ? { filePath: String(f.filePath) } : {}),
        })),
      });
    }

    // Explicitly pick only scalar fields — skip serviceLines and claimFiles (handled above)
    // Use req.user!.id for userId (always trust the authenticated session, not the client body)
    // Skip null/empty-string values for optional fields to avoid coerce.date() failures
    const toOptionalNum = (v: any) => (v != null && !Number.isNaN(Number(v)) ? Number(v) : undefined);
    const toOptionalDate = (v: any) => (v != null && String(v).trim() !== "" ? v : undefined);

    const claimData = (updateClaimSchema as unknown as z.ZodObject<any>).strip().parse({
      patientId: toOptionalNum(req.body.patientId),
      appointmentId: toOptionalNum(req.body.appointmentId),
      userId: req.user!.id,
      staffId: toOptionalNum(req.body.staffId),
      patientName: req.body.patientName,
      memberId: req.body.memberId,
      dateOfBirth: toOptionalDate(req.body.dateOfBirth),
      remarks: req.body.remarks ?? "",
      missingTeethStatus: req.body.missingTeethStatus,
      missingTeeth: req.body.missingTeeth,
      serviceDate: toOptionalDate(req.body.serviceDate),
      insuranceProvider: req.body.insuranceProvider,
      status: req.body.status,
      ...(req.body.claimNumber != null ? { claimNumber: req.body.claimNumber } : {}),
      ...(req.body.npiProviderId ? { npiProviderId: Number(req.body.npiProviderId) } : {}),
    });
    const updatedClaim = await storage.updateClaim(claimId, claimData);

    // Link any AppointmentFile records saved via procedures form (dedup by filename)
    await autoLinkAppointmentFiles(claimId, updatedClaim.appointmentId);

    // Propagate provider change to the linked payment so both stay in sync
    if (req.body.npiProviderId) {
      const { prisma: db } = await import("@repo/db/client");
      await db.payment.updateMany({
        where: { claimId },
        data: { npiProviderId: Number(req.body.npiProviderId) },
      });
    }

    res.json(updatedClaim);
  } catch (error) {
    if (error instanceof z.ZodError) {
      const firstIssue = error.issues[0];
      const fieldPath = firstIssue?.path?.join(".") ?? "unknown";
      const fieldMsg = firstIssue?.message ?? "invalid";
      console.error("❌ Claim update validation error:", error.issues);
      return res.status(400).json({
        message: `Validation error on field "${fieldPath}": ${fieldMsg}`,
        errors: error.format(),
      });
    }
    console.error("❌ Failed to update claim:", error);
    res.status(500).json({ message: "Failed to update claim" });
  }
});

// Delete a claim
router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
  try {
    const idParam = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
    if (!idParam) {
      return res.status(400).json({ error: "Missing claim ID" });
    }

    const claimId = parseInt(idParam, 10);
    if (isNaN(claimId)) {
      return res.status(400).json({ error: "Invalid claim ID" });
    }

    const existingClaim = await storage.getClaim(claimId);
    if (!existingClaim) {
      return res.status(404).json({ message: "Claim not found" });
    }

    if (existingClaim.userId !== req.user!.id) {
      return res.status(403).json({
        message:
          "Forbidden: Claim belongs to a different user, you can't delete this.",
      });
    }

    await storage.deleteClaim(claimId);
    res.status(204).send();
  } catch (error) {
    res.status(500).json({ message: "Failed to delete claim" });
  }
});

// POST /api/claims/void-for-appointment
// Marks the claim for an appointment as VOID so batch-column skips it permanently.
// If no claim exists yet, creates a minimal placeholder VOID claim.
router.post("/void-for-appointment", async (req: Request, res: Response): Promise<any> => {
  try {
    if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
    const { appointmentId } = req.body;
    if (!appointmentId || isNaN(Number(appointmentId))) {
      return res.status(400).json({ error: "Invalid appointmentId" });
    }

    const existing = await storage.getActiveClaimByAppointmentId(Number(appointmentId));
    if (existing) {
      await storage.updateClaim(Number(existing.id), { status: "VOID" } as any);
      return res.json({ voided: true, claimId: existing.id });
    }

    // No claim yet — look up appointment + patient to create a minimal VOID placeholder
    const apt = await storage.getAppointment(Number(appointmentId));
    if (!apt) return res.status(404).json({ error: "Appointment not found" });
    const patient = apt.patientId ? await storage.getPatient(apt.patientId) : null;
    if (!patient) return res.status(404).json({ error: "Patient not found" });

    const newClaim = await storage.createClaim({
      patientId: Number(patient.id),
      appointmentId: Number(appointmentId),
      userId: req.user.id,
      staffId: Number(apt.staffId),
      patientName: `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(),
      memberId: String(patient.insuranceId ?? ""),
      dateOfBirth: patient.dateOfBirth ? new Date(patient.dateOfBirth) : new Date(),
      serviceDate: apt.date instanceof Date ? apt.date : new Date(apt.date as any),
      insuranceProvider: "MassHealth",
      remarks: "",
      missingTeethStatus: "No_missing",
      missingTeeth: {},
      status: "VOID",
    } as any);
    return res.json({ voided: true, claimId: newClaim.id });
  } catch (err: any) {
    console.error("void-for-appointment error", err);
    return res.status(500).json({ error: err.message ?? "Server error" });
  }
});

// POST /api/claims/reset-for-resubmit
// Resets the active claim for an appointment back to PENDING with no claimNumber,
// so the batch-column will pick it up again on the next run.
router.post("/reset-for-resubmit", async (req: Request, res: Response): Promise<any> => {
  try {
    const { appointmentId } = req.body;
    if (!appointmentId || isNaN(Number(appointmentId))) {
      return res.status(400).json({ error: "Invalid appointmentId" });
    }

    const claim = await storage.getActiveClaimByAppointmentId(Number(appointmentId));
    if (!claim) {
      return res.json({ reset: false, message: "No existing claim found — will be created fresh on next submit" });
    }

    await storage.updateClaim(Number(claim.id), { status: "PENDING", claimNumber: null } as any);
    return res.json({ reset: true, claimId: claim.id });
  } catch (err: any) {
    console.error("reset-for-resubmit error", err);
    return res.status(500).json({ error: err.message ?? "Server error" });
  }
});

export default router;
