"use client";

import { useEffect, useId, useMemo, useRef, useState } from "react";
import { Check, ChevronsUpDown, CloudUpload, ExternalLink, FileSearch, Paperclip, Upload, X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { FilterSelect, SearchInput } from "@/components/app/filters";
import { cn } from "@/lib/utils";
import { EmptyState, ErrorState } from "../components/ui/page-states";
import { notify } from "../components/ui/feedback-store";
import { describeError } from "../lib/useAsyncAction";
import { useDebouncedSearch } from "../lib/useDebouncedSearch";
import { GROUP_NODES, kindNoun, pathLabel } from "./file-nodes";
import { FileTree } from "./file-tree";
import { fileTypeLabel, formatBytes, formatDate, isImage } from "./file-utils";
import { FileIcon } from "./file-views";
import { documentPreviewUrl } from "./useDocuments";
import { uploadFile, useDocumentById, useFileCategories, useFileList, useFileNode, useFileTree, useRecordNode } from "./useFiles";

/**
 * Pick one file as evidence, handover copy, report… from ANY file the user can see: suggestions from the
 * record, its PO, styles, inquiry and buyer first, then browse the whole tree, recent files, or search.
 * value / onChange use the document id as a string ("" = none); the id is never shown.
 *
 *   <DocumentSelectField record={{ type: "shipment", code: "SHP-0003" }} category="commercial_invoice" value={…} onChange={…} />
 *
 * `record` is where suggestions start and where "upload a new file" saves to; `category` (a documentType code)
 * floats matching files to the top and presets the upload's category. Legacy `ownerType` + `ownerId` still work.
 */
export function DocumentSelectField({ record, ownerType, ownerId, ownerLabel, category = "", label = "Evidence file", value = "", onChange, helper, error: fieldError, emptyLabel = "No file selected", compact = false, wide = false, className }) {
  const { node, isResolving, error: resolveError } = useRecordNode({ record, ownerType, ownerId });
  const [open, setOpen] = useState(false);
  const id = `document-select-${useId()}`;
  const { document, isLoading, error: loadError, setDocument } = useDocumentById(value || null);
  const newer = useDocumentById(document?.newerVersionId || null).document;
  const disabled = !node && !isResolving;
  const errorMessage = fieldError || (value && loadError ? (loadError.status === 403 ? "You cannot open the linked file." : loadError.status === 404 ? "The linked file no longer exists." : describeError(loadError)) : null) || (resolveError ? describeError(resolveError) : null);
  const where = document ? pathLabel(document.path) || document.owner?.label || "" : "";

  const choose = (next) => {
    if (next && typeof next === "object") setDocument(next);
    onChange?.(next ? String(next.id ?? next) : "");
  };

  const trigger = <Button
    id={id}
    type="button"
    variant="outline"
    aria-haspopup="dialog"
    aria-invalid={Boolean(errorMessage)}
    disabled={disabled}
    onClick={() => setOpen(true)}
    className={cn("h-auto min-h-9 min-w-0 flex-1 basis-56 justify-between gap-2 py-1.5 text-left font-normal", compact && "min-h-8 py-1", errorMessage && "border-destructive")}
  >
    {value && isLoading && !document
      ? <span className="grid flex-1 gap-1"><Skeleton className="h-4 w-2/3" /><Skeleton className="h-3 w-1/2" /></span>
      : document
        ? <span className="flex min-w-0 flex-1 items-center gap-2">
          <FileIcon document={document} size="small" />
          <span className="grid min-w-0">
            <span className="truncate text-sm">{document.title} · v{document.version}</span>
            {where && <span className="truncate text-xs text-muted-foreground">{where}</span>}
          </span>
        </span>
        : <span className="flex min-w-0 items-center gap-2 text-muted-foreground"><Paperclip className="size-4 shrink-0" /><span className="truncate">{value ? "Linked file" : isResolving ? "Loading…" : emptyLabel}</span></span>}
    <ChevronsUpDown className="shrink-0 opacity-50" />
  </Button>;

  return <Field data-invalid={Boolean(errorMessage)} className={cn("min-w-0", wide && "col-span-full", className)}>
    <FieldLabel htmlFor={id}>{label}</FieldLabel>
    <div className="flex min-w-0 flex-wrap items-center gap-2">
      {trigger}
      {value && !disabled && <Button type="button" variant="ghost" size={compact ? "icon-sm" : "icon"} onClick={() => choose("")} aria-label={`Clear ${label}`}><X /></Button>}
    </div>
    {newer && newer.id !== document?.id && <div className="flex flex-wrap items-center gap-2 text-xs">
      <Badge variant="warning">v{newer.version} available</Badge>
      <span className="text-muted-foreground">A newer version of this file was uploaded.</span>
      <Button type="button" variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => choose(newer)}>Use v{newer.version}</Button>
    </div>}
    {errorMessage
      ? <FieldError>{errorMessage}</FieldError>
      : <FieldDescription>{disabled ? "Save the record first; files are filed against it." : helper || "Pick any file you can see, or upload a new one. It is linked, not copied."}</FieldDescription>}
    {open && node && <FilePickerDialog
      node={node}
      recordLabel={ownerLabel}
      category={category}
      value={value}
      onClose={() => setOpen(false)}
      onSelect={(picked) => { choose(picked); setOpen(false); }}
    />}
  </Field>;
}

