import { useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import {
  AlertTriangle,
  ArrowRight,
  ChevronRight,
  CloudUpload,
  Download,
  FileText,
  History as HistoryIcon,
  Inbox,
  Loader2,
  Upload as UploadIcon,
  X,
  XCircle,
} from "lucide-react";
import { toast } from "sonner";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tabs } from "@/components/ui/tabs";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { PageHeader } from "@/components/PageHeader";
import { ClaimCard837 } from "@/components/ClaimCard837";
import { ExportBar } from "@/components/ExportBar";
import { StatPill, ValidationDot } from "@/components/ClaimCard/shared";
import { api, ApiError, type BatchSummary, type ParseProgress } from "@/lib/api";
import { downloadBlob } from "@/lib/download";
import { fmt, toNum } from "@/lib/format";
import { useAppStore } from "@/store";
import { useParse } from "@/hooks/useParse";
import { useBatchExport } from "@/hooks/useBatchExport";
import { useBatches } from "@/hooks/useBatches";
import type {
  ClaimOutput,
  ClaimPayment,
  ParsedBatch,
  ParsedBatchKind,
  ServicePayment,
} from "@/types";
import { cn } from "@/lib/utils";
import { RoleGate } from "@/auth/RoleGate";

// ---------------------------------------------------------------------------
// Streaming state — every claim that arrives from the backend accumulates
// in this list. The 837P and 835 cases are kept as separate slots so the UI
// can render the right "card" type for each.
//
// NOTE: ClaimCard837 and ClaimCard835 use the --surface (warm paper) family
// on purpose — they read as "ingestion documents" within an otherwise dark
// instrument. The dark page chrome around them is what gives the section
// its place in the rest of the app.
// ---------------------------------------------------------------------------

type StreamedClaim837 = { kind: "837p"; data: ClaimOutput };
type StreamedClaim835 = { kind: "835"; data: ClaimPayment };
type StreamedClaim = StreamedClaim837 | StreamedClaim835;

interface StreamState {
  items: StreamedClaim[];
  expectedTotal: number | null;
  passed: number;
  failed: number;
}

const PAYERS_837 = [
  { value: "co_medicaid", label: "CO Medicaid" },
  { value: "generic_837p", label: "Generic 837P" },
];

const PAYERS_835 = [
  { value: "co_medicaid_835", label: "CO Medicaid" },
  { value: "generic_835", label: "Generic 835" },
];