const TABS = [
  { value: "suggested", label: "Suggested" },
  { value: "browse", label: "Browse" },
  { value: "recent", label: "Recent" },
  { value: "search", label: "Search everywhere" },
];

/**
 * The picker dialog. node: the record the picker opened from (suggestions + uploads go there).
 * onSelect(document) receives the chosen document payload.
 * `multiple`: tick several files (checkboxes) and confirm once; onSelectMany(documents) receives them.
 * `excludeIds` hides files that are already chosen (e.g. already attached). `confirmLabel(n)` names the confirm button.
 */
export function FilePickerDialog({ node, recordLabel, category = "", value = "", onSelect, onClose, multiple = false, onSelectMany, excludeIds, confirmLabel }) {
  const { info } = useFileNode(node);
  const { categories } = useFileCategories();
  const [tab, setTab] = useState("suggested");
  const [q, setQ] = useState("");
  const [draftQ, setDraftQ] = useDebouncedSearch(q, setQ, 300);
  const [categoryFilter, setCategoryFilter] = useState("");
  const [selected, setSelected] = useState(null);
  const [picked, setPicked] = useState([]);
  const [browse, setBrowse] = useState({ node, folder: "" });
  const [scopeNode, setScopeNode] = useState("");
  const label = info?.label || recordLabel || "this record";
  const categoryLabel = categories.find((item) => item.value === category)?.label;
  const browseTree = useBrowseTree(browse.node, tab === "browse");

  const common = { q, category: categoryFilter, perPage: 100 };
  const suggested = useFileList(useMemo(() => ({ ...common, node, include: "suggested" }), [categoryFilter, node, q]), { enabled: tab === "suggested" }); // eslint-disable-line react-hooks/exhaustive-deps
  const browsed = useFileList(useMemo(() => ({ ...common, node: browse.node, folder: browse.folder, recursive: 1 }), [browse.folder, browse.node, categoryFilter, q]), { enabled: tab === "browse" }); // eslint-disable-line react-hooks/exhaustive-deps
  const recent = useFileList(useMemo(() => ({ ...common, recent: 1 }), [categoryFilter, q]), { enabled: tab === "recent" }); // eslint-disable-line react-hooks/exhaustive-deps
  const searchReady = Boolean(q || categoryFilter || scopeNode);
  const searched = useFileList(useMemo(() => ({ ...common, node: scopeNode || undefined, recursive: scopeNode ? 1 : undefined }), [categoryFilter, q, scopeNode]), { enabled: tab === "search" && searchReady }); // eslint-disable-line react-hooks/exhaustive-deps
  const active = { suggested, browse: browsed, recent, search: searched }[tab];

  const excluded = useMemo(() => new Set((excludeIds || []).map(String)), [excludeIds]);
  const groups = useMemo(() => {
    const visible = (documents) => (excluded.size ? documents.filter((document) => !excluded.has(String(document.id))) : documents);
    if (tab !== "suggested") return [{ key: "all", label: "", documents: visible(active.documents) }];
    return groupSuggestions(visible(suggested.documents), category && !categoryFilter ? category : "", categoryLabel);
  }, [active.documents, category, categoryFilter, categoryLabel, excluded, suggested.documents, tab]);

  const selectedDocument = selected || null;
  const currentId = String(value || "");

  // Ancestor records of the picker's record, for the search scope chips ("This shipment · PO-0012 · H&M").
  const scopeChips = useMemo(() => {
    const crumbs = (info?.breadcrumb || []).filter((item) => !GROUP_NODES.includes(item.node) && !item.node.includes("/"));
    return crumbs.reverse().map((item, index) => ({ node: item.node, label: index === 0 ? `This ${kindNoun(item.node)}` : item.label }));
  }, [info?.breadcrumb]);

  const pickedIds = useMemo(() => new Set(picked.map((document) => document.id)), [picked]);
  const pickedBytes = picked.reduce((sum, document) => sum + (Number(document.fileSize) || 0), 0);
  // Multiple mode: a click ticks or unticks the file and shows it in the preview.
  const toggle = (document) => {
    setSelected(document);
    setPicked((current) => (current.some((item) => item.id === document.id) ? current.filter((item) => item.id !== document.id) : [...current, document]));
  };

  const afterUpload = (document) => {
    setSelected(document);
    if (multiple && document) setPicked((current) => (current.some((item) => item.id === document.id) ? current : [...current, document]));
    suggested.refresh();
    if (tab !== "suggested") setTab("suggested");
  };

  return <Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
    <DialogContent className="flex h-dvh max-h-dvh w-screen max-w-none flex-col gap-0 overflow-hidden rounded-none p-0 sm:h-[min(760px,calc(100dvh-2rem))] sm:w-[calc(100vw-2rem)] sm:max-w-5xl sm:rounded-xl">
      <DialogHeader className="border-b px-4 py-3 pr-12 text-left sm:px-5">
        <DialogTitle>{multiple ? "Choose files" : "Choose a file"} · {label}</DialogTitle>
        <DialogDescription>{multiple ? "Tick every file you need. They come from the file manager and are not copied." : <>Pick any file you can see. It&apos;s linked, not copied.</>}</DialogDescription>
      </DialogHeader>

      <div className="grid gap-2 border-b px-4 py-3 sm:px-5">
        <Tabs value={tab} onValueChange={setTab}>
          <TabsList className="grid h-auto w-full grid-cols-2 sm:inline-flex sm:w-auto">
            {TABS.map((item) => <TabsTrigger key={item.value} value={item.value}>{item.label}</TabsTrigger>)}
          </TabsList>
        </Tabs>
        <div className="flex flex-wrap items-center gap-2">
          <SearchInput value={draftQ} onChange={setDraftQ} placeholder={tab === "search" ? "Search every file: title, code, uploader…" : "Filter by title, code, uploader…"} className="min-w-40 flex-1 sm:max-w-sm" />
          <FilterSelect label="Category" value={categoryFilter} onChange={setCategoryFilter} options={categories} allLabel="All categories" className="h-9" />
        </div>
        {tab === "search" && scopeChips.length > 0 && <div className="flex flex-wrap items-center gap-1.5" role="radiogroup" aria-label="Search in">
          {[...scopeChips, { node: "", label: "Everywhere" }].map((chip) => <Button
            key={chip.node || "everywhere"}
            type="button"
            role="radio"
            aria-checked={scopeNode === chip.node}
            variant="outline"
            size="sm"
            className={cn("h-7 rounded-full px-3 text-xs", scopeNode === chip.node && "border-primary/40 bg-primary/10")}
            onClick={() => setScopeNode(chip.node)}
          >{chip.label}</Button>)}
        </div>}
      </div>

      <div className="flex min-h-0 flex-1">
        {tab === "browse" && <BrowseTree tree={browseTree} browse={browse} onBrowse={setBrowse} />}

        <div className="flex min-h-0 min-w-0 flex-1 flex-col">
          {tab === "browse" && <BrowseTreeMobile tree={browseTree} browse={browse} onBrowse={setBrowse} />}
          <div className="min-h-0 flex-1 overflow-y-auto p-3 sm:p-4">
            <PickerList
              tab={tab}
              list={active}
              groups={groups}
              searchReady={searchReady}
              selectedId={selectedDocument?.id}
              currentId={currentId}
              multiple={multiple}
              pickedIds={pickedIds}
              onSelect={multiple ? toggle : setSelected}
              onConfirm={multiple ? toggle : (document) => onSelect(document)}
              onClearFilters={() => { setDraftQ(""); setQ(""); setCategoryFilter(""); }}
              onSearchEverywhere={() => { setTab("search"); setScopeNode(""); }}
            />
          </div>
          <UploadStrip node={node} label={label} category={category || info?.defaultCategory || ""} onUploaded={afterUpload} />
        </div>

        <PreviewPane document={selectedDocument} />
      </div>

      {multiple
        ? <DialogFooter className="flex-row items-center gap-2 border-t px-4 py-3 sm:justify-between sm:px-5">
          <span className="min-w-0 truncate text-xs text-muted-foreground" aria-live="polite">{picked.length ? <><strong className="font-medium tabular-nums text-foreground">{picked.length}</strong> {picked.length === 1 ? "file" : "files"} selected · <span className="tabular-nums">{formatBytes(pickedBytes)}</span></> : "Tick the files you need."}</span>
          <div className="ml-auto flex items-center gap-2">
            {picked.length > 0 && <Button type="button" variant="ghost" className="hidden sm:inline-flex" onClick={() => setPicked([])}>Clear</Button>}
            <Button type="button" variant="outline" onClick={onClose}>Cancel</Button>
            <Button type="button" disabled={!picked.length} onClick={() => onSelectMany?.(picked)}>{confirmLabel ? confirmLabel(picked.length) : picked.length > 1 ? `Use ${picked.length} files` : "Use file"}</Button>
          </div>
        </DialogFooter>
        : <DialogFooter className="flex-row items-center gap-2 border-t px-4 py-3 sm:justify-between sm:px-5">
          <span className="hidden min-w-0 truncate text-xs text-muted-foreground sm:block">{selectedDocument ? <>Selected: <strong className="font-medium text-foreground">{selectedDocument.title}</strong> · v{selectedDocument.version}</> : "Select a file from the list."}</span>
          <div className="ml-auto flex items-center gap-2">
            {currentId && <Button type="button" variant="ghost" onClick={() => onSelect(null)}>Clear selection</Button>}
            <Button type="button" variant="outline" onClick={onClose}>Cancel</Button>
            <Button type="button" disabled={!selectedDocument} onClick={() => onSelect(selectedDocument)}>Use this file</Button>
          </div>
        </DialogFooter>}
    </DialogContent>
  </Dialog>;
}