function formatBytes(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

export function ClaimCard835({ claim }: { claim: ClaimPayment }) {
  const [open, setOpen] = useState(false);
  const passed = claim.service_payments.length > 0;
  // SP21 Phase 5 Task 5.7: same drill gate as ClaimCard837. Only show
  // "See claim in detail" once the claim_id is in the persisted
  // batches (otherwise ClaimDrawer would 404).
  const parsedBatches = useAppStore((s) => s.parsedBatches);
  const persistedClaimIds = useMemo(
    () => new Set(parsedBatches.flatMap((b) => b.claimIds)),
    [parsedBatches],
  );
  const canDrill = persistedClaimIds.has(claim.payer_claim_control_number);
  const navigate = useNavigate();
  return (
    <div
      className="rounded-lg overflow-hidden border"
      style={{
        backgroundColor: "hsl(36 22% 96%)",
        borderColor: "hsl(30 14% 14% / 0.10)",
        boxShadow: "inset 0 1px 0 0 hsl(0 0% 100% / 0.5)",
      }}
    >
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        className="w-full text-left px-4 py-3 hover:bg-[hsl(36_22%_92%)] transition-colors"
        aria-expanded={open}
      >
        <div className="flex items-center gap-3">
          <ChevronRight
            className={cn(
              "h-3.5 w-3.5 transition-transform shrink-0",
              open && "rotate-90"
            )}
            strokeWidth={1.75}
            style={{ color: "hsl(var(--surface-ink-3))" }}
          />
          <div className="flex-1 min-w-0">
            <div className="flex items-center gap-2.5 flex-wrap">
              <span
                className="display mono text-[12.5px]"
                style={{ color: "hsl(var(--surface-ink))" }}
              >
                {claim.payer_claim_control_number}
              </span>
              <span
                className="text-[12px]"
                style={{ color: "hsl(var(--surface-ink-2))" }}
              >
                {claim.status_label ?? `Status ${claim.status_code}`}
              </span>
            </div>
            <div
              className="mono text-[10.5px] flex items-center gap-2 mt-1"
              style={{ color: "hsl(var(--surface-ink-3))" }}
            >
              <span>CLP {claim.status_code}</span>
              {claim.original_claim_id ? (
                <>
                  <span className="opacity-40">·</span>
                  <span>Orig {claim.original_claim_id}</span>
                </>
              ) : null}
              <span className="opacity-40">·</span>
              <span>{claim.service_payments.length} svc</span>
            </div>
          </div>
          <div className="text-right shrink-0">
            <div
              className="display mono text-[14px] text-[hsl(var(--success))]"
            >
              {fmt.usdDecimal(claim.total_paid)}
            </div>
            <div
              className="mono text-[10.5px]"
              style={{ color: "hsl(var(--surface-ink-3))" }}
            >
              of {fmt.usdDecimal(claim.total_charge)}
            </div>
            <div className="mt-0.5">
              <ValidationDot passed={passed} />
            </div>
          </div>
        </div>
      </button>
      {open ? (
        <div
          className="border-t px-4 py-3 grid gap-4 animate-fade-in"
          style={{
            borderColor: "hsl(30 14% 14% / 0.08)",
            backgroundColor: "hsl(36 22% 92%)",
          }}
        >
          <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
            <StatPill label="Filing" value={claim.claim_filing_indicator ?? "—"} />
            <StatPill label="Facility" value={claim.facility_type ?? "—"} />
            <StatPill label="Frequency" value={claim.frequency_code ?? "—"} />
            <StatPill
              label="Patient resp"
              value={
                claim.patient_responsibility
                  ? fmt.usdDecimal(claim.patient_responsibility)
                  : "—"
              }
            />
          </div>

          {claim.service_payments.length > 0 ? (
            <div>
              <div
                className="mono text-[10px] uppercase tracking-[0.18em] font-semibold mb-1.5"
                style={{ color: "hsl(var(--surface-ink-3))" }}
              >
                Service payments
              </div>
              <div
                className="rounded-md border overflow-x-auto"
                style={{
                  borderColor: "hsl(30 14% 14% / 0.10)",
                  backgroundColor: "hsl(36 22% 98%)",
                }}
              >
                <table className="w-full text-[12px]">
                  <thead style={{ backgroundColor: "hsl(36 22% 90%)" }}>
                    <tr className="text-left" style={{ color: "hsl(var(--surface-ink-2))" }}>
                      <th className="px-2.5 py-1.5 font-medium">#</th>
                      <th className="px-2.5 py-1.5 font-medium">Code</th>
                      <th className="px-2.5 py-1.5 font-medium">Mods</th>
                      <th className="px-2.5 py-1.5 font-medium">Date</th>
                      <th className="px-2.5 py-1.5 font-medium">Units</th>
                      <th className="px-2.5 py-1.5 font-medium text-right">Charge</th>
                      <th className="px-2.5 py-1.5 font-medium text-right">Paid</th>
                    </tr>
                  </thead>
                  <tbody>
                    {claim.service_payments.map((svc) => (
                      <ServicePaymentRow key={svc.line_number} svc={svc} />
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
          ) : null}

          {/* SP21 Phase 5 Task 5.7: drill to the persisted remit. The
              835 cards have no separate "claim" (they're the payment
              side), so the link goes to /remittances?remit=PCN which
              opens the RemitDrawer for this payment. Same persisted-
              gate: only show when the PCN is in a saved batch. */}
          {canDrill ? (
            <div className="pt-1">
              <button
                type="button"
                onClick={() =>
                  navigate(
                    `/remittances?remit=${encodeURIComponent(claim.payer_claim_control_number)}`,
                  )
                }
                data-testid="upload-remit-drill"
                aria-label={`See remittance ${claim.payer_claim_control_number} in detail`}
                className="inline-flex items-center gap-1.5 text-[12px] mono font-semibold cursor-pointer rounded-sm px-1 -mx-1 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
                style={{ color: "hsl(var(--surface-ink))" }}
              >
                See claim in detail
                <ArrowRight className="h-3 w-3" strokeWidth={2} />
              </button>
            </div>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

function ServicePaymentRow({ svc }: { svc: ServicePayment }) {
  return (
    <tr
      className="border-t"
      style={{ borderColor: "hsl(30 14% 14% / 0.08)" }}
    >
      <td
        className="px-2.5 py-1.5 mono"
        style={{ color: "hsl(var(--surface-ink))" }}
      >
        {svc.line_number}
      </td>
      <td
        className="px-2.5 py-1.5 mono"
        style={{ color: "hsl(var(--surface-ink))" }}
      >
        {svc.procedure_qualifier}·{svc.procedure_code}
      </td>
      <td
        className="px-2.5 py-1.5 mono"
        style={{ color: "hsl(var(--surface-ink-3))" }}
      >
        {svc.modifiers.length > 0 ? svc.modifiers.join(", ") : "—"}
      </td>
      <td
        className="px-2.5 py-1.5 mono"
        style={{ color: "hsl(var(--surface-ink-3))" }}
      >
        {svc.service_date ?? "—"}
      </td>
      <td
        className="px-2.5 py-1.5 mono"
        style={{ color: "hsl(var(--surface-ink-3))" }}
      >
        {svc.units ? `${toNum(svc.units)} ${svc.unit_type ?? ""}`.trim() : "—"}
      </td>
      <td
        className="px-2.5 py-1.5 mono text-right display"
        style={{ color: "hsl(var(--surface-ink))" }}
      >
        {fmt.usdDecimal(svc.charge)}
      </td>
      <td
        className="px-2.5 py-1.5 mono text-right display text-[hsl(var(--success))]"
      >
        {fmt.usdDecimal(svc.payment)}
      </td>
    </tr>
  );
}

// ---------------------------------------------------------------------------
// Hero stat cell — compact KPI-style cell for the page's hero strip.
// Echoes the dashboard's KPI rhythm in a tighter, single-row form.
// ---------------------------------------------------------------------------

function HeroStat({
  label,
  value,
  tone = "default",
}: {
  label: string;
  value: React.ReactNode;
  tone?: "default" | "accent" | "success";
}) {
  const toneClass =
    tone === "accent"
      ? "text-accent"
      : tone === "success"
        ? "text-[hsl(var(--success))]"
        : "text-foreground";
  return (
    <div className="flex-1 min-w-0">
      <div className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
        {label}
      </div>
      <div
        className={cn(
          "display mono text-[22px] leading-[1.05] mt-1.5 tracking-tight",
          toneClass
        )}
      >
        {value}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------

export function Upload() {
  const inputRef = useRef<HTMLInputElement>(null);
  const [searchParams, setSearchParams] = useSearchParams();
  // Tab URL state — `?tab=history` deep-links the History tab;
  // `?tab=upload` (or missing) is the default. Stored in the URL so
  // the back button round-trips between the two surfaces.
  const tab = searchParams.get("tab") === "history" ? "history" : "upload";
  const setTab = (next: "upload" | "history") => {
    setSearchParams(
      (prev) => {
        if (next === "upload") prev.delete("tab");
        else prev.set("tab", next);
        return prev;
      },
      { replace: true },
    );
  };
  const [file, setFile] = useState<File | null>(null);
  const [kind, setKind] = useState<ParsedBatchKind>("837p");
  const [payer, setPayer] = useState<string>(PAYERS_837[0]!.value);
  const [stream, setStream] = useState<StreamState>({
    items: [],
    expectedTotal: null,
    passed: 0,
    failed: 0,
  });
  const [dragging, setDragging] = useState(false);
  const addParsedBatch = useAppStore((s) => s.addParsedBatch);
  const parsedBatches = useAppStore((s) => s.parsedBatches);
  const parseMutation = useParse(kind);

  // Persisted batch count for the History tab badge. Disabled when
  // there's no backend configured (sample-data mode); in that case we
  // fall back to the in-memory parsedBatches so the badge still reads.
  const batchesQuery = useBatches();
  const persistedBatchCount = batchesQuery.data?.length ?? 0;
  const historyCount =
    persistedBatchCount > 0 ? persistedBatchCount : parsedBatches.length;

  // Batch-export wiring (SP9 — Upload → ZIP flow). The hook owns the
  // selection set, the exporting flag, the export handler, and the
  // captured server-side batch id. See `src/hooks/useBatchExport.ts`.
  const {
    selectedClaimIds,
    exporting,
    total837InStream,
    onToggleSelect,
    onToggleAll,
    onExport,
    recordBatchId,
  } = useBatchExport(stream.items);

  const payerOptions = kind === "837p" ? PAYERS_837 : PAYERS_835;

  const expectedTotal = stream.expectedTotal;
  const totalSoFar = stream.items.length;
  const running = parseMutation.isPending;
  const progressPct = useMemo(() => {
    if (!expectedTotal || expectedTotal <= 0) return 0;
    return Math.min(100, Math.round((totalSoFar / expectedTotal) * 100));
  }, [expectedTotal, totalSoFar]);

  // Hero stat derivations — anchors the page in the same rhythm as the
  // Dashboard's KPI row. Computed from the parsedBatches store.
  const heroStats = useMemo(() => {
    const totalClaims = parsedBatches.reduce((s, b) => s + b.claimCount, 0);
    const totalPassed = parsedBatches.reduce((s, b) => s + b.passed, 0);
    const passRate = totalClaims > 0 ? (totalPassed / totalClaims) * 100 : 0;
    const last = parsedBatches[0];
    return {
      batches: parsedBatches.length,
      totalClaims,
      passRate,
      lastAt: last ? fmt.dateShort(last.parsedAt) : "—",
    };
  }, [parsedBatches]);

  // Auto-select newly streamed 837P claims. Keyed on stream.items.length
  // so this only fires when the count changes (i.e. a new claim arrived
  // or the stream was reset). 835 streams don't carry the
  // claim-id-into-export flow (out of scope for SP9), so their items
  // are deliberately ignored.
  // (Selection + export logic now lives in useBatchExport — see the
  // hook call above. Keeping the comment as a breadcrumb for future
  // readers who grep "Auto-select".)

  function pickFile(f: File | null) {
    setFile(f);
    setStream({ items: [], expectedTotal: null, passed: 0, failed: 0 });
  }

  async function onParse() {
    if (!file) return;
    if (!api.isConfigured) {
      toast.error(
        "Backend not configured. Set VITE_API_BASE_URL in .env.local to use the Upload page."
      );
      return;
    }
    setStream({ items: [], expectedTotal: null, passed: 0, failed: 0 });
    const items: StreamedClaim[] = [];
    const onProgress: ParseProgress = (event) => {
      if (event.type === "claim") {
        items.push({ kind: "837p", data: event.data });
        setStream((s) => ({
          ...s,
          items: [...items],
        }));
      } else if (event.type === "claim_payment") {
        items.push({ kind: "835", data: event.data });
        setStream((s) => ({
          ...s,
          items: [...items],
        }));
      } else if (event.type === "summary") {
        setStream((s) => ({
          ...s,
          expectedTotal: event.data.total_claims,
          passed: event.data.passed,
          failed: event.data.failed,
        }));
      }
    };

    try {
      const summary = await parseMutation.mutateAsync({
        file,
        options: { payer, onProgress },
      });

      const finalSummary =
        summary && typeof summary === "object" && "total_claims" in summary
          ? summary
          : null;

      if (finalSummary) {
        const id = `BATCH-${Date.now()}`;
        const claimIds = items
          .map((c) =>
            c.kind === "837p" ? c.data.claim_id : c.data.payer_claim_control_number
          )
          .filter(Boolean);
        const batch: ParsedBatch = {
          id,
          kind,
          inputFilename: file.name,
          parsedAt: new Date().toISOString(),
          claimCount: finalSummary.total_claims,
          passed: finalSummary.passed,
          failed: finalSummary.failed,
          claimIds,
          summary: finalSummary,
        };
        addParsedBatch(batch);
        // Capture the server-side batch id returned by the parser so
        // onExport can call /api/batches/{server_id}/export-837. The
        // client-side `id` (BATCH-{timestamp}) is the in-memory key;
        // the server uses a UUID stored in the DB.
        recordBatchId(finalSummary.batch_id ?? null);
        toast.success(
          `Parsed ${finalSummary.total_claims} ${kind === "837p" ? "claims" : "payments"} · ${
            finalSummary.passed
          } passed · ${finalSummary.failed} failed`,
          { description: file.name }
        );
      }
    } catch (err) {
      toast.error(
        err instanceof Error ? err.message : "Failed to parse file"
      );
    }
  }

  function onDrop(e: React.DragEvent<HTMLDivElement>) {
    e.preventDefault();
    setDragging(false);
    const f = e.dataTransfer.files?.[0];
    if (f) pickFile(f);
  }

  // -----------------------------------------------------------------
  // Stagger choreography — hero lands first, drop zone at 140ms,
  // stream at 240ms, batches at 320ms. Total < 500ms so it feels
  // like the instrument powering up, not a long intro.
  // -----------------------------------------------------------------
  const heroDelay = 0;
  const dropDelay = 140;
  const streamDelay = 240;
  const batchesDelay = 320;

  return (
    <div className="space-y-6 lg:space-y-10">
      {/* =================================================================
          HERO — same PageHeader pattern as Dashboard, with a subtle
          ghost editorial watermark behind it (echoes the Dashboard's
          ghost USD watermark, but uses claims count instead so the
          Upload page's "scale" reads as "things ingested").
          ================================================================= */}
      <section
        className="relative animate-fade-in overflow-hidden"
        style={{ animationDelay: `${heroDelay}ms` }}
      >
        <div
          aria-hidden="true"
          className="pointer-events-none select-none absolute -right-4 top-1/2 -translate-y-1/2 whitespace-nowrap display text-foreground"
          style={{
            fontSize: "clamp(72px, 12vw, 140px)",
            letterSpacing: "-0.04em",
            opacity: 0.045,
            lineHeight: 1,
          }}
        >
          {heroStats.totalClaims.toLocaleString()}
        </div>

        <div className="relative z-10 flex items-end justify-between gap-4 sm:gap-6 flex-wrap">
          <div className="min-w-0">
            <PageHeader
              eyebrow="Ingestion · EDI parser · 837P / 835"
              title={
                <>
                  Upload an <span className="italic text-muted-foreground/85">X12</span> file.
                </>
              }
              subtitle={
                <>
                  Drop an 837P professional claim or 835 ERA remittance.
                  The parser streams claims back as they're produced, so
                  the instrument updates <span className="text-foreground">live</span>.
                </>
              }
            />
          </div>
          <div
            className={cn(
              "inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-[11px] mono uppercase tracking-[0.12em] backdrop-blur",
              api.isConfigured
                ? "border-border/60 bg-card/60 text-muted-foreground"
                : "border-[hsl(var(--warning)/0.4)] bg-[hsl(var(--warning)/0.08)] text-[hsl(var(--warning))]"
            )}
          >
            <span
              className={cn(
                "relative inline-flex h-1.5 w-1.5 rounded-full",
                api.isConfigured
                  ? "bg-[hsl(var(--success))]"
                  : "bg-[hsl(var(--warning))]"
              )}
            >
              {api.isConfigured ? (
                <span className="absolute inline-flex h-full w-full rounded-full bg-[hsl(var(--success))] opacity-60 animate-ping" />
              ) : null}
            </span>
            {api.isConfigured ? "Backend ready" : "No backend"}
          </div>
        </div>

        {/* Hero stat strip — four compact cells, anchored to the
            parsedBatches store. Lives below the header, separated by
            a hairline, in the same rhythm as Dashboard's KPI row but
            tighter. */}
        <div className="relative z-10 mt-7 grid grid-cols-2 md:grid-cols-4 gap-6 pt-5 border-t border-border/40">
          <HeroStat
            label="Batches ingested"
            value={fmt.num(heroStats.batches)}
            tone="accent"
          />
          <HeroStat
            label="Claims ingested"
            value={fmt.num(heroStats.totalClaims)}
          />
          <HeroStat
            label="Pass rate"
            value={
              heroStats.totalClaims > 0
                ? `${heroStats.passRate.toFixed(1)}%`
                : "—"
            }
            tone={heroStats.passRate >= 95 ? "success" : "default"}
          />
          <HeroStat label="Last ingest" value={heroStats.lastAt} />
        </div>
      </section>

      {/* =================================================================
          TABS — the page's content switcher. Two surfaces:
            · Upload   — drop zone + stream + recent batches (the
                        "in-flight" instrument)
            · History  — persisted batch archive with one-click
                        Re-export ZIP per row
          Tab is mirrored to `?tab=` so deep-links round-trip.
          ================================================================= */}
      <Tabs.Root
        value={tab}
        onValueChange={(v) => setTab(v as "upload" | "history")}
      >
        <Tabs.List aria-label="Upload page sections">
          <Tabs.Trigger value="upload">Upload</Tabs.Trigger>
          <Tabs.Trigger value="history">
            History
            {historyCount > 0 ? (
              <span
                aria-hidden
                className="mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/60 ml-1.5"
              >
                · {historyCount}
              </span>
            ) : null}
          </Tabs.Trigger>
        </Tabs.List>

      <Tabs.Content value="upload">
      {/* =================================================================
          DROP ZONE — the page's centerpiece. Single large surface-2
          card with an inline payer-config header bar, a centered drop
          area, and an action bar at the bottom.
          ================================================================= */}
      <section
        aria-label="File upload"
        className="surface-2 rounded-xl p-6 lg:p-8 animate-fade-in-up relative overflow-hidden"
        style={{ animationDelay: `${dropDelay}ms` }}
      >
        {/* Top hairline + machine-tag header bar */}
        <div className="flex items-center justify-between gap-3 mb-6">
          <div className="flex items-center gap-2.5">
            <span className="mono text-[10.5px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
              Ingest
            </span>
            <span
              className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/40"
              aria-hidden
            >
              · .txt
            </span>
            <span
              className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/40 hidden sm:inline"
              aria-hidden
            >
              · 837P / 835
            </span>
          </div>
          <div
            className={cn(
              "inline-flex items-center gap-1.5 mono text-[10.5px] uppercase tracking-[0.18em] font-semibold",
              api.isConfigured
                ? "text-[hsl(var(--success))]"
                : "text-[hsl(var(--warning))]"
            )}
          >
            <span
              className={cn(
                "h-1.5 w-1.5 rounded-full",
                api.isConfigured
                  ? "bg-[hsl(var(--success))]"
                  : "bg-[hsl(var(--warning))]"
              )}
            />
            {api.isConfigured ? "Ready" : "No backend"}
          </div>
        </div>

        {/* Payer config — Bloomberg-style form field row: small mono
            label + control. Two selects side-by-side. */}
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
          <div className="grid gap-1.5">
            <label
              htmlFor="upload-kind"
              className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70"
            >
              Kind
            </label>
            <Select
              value={kind}
              onValueChange={(v) => {
                const next = v as ParsedBatchKind;
                setKind(next);
                setPayer(
                  next === "837p" ? PAYERS_837[0]!.value : PAYERS_835[0]!.value
                );
              }}
            >
              <SelectTrigger id="upload-kind">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="837p">837P — Professional claim</SelectItem>
                <SelectItem value="835">835 — ERA remittance</SelectItem>
              </SelectContent>
            </Select>
          </div>
          <div className="grid gap-1.5">
            <label
              htmlFor="upload-payer"
              className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70"
            >
              Payer config
            </label>
            <Select value={payer} onValueChange={setPayer}>
              <SelectTrigger id="upload-payer">
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {payerOptions.map((p) => (
                  <SelectItem key={p.value} value={p.value}>
                    {p.label}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
        </div>

        {/* Write affordances — dropzone + Parse button — are gated by
            RoleGate so a viewer-role account can SEE the configuration
            controls (kind, payer) above but can't actually ingest a file. */}
        <RoleGate
          allow={["admin", "user"]}
          fallback={
            <div className="relative rounded-lg border border-dashed border-border/70 bg-muted/20 px-6 py-12 min-h-[200px] flex flex-col items-center justify-center gap-2 text-center text-muted-foreground">
              <UploadIcon className="h-5 w-5" strokeWidth={1.5} aria-hidden />
              <div className="text-[13.5px] font-medium text-foreground">
                Read-only access
              </div>
              <div className="mono text-[10.5px] uppercase tracking-[0.18em]">
                Your role (viewer) cannot ingest new files.
              </div>
            </div>
          }
        >
          {/* Drop area — dashed border at idle, accent glow on drag */}
          <div
            onDragOver={(e) => {
              e.preventDefault();
              setDragging(true);
            }}
            onDragLeave={() => setDragging(false)}
            onDrop={onDrop}
            onClick={() => inputRef.current?.click()}
            role="button"
            tabIndex={0}
            onKeyDown={(e) => {
              if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
            }}
            aria-label="Drop a file here, or click to choose"
            className={cn(
              "relative rounded-lg border border-dashed transition-all duration-200",
              "px-6 py-12 min-h-[200px] cursor-pointer overflow-hidden",
              dragging
                ? "border-accent bg-accent/[0.06] shadow-[inset_0_0_0_1px_hsl(var(--accent)/0.35),0_0_0_4px_hsl(var(--accent)/0.08)]"
                : "border-border/70 bg-background/40 hover:bg-background/60 hover:border-border"
            )}
          >
            {/* On drag — an accent scan-line sweeps across to reinforce
                that the dropzone is hot. Pure CSS via the `animate-scan`
                keyframe already in the Tailwind config. */}
            {dragging ? (
              <div
                aria-hidden
                className="pointer-events-none absolute inset-0 overflow-hidden rounded-lg"
              >
                <div
                  className="absolute inset-y-0 -left-1/3 w-1/3 animate-scan"
                  style={{
                    background:
                      "linear-gradient(90deg, transparent, hsl(var(--accent) / 0.18), transparent)",
                  }}
                />
              </div>
            ) : null}

            <div className="relative flex flex-col items-center justify-center gap-2.5 text-center">
              <div
                className={cn(
                  "h-11 w-11 rounded-md ring-1 ring-inset flex items-center justify-center transition-colors",
                  dragging
                    ? "bg-accent/15 text-accent ring-accent/40"
                    : "bg-muted/50 text-muted-foreground ring-border/60"
                )}
              >
                {dragging ? (
                  <CloudUpload className="h-5 w-5" strokeWidth={1.5} />
                ) : (
                  <UploadIcon className="h-5 w-5" strokeWidth={1.5} />
                )}
              </div>
              {file ? (
                <div className="flex items-center gap-2 text-[13.5px] mt-1">
                  <FileText
                    className="h-4 w-4 text-muted-foreground"
                    strokeWidth={1.75}
                  />
                  <span className="font-medium text-foreground truncate max-w-[40ch]">
                    {file.name}
                  </span>
                  <span className="mono text-[11px] text-muted-foreground">
                    · {formatBytes(file.size)}
                  </span>
                </div>
              ) : (
                <>
                  <div className="display text-[20px] tracking-tight text-foreground mt-1">
                    {dragging ? (
                      <>
                        Release to <span className="italic text-accent">parse</span>
                      </>
                    ) : (
                      <>
                        Drop a file, or click to <span className="italic text-muted-foreground/80">choose</span>
                      </>
                    )}
                  </div>
                  <div className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/60">
                    .txt — X12 837/835 as exported from your clearinghouse
                  </div>
                </>
              )}
              <input
                ref={inputRef}
                type="file"
                accept=".txt"
                className="sr-only"
                onChange={(e) => pickFile(e.target.files?.[0] ?? null)}
              />
            </div>
          </div>
        </RoleGate>

        {/* Action bar — backend status on the left, controls on the right */}
        <div className="mt-5 pt-5 border-t border-border/40 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
          <div className="mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/70 min-w-0 truncate">
            {api.isConfigured ? (
              <span className="inline-flex items-center gap-1.5">
                <span className="h-1.5 w-1.5 rounded-full bg-[hsl(var(--success))]" />
                <span className="text-muted-foreground/60">Target</span>{" "}
                <span className="text-foreground/80 normal-case tracking-normal">
                  {api.baseUrl}
                </span>
              </span>
            ) : (
              <span className="inline-flex items-center gap-1.5 text-[hsl(var(--warning))]">
                <AlertTriangle className="h-3 w-3" strokeWidth={1.75} />
                Set VITE_API_BASE_URL to enable parsing
              </span>
            )}
          </div>
          {/* Parse button — the second half of the write affordance.
              Wrapped independently so the dropzone's `fallback`
              message doesn't sit next to a disabled-looking Parse
              button when the role is viewer. */}
          <RoleGate allow={["admin", "user"]} fallback={null}>
            <div className="flex items-center gap-2 flex-wrap">
              {file ? (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => pickFile(null)}
                  disabled={running}
                >
                  <X className="h-3.5 w-3.5" />
                  Clear
                </Button>
              ) : null}
              <Button
                onClick={onParse}
                disabled={!file || running}
                size="sm"
              >
                {running ? (
                  <>
                    <Loader2 className="h-3.5 w-3.5 animate-spin" />
                    Parsing…
                  </>
                ) : (
                  <>
                    <UploadIcon className="h-3.5 w-3.5" />
                    Parse file
                  </>
                )}
              </Button>
            </div>
          </RoleGate>
        </div>
      </section>

      {/* =================================================================
          STREAM — appears while parsing or after a parse. Standard
          Card wrapper so the section reads as the rest of the app.
          ClaimCard837 / ClaimCard835 keep their warm-paper surface
          so each claim reads as a document inside the panel.
          ================================================================= */}
      {(running || stream.items.length > 0) ? (
        <section
          aria-label="Parse stream"
          className="animate-fade-in-up"
          style={{ animationDelay: `${streamDelay}ms` }}
        >
          <Card>
            <CardContent className="p-6 lg:p-7 space-y-5">
              {/* Stream header — eyebrow + display title + progress stats */}
              <div className="flex items-end justify-between gap-6 flex-wrap">
                <div className="min-w-0">
                  <div className="eyebrow flex items-center gap-2 mb-2">
                    <span className="inline-block h-px w-6 bg-foreground/20" />
                    Stream
                    {running ? (
                      <span className="relative inline-flex h-1.5 w-1.5 ml-1">
                        <span className="absolute inline-flex h-full w-full rounded-full bg-accent opacity-60 animate-ping" />
                        <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-accent" />
                      </span>
                    ) : null}
                  </div>
                  <h2 className="display text-[26px] leading-[1.05] tracking-[-0.02em]">
                    {running ? (
                      <>
                        Parsing <span className="italic text-muted-foreground/85">live.</span>
                      </>
                    ) : (
                      <>
                        Stream <span className="italic text-muted-foreground/85">complete.</span>
                      </>
                    )}
                  </h2>
                </div>
                <div className="flex items-center gap-7 shrink-0">
                  <div className="text-right">
                    <div className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
                      Parsed
                    </div>
                    <div className="display mono text-[24px] leading-[1.05] mt-1.5 tracking-tight">
                      {totalSoFar}
                      <span className="text-[14px] text-muted-foreground/60 ml-1">
                        / {expectedTotal ?? "—"}
                      </span>
                    </div>
                  </div>
                  <div className="text-right">
                    <div className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
                      Progress
                    </div>
                    <div className="display mono text-[24px] leading-[1.05] mt-1.5 tracking-tight">
                      {expectedTotal ? `${progressPct}%` : "…"}
                    </div>
                  </div>
                </div>
              </div>

              {/* Progress bar — accent gradient, like the Inbox stream bar. */}
              <div
                className="h-1 w-full rounded-full overflow-hidden bg-border/60"
                role="progressbar"
                aria-valuemin={0}
                aria-valuemax={100}
                aria-valuenow={progressPct}
              >
                <div
                  className="h-full transition-[width] duration-200 ease-out"
                  style={{
                    width: `${progressPct}%`,
                    background:
                      "linear-gradient(to right, hsl(var(--accent)), hsl(var(--success)))",
                    boxShadow: "0 0 12px hsl(var(--accent) / 0.4)",
                  }}
                />
              </div>

              {/* Streamed claim list — ClaimCards stay as "documents".
                  For 837P streams, render the ExportBar above the list
                  so the user can select all / pick a subset / export. */}
              {kind === "837p" && !running && total837InStream > 0 ? (
                <ExportBar
                  total={total837InStream}
                  selectedCount={selectedClaimIds.size}
                  exporting={exporting}
                  onToggleAll={onToggleAll}
                  onExport={onExport}
                />
              ) : null}

              <div className="max-h-[640px] overflow-y-auto -mx-2 px-2 space-y-2">
                {stream.items.length === 0 && running ? (
                  <div className="text-center py-10 text-[12.5px] text-muted-foreground/70 mono uppercase tracking-[0.14em]">
                    Waiting for first claim…
                  </div>
                ) : null}
                {stream.items.map((item, i) =>
                  item.kind === "837p" ? (
                    <ClaimCard837
                      key={`837-${item.data.claim_id}-${i}`}
                      claim={item.data}
                      selected={selectedClaimIds.has(item.data.claim_id)}
                      onToggleSelect={onToggleSelect}
                    />
                  ) : (
                    <ClaimCard835
                      key={`835-${item.data.payer_claim_control_number}-${i}`}
                      claim={item.data}
                    />
                  )
                )}
              </div>
            </CardContent>
          </Card>
        </section>
      ) : null}

      {/* =================================================================
          RECENT BATCHES — ledger-style list with sequential numbering.
          Standard Card wrapper, numbered rows, kind badge + counts.
          ================================================================= */}
      {parsedBatches.length > 0 ? (
        <section
          aria-label="Recent batches"
          className="animate-fade-in-up"
          style={{ animationDelay: `${batchesDelay}ms` }}
        >
          <Card>
            <CardContent className="p-6 lg:p-7">
              <div className="flex items-end justify-between gap-6 flex-wrap mb-5">
                <div>
                  <div className="eyebrow flex items-center gap-2 mb-2">
                    <span className="inline-block h-px w-6 bg-foreground/20" />
                    Recent batches
                  </div>
                  <h2 className="display text-[26px] leading-[1.05] tracking-[-0.02em]">
                    Archive{" "}
                    <span className="italic text-muted-foreground/85">
                      · {parsedBatches.length}
                    </span>
                  </h2>
                </div>
                <div className="text-right">
                  <div className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
                    Pass rate
                  </div>
                  <div className="display mono text-[22px] leading-[1.05] mt-1.5 tracking-tight">
                    {heroStats.totalClaims > 0
                      ? `${heroStats.passRate.toFixed(1)}%`
                      : "—"}
                  </div>
                </div>
              </div>

              <ul className="divide-y divide-border/40">
                {parsedBatches.map((b, i) => {
                  const passRatio =
                    b.claimCount > 0
                      ? ((b.passed / b.claimCount) * 100).toFixed(1)
                      : "—";
                  const idx = String(parsedBatches.length - i).padStart(2, "0");
                  return (
                    <li
                      key={b.id}
                      className="flex items-center gap-4 py-3.5 first:pt-0 last:pb-0"
                    >
                      <span className="mono text-[11px] text-muted-foreground/60 w-7 shrink-0">
                        #{idx}
                      </span>
                      <Badge variant={b.kind === "837p" ? "default" : "muted"}>
                        {b.kind === "837p" ? "837P" : "835"}
                      </Badge>
                      <div className="flex-1 min-w-0">
                        <div className="text-[13.5px] font-medium truncate">
                          {b.inputFilename}
                        </div>
                        <div className="mono text-[10.5px] text-muted-foreground/70 mt-0.5">
                          {fmt.dateShort(b.parsedAt)} · {b.claimCount}{" "}
                          {b.kind === "837p" ? "claims" : "payments"}
                        </div>
                      </div>
                      <div className="text-right shrink-0">
                        <div className="display mono text-[13.5px] tracking-tight">
                          {b.passed} <span className="text-muted-foreground/50">/</span>{" "}
                          {b.claimCount}
                        </div>
                        <div
                          className={cn(
                            "mono text-[10.5px] mt-0.5",
                            b.failed > 0
                              ? "text-[hsl(var(--warning))]"
                              : "text-muted-foreground/70"
                          )}
                        >
                          {passRatio}% pass
                        </div>
                      </div>
                    </li>
                  );
                })}
              </ul>
            </CardContent>
          </Card>
        </section>
      ) : null}

      {/* =================================================================
          EMPTY STATE — shown only on a fresh browser with no parsed
          batches and nothing currently streaming.
          ================================================================= */}
      {parsedBatches.length === 0 && stream.items.length === 0 && !running ? (
        <section
          className="animate-fade-in-up"
          style={{ animationDelay: `${batchesDelay}ms` }}
        >
          <Card>
            <CardContent className="p-10 lg:p-14 flex flex-col items-center justify-center text-center">
              <div className="h-10 w-10 rounded-md bg-muted/50 ring-1 ring-inset ring-border/60 flex items-center justify-center text-muted-foreground mb-3">
                <Inbox className="h-4 w-4" strokeWidth={1.5} />
              </div>
              <div className="display text-[20px] tracking-tight">
                No batches yet.
              </div>
              <div className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/70 mt-2">
                Drop a file above to start a parse.
              </div>
            </CardContent>
          </Card>
        </section>
      ) : null}
      </Tabs.Content>

      <Tabs.Content value="history">
        <UploadHistory />
      </Tabs.Content>
      </Tabs.Root>
    </div>
  );
}

// ---------------------------------------------------------------------------
// UploadHistory — persisted batch archive rendered inside the History tab.
//
// Source: `useBatches()` (the backend's `/api/batches` list). Independent of
// the in-memory `parsedBatches` store so it survives a page reload — the
// store is purely session-local. When the backend isn't configured (sample-
// data mode) we fall back to the in-memory list so the tab still renders.
// ---------------------------------------------------------------------------

function UploadHistory() {
  const parsedBatches = useAppStore((s) => s.parsedBatches);
  const batchesQuery = useBatches();
  const liveBatches: BatchSummary[] = useMemo(() => {
    if (batchesQuery.data && batchesQuery.data.length > 0) {
      return batchesQuery.data;
    }
    // Sample-data fallback — synthesize BatchSummary rows from the
    // in-memory parsedBatches so the History tab is never empty in
    // a demo. Newest first.
    return [...parsedBatches]
      .reverse()
      .map<BatchSummary>((b) => ({
        id: b.id,
        kind: b.kind,
        inputFilename: b.inputFilename,
        parsedAt: b.parsedAt,
        claimCount: b.claimCount,
        claimIds: b.claimIds,
      }));
  }, [batchesQuery.data, parsedBatches]);

  if (batchesQuery.isLoading && batchesQuery.data === undefined) {
    return (
      <Card>
        <CardContent className="p-10 lg:p-14 flex flex-col items-center justify-center text-center">
          <Loader2
            className="h-4 w-4 animate-spin text-muted-foreground"
            aria-hidden
          />
          <div className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/70 mt-3">
            Loading archive…
          </div>
        </CardContent>
      </Card>
    );
  }

  if (batchesQuery.isError) {
    return (
      <Card>
        <CardContent className="p-10 lg:p-14 flex flex-col items-center justify-center text-center">
          <XCircle
            className="h-5 w-5 text-[hsl(var(--destructive))]"
            strokeWidth={1.5}
            aria-hidden
          />
          <div className="display text-[18px] tracking-tight mt-3">
            Couldn't load the archive.
          </div>
          <div className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/70 mt-2">
            {batchesQuery.error instanceof Error
              ? batchesQuery.error.message
              : "Network error"}
          </div>
        </CardContent>
      </Card>
    );
  }

  if (liveBatches.length === 0) {
    return (
      <Card>
        <CardContent className="p-10 lg:p-14 flex flex-col items-center justify-center text-center">
          <div className="h-10 w-10 rounded-md bg-muted/50 ring-1 ring-inset ring-border/60 flex items-center justify-center text-muted-foreground mb-3">
            <HistoryIcon className="h-4 w-4" strokeWidth={1.5} />
          </div>
          <div className="display text-[20px] tracking-tight">
            No batches in the archive yet.
          </div>
          <div className="mono text-[10.5px] uppercase tracking-[0.18em] text-muted-foreground/70 mt-2">
            Switch to Upload and drop a file to ingest your first batch.
          </div>
        </CardContent>
      </Card>
    );
  }

  return <HistoryTable batches={liveBatches} />;
}

function HistoryTable({ batches }: { batches: BatchSummary[] }) {
  const totalClaims = batches.reduce((s, b) => s + b.claimCount, 0);
  const lastAt = batches[0] ? fmt.dateShort(batches[0].parsedAt) : "—";

  return (
    <Card>
      <CardContent className="p-6 lg:p-7">
        <div className="flex items-end justify-between gap-6 flex-wrap mb-5">
          <div>
            <div className="eyebrow flex items-center gap-2 mb-2">
              <span className="inline-block h-px w-6 bg-foreground/20" />
              Batch history
            </div>
            <h2 className="display text-[26px] leading-[1.05] tracking-[-0.02em]">
              Archive{" "}
              <span className="italic text-muted-foreground/85">
                · {batches.length}
              </span>
            </h2>
          </div>
          <div className="text-right">
            <div className="mono text-[10px] uppercase tracking-[0.18em] font-semibold text-muted-foreground/70">
              Last ingest
            </div>
            <div className="display mono text-[22px] leading-[1.05] mt-1.5 tracking-tight">
              {lastAt}
            </div>
            <div className="mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/60 mt-1">
              {fmt.num(totalClaims)} claims
            </div>
          </div>
        </div>

        <div
          className="rounded-md border overflow-hidden"
          style={{ borderColor: "hsl(30 14% 14% / 0.10)" }}
        >
          <table className="w-full text-[12.5px]">
            <thead style={{ backgroundColor: "hsl(36 22% 90%)" }}>
              <tr
                className="text-left"
                style={{ color: "hsl(var(--surface-ink-2))" }}
              >
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px] w-12">
                  #
                </th>
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px] w-20">
                  Kind
                </th>
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px]">
                  File
                </th>
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px] w-20 text-right">
                  Claims
                </th>
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px] w-28">
                  Parsed
                </th>
                <th className="px-3 py-2 font-medium mono uppercase tracking-[0.14em] text-[10.5px] w-44 text-right">
                  Action
                </th>
              </tr>
            </thead>
            <tbody>
              {batches.map((b, i) => (
                <HistoryRow
                  key={b.id}
                  batch={b}
                  index={batches.length - i}
                />
              ))}
            </tbody>
          </table>
        </div>
      </CardContent>
    </Card>
  );
}

function HistoryRow({
  batch,
  index,
}: {
  batch: BatchSummary;
  index: number;
}) {
  const [exporting, setExporting] = useState(false);
  const canReexport = batch.kind === "837p" && batch.claimIds.length > 0;

  async function onReexport() {
    if (!canReexport || exporting) return;
    setExporting(true);
    try {
      const result = await api.exportBatch837(batch.id, batch.claimIds);
      downloadBlob(result.filename, result.blob);
      const warn =
        result.serializeErrors.length > 0
          ? ` · ${result.serializeErrors.length} skipped`
          : "";
      toast.success(`Re-exported ${batch.claimIds.length} claims${warn}`, {
        description: result.filename,
      });
    } catch (err) {
      toast.error(
        err instanceof ApiError
          ? `Re-export failed (${err.status})`
          : err instanceof Error
            ? err.message
            : "Re-export failed",
      );
    } finally {
      setExporting(false);
    }
  }

  return (
    <tr
      className="border-t align-middle"
      style={{ borderColor: "hsl(30 14% 14% / 0.08)" }}
    >
      <td
        className="px-3 py-2.5 mono text-[11px] text-muted-foreground/60"
      >
        #{String(index).padStart(2, "0")}
      </td>
      <td className="px-3 py-2.5">
        <Badge variant={batch.kind === "837p" ? "default" : "muted"}>
          {batch.kind === "837p" ? "837P" : "835"}
        </Badge>
      </td>
      <td className="px-3 py-2.5">
        <div className="font-medium truncate max-w-[42ch]">
          {batch.inputFilename}
        </div>
      </td>
      <td className="px-3 py-2.5 text-right display mono">
        {fmt.num(batch.claimCount)}
      </td>
      <td
        className="px-3 py-2.5 mono text-[11px]"
        style={{ color: "hsl(var(--surface-ink-3))" }}
      >
        {fmt.dateShort(batch.parsedAt)}
      </td>
      <td className="px-3 py-2.5 text-right">
        {canReexport ? (
          <Button
            variant="ghost"
            size="sm"
            onClick={onReexport}
            disabled={exporting}
            data-testid="history-row-reexport"
            data-batch-id={batch.id}
          >
            {exporting ? (
              <>
                <Loader2 className="h-3.5 w-3.5 animate-spin" />
                Exporting…
              </>
            ) : (
              <>
                <Download className="h-3.5 w-3.5" />
                Re-export ZIP
              </>
            )}
          </Button>
        ) : (
          <span className="mono text-[10.5px] uppercase tracking-[0.14em] text-muted-foreground/50">
            No re-export
          </span>
        )}
      </td>
    </tr>
  );
}