// Suggestions arrive with a group label ("On this shipment", "PO-0012", "Style STL-0045", "H&M buyer files").
// With a preset category, matching files are also listed first under "Best matches".
function groupSuggestions(documents, category, categoryLabel) {
  const groups = [];
  const byKey = new Map();
  if (category) {
    const matches = documents.filter((document) => document.type?.code === category);
    if (matches.length) groups.push({ key: "best", label: `Best matches · ${categoryLabel || "this category"}`, documents: matches });
  }
  documents.forEach((document) => {
    const key = document.suggestGroup || "Other files";
    if (!byKey.has(key)) {
      const group = { key, label: key, documents: [] };
      byKey.set(key, group);
      groups.push(group);
    }
    byKey.get(key).documents.push(document);
  });
  return groups;
}

function PickerList({ tab, list, groups, searchReady, selectedId, currentId, multiple = false, pickedIds, onSelect, onConfirm, onClearFilters, onSearchEverywhere }) {
  if (tab === "search" && !searchReady) {
    return <EmptyState compact icon={FileSearch} title="Search every file you can see" detail="Type a title, a record code (PO-0012, SHP-0003, STL-0045), a buyer or an uploader. Or pick a category." />;
  }
  if (list.error && !list.documents.length) return <ErrorState error={list.error} title="Files could not be loaded" onRetry={list.refresh} compact />;
  if (list.isLoading) return <div className="grid gap-2" aria-busy="true" aria-label="Loading files">{[0, 1, 2, 3, 4].map((index) => <Skeleton key={index} className="h-14" />)}</div>;
  if (!groups.some((group) => group.documents.length)) {
    return <EmptyState
      compact
      filtered
      title={tab === "recent" ? "No recent files" : "No files here"}
      detail={tab === "suggested" ? "Nothing on this record or the records around it matches. Search everywhere, or upload the file below." : "Try another search or category, or upload the file below."}
      onClearFilters={onClearFilters}
      action={tab !== "search" && <Button variant="outline" size="sm" onClick={onSearchEverywhere}>Search everywhere</Button>}
    />;
  }

  return <div className={cn("grid gap-4 transition-opacity", list.isRefreshing && "opacity-60")} role={multiple ? "group" : "radiogroup"} aria-label="Files">
    {groups.map((group) => <section key={group.key} className="grid gap-1.5">
      {group.label && <h3 className="text-xs font-medium text-muted-foreground">{group.label} <span className="tabular-nums">· {group.documents.length}</span></h3>}
      {group.documents.map((document) => <PickerRow key={`${group.key}-${document.id}`} document={document} multiple={multiple} selected={multiple ? Boolean(pickedIds?.has(document.id)) : selectedId === document.id} current={currentId === String(document.id)} onSelect={onSelect} onConfirm={onConfirm} />)}
    </section>)}
  </div>;
}

function PickerRow({ document, multiple = false, selected, current, onSelect, onConfirm }) {
  const where = pathLabel(document.path) || document.owner?.label || "";
  return <button
    type="button"
    role={multiple ? "checkbox" : "radio"}
    aria-checked={selected}
    onClick={() => onSelect(document)}
    onDoubleClick={multiple ? undefined : () => onConfirm(document)}
    className={cn(
      "flex w-full min-w-0 items-center gap-3 rounded-lg border px-3 py-2 text-left outline-none transition-colors hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring/50",
      selected ? "border-primary bg-accent" : "border-transparent",
    )}
  >
    {multiple
      ? <span className={cn("grid size-4 shrink-0 place-items-center rounded-[4px] border", selected ? "border-primary bg-primary text-primary-foreground" : "border-input")} aria-hidden="true">
        {selected && <Check className="size-3" />}
      </span>
      : <span className={cn("grid size-4 shrink-0 place-items-center rounded-full border", selected ? "border-primary" : "border-input")} aria-hidden="true">
        {selected && <span className="size-2 rounded-full bg-primary" />}
      </span>}
    <FileIcon document={document} />
    <span className="grid min-w-0 flex-1 gap-0.5">
      <span className="flex min-w-0 flex-wrap items-center gap-1.5">
        <span className="truncate text-sm font-medium">{document.title}</span>
        {document.type?.label && <Badge variant="secondary" className="h-5 px-1.5 text-xs font-normal">{document.type.label}</Badge>}
        <Badge variant="neutral" className="h-5 px-1.5 text-xs font-normal">v{document.version}</Badge>
        {current && <Badge variant="info" className="h-5 px-1.5 text-xs font-normal">Linked now</Badge>}
        {document.newerVersionId && <Badge variant="warning" className="h-5 px-1.5 text-xs font-normal">Newer version exists</Badge>}
      </span>
      <span className="truncate text-xs text-muted-foreground">{[where, document.uploadedBy?.name, formatDate(document.uploadedAt)].filter(Boolean).join(" · ")}</span>
    </span>
  </button>;
}

// One tree for the Browse tab (desktop column and phone strip), opened down to the picker's record.
function useBrowseTree(node, active) {
  const tree = useFileTree("root");
  const { info } = useFileNode(active ? node : "");
  const revealed = useRef("");
  const { reveal } = tree;
  useEffect(() => {
    const key = (info?.breadcrumb || []).map((item) => item.node).join("|");
    if (!key || revealed.current) return;
    revealed.current = key;
    reveal([...(info.breadcrumb || []), { node: "__self" }]);
  }, [info?.breadcrumb, reveal]);
  return tree;
}

function BrowseTree({ tree, browse, onBrowse }) {
  return <aside className="hidden w-64 shrink-0 overflow-y-auto border-r bg-muted/20 p-2 sm:block" aria-label="Browse folders">
    <FileTree tree={tree} selectedNode={browse.node} selectedFolder={browse.folder} onSelect={(item) => onBrowse({ node: item.node, folder: item.folder || "" })} label="Browse folders" dense />
  </aside>;
}

// Phones: the tree sits above the list in a short scroll area.
function BrowseTreeMobile({ tree, browse, onBrowse }) {
  return <div className="max-h-44 overflow-y-auto border-b p-2 sm:hidden">
    <FileTree tree={tree} selectedNode={browse.node} selectedFolder={browse.folder} onSelect={(item) => onBrowse({ node: item.node, folder: item.folder || "" })} label="Browse folders" dense />
  </div>;
}

function PreviewPane({ document }) {
  if (!document) {
    return <aside className="hidden w-72 shrink-0 border-l p-4 lg:grid lg:place-items-center" aria-label="Preview">
      <p className="text-center text-xs text-muted-foreground">Select a file to preview it here.</p>
    </aside>;
  }
  const canPreview = document.previewable && document.permissions?.download && document.status !== "deleted";

  return <aside className="hidden w-72 shrink-0 overflow-y-auto border-l p-4 lg:grid lg:content-start lg:gap-3" aria-label="Preview">
    <div className="relative aspect-3/4 overflow-hidden rounded-lg border bg-muted/40">
      {canPreview
        ? isImage(document)
          ? <img src={documentPreviewUrl(document.id)} alt={`Preview of ${document.title}`} className="size-full object-contain" />
          : <iframe src={`${documentPreviewUrl(document.id)}#toolbar=0&navpanes=0&view=FitH`} title={`Preview of ${document.title}`} tabIndex={-1} className="size-full bg-background" />
        : <div className="grid size-full place-items-center"><FileIcon document={document} size="large" /></div>}
    </div>
    <div className="grid gap-1">
      <p className="text-sm font-medium break-words">{document.title}</p>
      <p className="text-xs text-muted-foreground">{fileTypeLabel(document)} · {formatBytes(document.fileSize)} · v{document.version}</p>
      {pathLabel(document.path) && <p className="text-xs text-muted-foreground">Filed in {pathLabel(document.path)}</p>}
      <p className="text-xs text-muted-foreground">Uploaded {formatDate(document.uploadedAt)}{document.uploadedBy?.name ? ` by ${document.uploadedBy.name}` : ""}</p>
    </div>
    {canPreview && <Button variant="outline" size="sm" asChild><a href={documentPreviewUrl(document.id)} target="_blank" rel="noopener"><ExternalLink /> Open in new tab</a></Button>}
  </aside>;
}

// Upload a new file straight into the picker's record, with the preset category; it is then selected.
function UploadStrip({ node, label, category, onUploaded }) {
  const inputRef = useRef(null);
  const [dragging, setDragging] = useState(false);
  const [busy, setBusy] = useState(false);

  const upload = async (files) => {
    const file = files?.[0];
    if (!file) return;
    setBusy(true);
    try {
      const saved = await uploadFile({ node, file, category: category || undefined });
      notify.success("File uploaded", `${saved?.title || file.name} is filed in ${label} and selected.`);
      onUploaded(saved);
    } catch (error) {
      const fieldMessage = error?.errors ? Object.values(error.errors).flat()[0] : null;
      notify.error("File not uploaded", fieldMessage || describeError(error));
    } finally {
      setBusy(false);
    }
  };

  return <div
    className={cn("flex flex-wrap items-center gap-2 border-t border-dashed px-4 py-2.5 text-sm transition-colors sm:px-5", dragging && "bg-accent")}
    onDragOver={(event) => { event.preventDefault(); setDragging(true); }}
    onDragLeave={() => setDragging(false)}
    onDrop={(event) => { event.preventDefault(); setDragging(false); upload(event.dataTransfer.files); }}
  >
    {busy ? <Spinner /> : <CloudUpload className="size-4 text-muted-foreground" />}
    <span className="min-w-0 flex-1 text-muted-foreground"><span className="hidden sm:inline">Drop a file to upload to </span><span className="sm:hidden">Upload to </span><strong className="font-medium text-foreground">{label}</strong></span>
    <Button type="button" variant="outline" size="sm" onClick={() => inputRef.current?.click()} disabled={busy}><Upload /> Browse…</Button>
    <input ref={inputRef} type="file" hidden onChange={(event) => { upload(event.target.files); event.target.value = ""; }} />
  </div>;
}
