'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Document, Page, pdfjs } from 'react-pdf';
import {
  ChevronLeft,
  ChevronRight,
  Eye,
  Loader2,
  Save,
  Sparkles,
  Trash2,
  Upload,
  X,
} from 'lucide-react';

import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';

import { Button } from '@/components/ui/button';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { PageHeader } from '@/components/shared/page-header';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
import { VALID_MERGE_TOKENS } from '@/lib/templates/merge-fields';
import type { FieldMap, FieldMapEntry } from '@/lib/templates/field-map';

// Worker setup mirrors src/components/files/pdf-viewer.tsx so we don't
// pay for a second bundle. Calling GlobalWorkerOptions.workerSrc twice
// with the same URL is a no-op in pdfjs.
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;

interface TemplateData {
  id: string;
  name: string;
  templateType: string;
  templateFormat: string;
  sourceFileId: string | null;
  overlayPositions: FieldMap | null;
  /** Tokens marked as required for the EOI flow - see
   *  STANDARD_EOI_MERGE_FIELDS in lib/templates/merge-fields. The editor
   *  surfaces a checklist of which required tokens are still unplaced. */
  mergeFields?: string[] | null;
}

interface PendingMarker {
  x: number;
  y: number;
  page: number;
}

type DragKind = 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se';

interface DragState {
  index: number;
  kind: DragKind;
  startMarkerX: number;
  startMarkerY: number;
  startMarkerW: number;
  startMarkerH: number;
  startClientX: number;
  startClientY: number;
  containerW: number;
  containerH: number;
}

const TOKEN_OPTIONS = Array.from(VALID_MERGE_TOKENS).sort();
const DEFAULT_MARKER_W = 0.18;
const DEFAULT_MARKER_H = 0.04;
const MIN_MARKER_DIM = 0.02;

/**
 * Phase 7.1 + 7.2 - PDF marker editor.
 *
 *  - Click anywhere to drop a marker (page-aware).
 *  - Drag markers to move; corner handles to resize.
 *  - Right-click for context-menu delete.
 *  - Multi-page navigation via page picker.
 *  - "Required tokens unplaced" checklist surfaces missing fields.
 *  - Unsaved-changes guard (beforeunload + visual diff indicator).
 *  - Responsive PDF width tracks the container via ResizeObserver.
 *  - Live preview pane renders the AcroForm fill against a chosen
 *    interest via POST /api/v1/document-templates/[id]/preview.
 *  - "Replace PDF" reuses the existing template-templates source-file
 *    upload route while preserving the field map (warn on page-count
 *    change).
 */
export function TemplateEditor({ templateId }: { templateId: string }) {
  const { data: template, isLoading } = useQuery<{ data: TemplateData }>({
    queryKey: ['document-template', templateId],
    queryFn: () => apiFetch<{ data: TemplateData }>(`/api/v1/document-templates/${templateId}`),
  });

  if (isLoading || !template) {
    return (
      <div className="flex h-64 items-center justify-center text-sm text-muted-foreground">
        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
        Loading template…
      </div>
    );
  }

  // Inner body keyed by templateId so a route change re-mounts and the
  // markers useState initializer re-reads the server payload, avoiding
  // an in-render setState pattern to seed from the query.
  return <TemplateEditorBody key={templateId} templateId={templateId} template={template.data} />;
}

function TemplateEditorBody({
  templateId,
  template,
}: {
  templateId: string;
  template: TemplateData;
}) {
  const qc = useQueryClient();
  const [markers, setMarkers] = useState<FieldMap>(template.overlayPositions ?? []);
  // Track the "last saved" baseline as state (not a ref) so the dirty
  // check re-runs on save. React Compiler forbids reading refs during
  // render, and the isDirty memo needs a stable baseline that updates
  // exactly when we commit a save.
  const [savedMarkers, setSavedMarkers] = useState<FieldMap>(template.overlayPositions ?? []);
  const [pending, setPending] = useState<PendingMarker | null>(null);
  const [pendingToken, setPendingToken] = useState<string>('');
  const [saving, setSaving] = useState(false);
  const [savedMsg, setSavedMsg] = useState<string | null>(null);
  const [pageNumber, setPageNumber] = useState(1);
  const [numPages, setNumPages] = useState<number | null>(null);
  const [pageWidth, setPageWidth] = useState(680);
  const [dragState, setDragState] = useState<DragState | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [previewLoading, setPreviewLoading] = useState(false);
  const [previewInterestId, setPreviewInterestId] = useState<string>('');
  const [autoDetectLoading, setAutoDetectLoading] = useState(false);
  const [autoDetectMsg, setAutoDetectMsg] = useState<string | null>(null);
  const pageContainerRef = useRef<HTMLDivElement | null>(null);
  const outerColumnRef = useRef<HTMLDivElement | null>(null);
  const fileInputRef = useRef<HTMLInputElement | null>(null);

  const pdfUrl = template.sourceFileId ? `/api/v1/files/${template.sourceFileId}/preview` : null;

  // ─── Unsaved changes detection ────────────────────────────────────────────
  const isDirty = useMemo(
    () => JSON.stringify(markers) !== JSON.stringify(savedMarkers),
    [markers, savedMarkers],
  );
  useEffect(() => {
    if (!isDirty) return;
    const handler = (e: BeforeUnloadEvent) => {
      e.preventDefault();
      // Modern browsers ignore the returnValue string and show their own
      // generic "you have unsaved changes" prompt - setting it still
      // triggers the prompt, just without our wording.
      e.returnValue = '';
    };
    window.addEventListener('beforeunload', handler);
    return () => window.removeEventListener('beforeunload', handler);
  }, [isDirty]);

  // ─── Responsive PDF width ─────────────────────────────────────────────────
  useEffect(() => {
    const el = outerColumnRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      const w = entries[0]?.contentRect.width ?? 680;
      // Subtract Card padding/border so the PDF doesn't overshoot the
      // container. 32 = px-4 (16) × 2.
      setPageWidth(Math.max(360, Math.floor(w - 32)));
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // ─── Click-to-place (only when not dragging) ──────────────────────────────
  function handlePageClick(e: React.MouseEvent<HTMLDivElement>) {
    if (dragState) return; // drag in progress; ignore click
    const container = pageContainerRef.current;
    if (!container) return;
    // Ignore clicks bubbling from a marker (handled by its own onClick).
    const target = e.target as HTMLElement;
    if (target.dataset.markerHandle === 'true') return;
    const rect = container.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    if (x < 0 || x > 1 || y < 0 || y > 1) return;
    setPending({ x, y, page: pageNumber });
    setPendingToken(TOKEN_OPTIONS[0] ?? '');
  }

  function commitPending() {
    if (!pending || !pendingToken) return;
    const entry: FieldMapEntry = {
      token: pendingToken,
      page: pending.page,
      x: pending.x,
      y: pending.y,
      w: DEFAULT_MARKER_W,
      h: DEFAULT_MARKER_H,
    };
    setMarkers((m) => [...m, entry]);
    setPending(null);
    setPendingToken('');
  }

  function cancelPending() {
    setPending(null);
    setPendingToken('');
  }

  function removeMarker(index: number) {
    setMarkers((m) => m.filter((_, i) => i !== index));
  }

  // ─── Drag + resize ────────────────────────────────────────────────────────
  const onMarkerMouseDown = useCallback(
    (index: number, kind: DragKind) => (e: React.MouseEvent<HTMLDivElement>) => {
      e.stopPropagation();
      e.preventDefault();
      const container = pageContainerRef.current;
      if (!container) return;
      const rect = container.getBoundingClientRect();
      const m = markers[index];
      if (!m) return;
      setDragState({
        index,
        kind,
        startMarkerX: m.x,
        startMarkerY: m.y,
        startMarkerW: m.w ?? DEFAULT_MARKER_W,
        startMarkerH: m.h ?? DEFAULT_MARKER_H,
        startClientX: e.clientX,
        startClientY: e.clientY,
        containerW: rect.width,
        containerH: rect.height,
      });
    },
    [markers],
  );

  useEffect(() => {
    if (!dragState) return;
    function onMove(e: MouseEvent) {
      if (!dragState) return;
      const dxPct = (e.clientX - dragState.startClientX) / dragState.containerW;
      const dyPct = (e.clientY - dragState.startClientY) / dragState.containerH;
      setMarkers((current) => {
        const next = current.slice();
        const m = next[dragState.index];
        if (!m) return current;
        const w = m.w ?? DEFAULT_MARKER_W;
        const h = m.h ?? DEFAULT_MARKER_H;
        if (dragState.kind === 'move') {
          // Clamp so the box stays fully on-page.
          next[dragState.index] = {
            ...m,
            x: clamp(dragState.startMarkerX + dxPct, 0, 1 - w),
            y: clamp(dragState.startMarkerY + dyPct, 0, 1 - h),
          };
        } else {
          // Resize from a specific corner; the opposing corner stays
          // pinned, so x/y/w/h all change for NW (drag the top-left
          // corner: x and y increase by the delta, w and h decrease by it).
          let nx = dragState.startMarkerX;
          let ny = dragState.startMarkerY;
          let nw = dragState.startMarkerW;
          let nh = dragState.startMarkerH;
          if (dragState.kind === 'resize-nw') {
            nx = dragState.startMarkerX + dxPct;
            ny = dragState.startMarkerY + dyPct;
            nw = dragState.startMarkerW - dxPct;
            nh = dragState.startMarkerH - dyPct;
          } else if (dragState.kind === 'resize-ne') {
            ny = dragState.startMarkerY + dyPct;
            nw = dragState.startMarkerW + dxPct;
            nh = dragState.startMarkerH - dyPct;
          } else if (dragState.kind === 'resize-sw') {
            nx = dragState.startMarkerX + dxPct;
            nw = dragState.startMarkerW - dxPct;
            nh = dragState.startMarkerH + dyPct;
          } else if (dragState.kind === 'resize-se') {
            nw = dragState.startMarkerW + dxPct;
            nh = dragState.startMarkerH + dyPct;
          }
          // Enforce min size + clamp so the box stays on-page.
          nw = Math.max(MIN_MARKER_DIM, Math.min(1, nw));
          nh = Math.max(MIN_MARKER_DIM, Math.min(1, nh));
          nx = clamp(nx, 0, 1 - nw);
          ny = clamp(ny, 0, 1 - nh);
          next[dragState.index] = { ...m, x: nx, y: ny, w: nw, h: nh };
        }
        return next;
      });
    }
    function onUp() {
      setDragState(null);
    }
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    return () => {
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
    };
  }, [dragState]);

  // ─── Save ────────────────────────────────────────────────────────────────
  async function save() {
    setSaving(true);
    setSavedMsg(null);
    try {
      await apiFetch(`/api/v1/document-templates/${templateId}`, {
        method: 'PATCH',
        body: { overlayPositions: markers },
      });
      setSavedMarkers(markers);
      await qc.invalidateQueries({ queryKey: ['document-template', templateId] });
      setSavedMsg('Markers saved.');
    } catch (err) {
      toastError(err);
    } finally {
      setSaving(false);
    }
  }

  // ─── Preview ─────────────────────────────────────────────────────────────
  async function generatePreview() {
    if (!previewInterestId) {
      toastError(new Error('Pick an interest to preview against.'));
      return;
    }
    setPreviewLoading(true);
    try {
      const res = await apiFetch<{ data: { previewUrl: string } }>(
        `/api/v1/document-templates/${templateId}/preview`,
        { method: 'POST', body: { interestId: previewInterestId } },
      );
      setPreviewUrl(res.data.previewUrl);
    } catch (err) {
      toastError(err);
    } finally {
      setPreviewLoading(false);
    }
  }

  // ─── Auto-detect anchors (Phase 4) ───────────────────────────────────────
  // Calls the field-detector service via POST /detect-fields, which scans
  // the current source PDF's text content for SIGNATURE / DATE / INITIALS /
  // NAME / EMAIL anchors. Converts the returned DetectedField[] (percent
  // 0..100) into FieldMapEntry markers (percent 0..1) and appends them to
  // the existing field map. Users retag tokens via the per-marker dropdown.
  async function autoDetect() {
    setAutoDetectLoading(true);
    setAutoDetectMsg(null);
    try {
      const res = await apiFetch<{
        data: {
          fields: Array<{
            type: string;
            pageNumber: number;
            pageX: number;
            pageY: number;
            pageWidth: number;
            pageHeight: number;
            confidence: number;
            anchorText: string;
            inferredRecipientLabel?: string | null;
          }>;
          totalAnchors: number;
        };
      }>(`/api/v1/document-templates/${templateId}/detect-fields`, { method: 'POST' });

      if (res.data.fields.length === 0) {
        setAutoDetectMsg('No anchors detected. Place markers manually.');
        return;
      }

      // Map detected type → best-guess merge token. Falls back to first
      // sorted token when the detector finds a Documenso-only field
      // (SIGNATURE / INITIALS) that has no direct merge-token equivalent
      // in the in-app fill pipeline - the user retags from the dropdown.
      const fallbackToken = TOKEN_OPTIONS[0] ?? '';
      const pick = (candidates: string[]): string => {
        for (const c of candidates) {
          if (TOKEN_OPTIONS.includes(c)) return c;
        }
        return fallbackToken;
      };
      const tokenForType = (type: string): string => {
        switch (type.toUpperCase()) {
          case 'DATE':
            return pick(['{{eoi.dateGenerated}}', '{{eoi.todayDate}}', '{{date}}']);
          case 'NAME':
            return pick(['{{client.fullName}}', '{{client.firstName}}']);
          case 'EMAIL':
            return pick(['{{client.email}}']);
          default:
            return fallbackToken;
        }
      };

      const newMarkers: FieldMap = res.data.fields.map((f) => ({
        token: tokenForType(f.type),
        page: f.pageNumber,
        x: f.pageX / 100,
        y: f.pageY / 100,
        w: Math.max(MIN_MARKER_DIM, f.pageWidth / 100),
        h: Math.max(MIN_MARKER_DIM, f.pageHeight / 100),
      }));

      setMarkers((current) => [...current, ...newMarkers]);
      setAutoDetectMsg(
        `Added ${newMarkers.length} marker${newMarkers.length === 1 ? '' : 's'} from auto-detect. Review the assigned tokens (defaults are best-guess) and save.`,
      );
    } catch (err) {
      toastError(err);
    } finally {
      setAutoDetectLoading(false);
    }
  }

  // ─── New-PDF upload (replace source) ─────────────────────────────────────
  async function onReplacePdf(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    try {
      const fd = new FormData();
      fd.append('file', file);
      const res = await fetch(`/api/v1/document-templates/${templateId}/source-pdf`, {
        method: 'POST',
        body: fd,
      });
      if (!res.ok) throw new Error((await res.text()) || 'Upload failed');
      const json = (await res.json()) as { data?: { pageCount?: number } };
      const newPageCount = json.data?.pageCount;
      if (newPageCount && numPages && newPageCount < numPages) {
        toastError(
          new Error(
            `New PDF has ${newPageCount} pages; the previous source had ${numPages}. Markers on pages ${newPageCount + 1}–${numPages} are now orphaned and won't render.`,
          ),
        );
      }
      await qc.invalidateQueries({ queryKey: ['document-template', templateId] });
    } catch (err) {
      toastError(err);
    } finally {
      if (fileInputRef.current) fileInputRef.current.value = '';
    }
  }

  // ─── Derived state ────────────────────────────────────────────────────────
  const visibleMarkers = useMemo(
    () =>
      markers
        .map((m, i) => ({ marker: m, index: i }))
        .filter((row) => row.marker.page === pageNumber),
    [markers, pageNumber],
  );
  const placedTokens = useMemo(() => new Set(markers.map((m) => m.token)), [markers]);
  const requiredTokens = template.mergeFields ?? [];
  const unplacedRequired = requiredTokens.filter((t) => !placedTokens.has(t));

  if (!pdfUrl) {
    return (
      <div className="space-y-4">
        <PageHeader
          title={`Edit "${template.name}"`}
          description="Place merge-field markers on the source PDF."
        />
        <Card>
          <CardContent className="py-10 text-center text-sm text-muted-foreground">
            This template has no source PDF attached. Upload one to get started.
            <div className="mt-4">
              <Button onClick={() => fileInputRef.current?.click()}>
                <Upload className="mr-1.5 h-4 w-4" /> Upload PDF
              </Button>
              <input
                ref={fileInputRef}
                type="file"
                accept="application/pdf"
                className="hidden"
                onChange={onReplacePdf}
              />
            </div>
          </CardContent>
        </Card>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      <PageHeader
        title={`Edit "${template.name}"`}
        description="Click to drop markers · drag to move · corner handles to resize · right-click to delete."
        actions={
          <div className="flex items-center gap-2">
            {isDirty ? (
              <span className="text-xs text-amber-700 font-medium">Unsaved changes</span>
            ) : null}
            <Button
              variant="outline"
              size="sm"
              onClick={autoDetect}
              disabled={autoDetectLoading || !pdfUrl}
            >
              {autoDetectLoading ? (
                <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
              ) : (
                <Sparkles className="mr-1.5 h-3.5 w-3.5" />
              )}
              Auto-detect
            </Button>
            <Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()}>
              <Upload className="mr-1.5 h-3.5 w-3.5" /> Replace PDF
            </Button>
            <input
              ref={fileInputRef}
              type="file"
              accept="application/pdf"
              className="hidden"
              onChange={onReplacePdf}
            />
            <Button onClick={save} disabled={saving || !isDirty}>
              <Save className="mr-1.5 h-4 w-4" />
              {saving ? 'Saving…' : 'Save'}
            </Button>
          </div>
        }
      />

      {savedMsg ? <p className="text-xs text-emerald-700">{savedMsg}</p> : null}
      {autoDetectMsg ? <p className="text-xs text-sky-700">{autoDetectMsg}</p> : null}

      <div className="grid gap-4 lg:grid-cols-[1fr_320px]">
        <div ref={outerColumnRef}>
          <Card>
            <CardHeader className="pb-2">
              <div className="flex items-center justify-between gap-2">
                <CardTitle className="text-sm">
                  Page {pageNumber}
                  {numPages ? ` / ${numPages}` : ''}
                </CardTitle>
                {numPages && numPages > 1 ? (
                  <div className="flex items-center gap-1">
                    <Button
                      size="icon"
                      variant="ghost"
                      onClick={() => setPageNumber((p) => Math.max(1, p - 1))}
                      disabled={pageNumber <= 1}
                      aria-label="Previous page"
                    >
                      <ChevronLeft className="h-4 w-4" />
                    </Button>
                    <Button
                      size="icon"
                      variant="ghost"
                      onClick={() => setPageNumber((p) => Math.min(numPages, p + 1))}
                      disabled={pageNumber >= numPages}
                      aria-label="Next page"
                    >
                      <ChevronRight className="h-4 w-4" />
                    </Button>
                  </div>
                ) : null}
              </div>
            </CardHeader>
            <CardContent>
              <div
                ref={pageContainerRef}
                onClick={handlePageClick}
                className="relative inline-block cursor-crosshair select-none"
              >
                <Document
                  file={pdfUrl}
                  onLoadSuccess={({ numPages: n }) => setNumPages(n)}
                  loading={
                    <div className="flex items-center gap-2 p-6 text-sm text-muted-foreground">
                      <Loader2 className="h-4 w-4 animate-spin" />
                      Loading PDF…
                    </div>
                  }
                  onLoadError={(err) => toastError(err)}
                >
                  <Page
                    pageNumber={pageNumber}
                    width={pageWidth}
                    renderAnnotationLayer={false}
                    renderTextLayer={false}
                  />
                </Document>

                {visibleMarkers.map(({ marker, index }) => (
                  <MarkerOverlay
                    key={index}
                    marker={marker}
                    onMouseDown={onMarkerMouseDown(index, 'move')}
                    onResize={(kind) => onMarkerMouseDown(index, kind)}
                    onContextMenu={(e) => {
                      e.preventDefault();
                      removeMarker(index);
                    }}
                  />
                ))}

                {pending && pending.page === pageNumber ? (
                  <div
                    style={{
                      position: 'absolute',
                      left: `${pending.x * 100}%`,
                      top: `${pending.y * 100}%`,
                      width: `${DEFAULT_MARKER_W * 100}%`,
                      height: `${DEFAULT_MARKER_H * 100}%`,
                    }}
                    className="pointer-events-none rounded border-2 border-amber-500 bg-amber-500/15"
                  />
                ) : null}
              </div>
            </CardContent>
          </Card>
        </div>

        <div className="space-y-4">
          {pending ? (
            <Card>
              <CardHeader className="pb-2">
                <CardTitle className="text-sm">New marker</CardTitle>
              </CardHeader>
              <CardContent className="space-y-3">
                <div className="space-y-1">
                  <Label className="text-xs">Merge token</Label>
                  <Select value={pendingToken} onValueChange={setPendingToken}>
                    <SelectTrigger>
                      <SelectValue placeholder="Pick a token…" />
                    </SelectTrigger>
                    <SelectContent className="max-h-72">
                      {TOKEN_OPTIONS.map((t) => (
                        <SelectItem key={t} value={t}>
                          {t}
                        </SelectItem>
                      ))}
                    </SelectContent>
                  </Select>
                </div>
                <div className="flex justify-between gap-2">
                  <Button variant="outline" size="sm" onClick={cancelPending}>
                    <X className="mr-1.5 h-3.5 w-3.5" /> Cancel
                  </Button>
                  <Button size="sm" disabled={!pendingToken} onClick={commitPending}>
                    Add marker
                  </Button>
                </div>
              </CardContent>
            </Card>
          ) : null}

          {unplacedRequired.length > 0 ? (
            <Card>
              <CardHeader className="pb-2">
                <CardTitle className="text-sm text-amber-700">
                  Required tokens unplaced ({unplacedRequired.length})
                </CardTitle>
              </CardHeader>
              <CardContent>
                <ul className="space-y-1 text-xs">
                  {unplacedRequired.map((t) => (
                    <li key={t} className="font-mono text-amber-900">
                      {t}
                    </li>
                  ))}
                </ul>
              </CardContent>
            </Card>
          ) : null}

          <Card>
            <CardHeader className="pb-2">
              <CardTitle className="text-sm">
                Markers on page {pageNumber} ({visibleMarkers.length})
              </CardTitle>
            </CardHeader>
            <CardContent className="space-y-2">
              {visibleMarkers.length === 0 ? (
                <p className="text-xs text-muted-foreground">
                  Click on the PDF to drop your first marker.
                </p>
              ) : (
                visibleMarkers.map(({ marker, index }) => (
                  <div
                    key={index}
                    className="flex items-center justify-between gap-2 rounded border px-2 py-1 text-xs"
                  >
                    <span className="font-mono">{marker.token}</span>
                    <button
                      type="button"
                      onClick={() => removeMarker(index)}
                      className="text-muted-foreground hover:text-destructive"
                      aria-label={`Remove ${marker.token} marker`}
                    >
                      <Trash2 className="h-3.5 w-3.5" />
                    </button>
                  </div>
                ))
              )}
            </CardContent>
          </Card>

          <Card>
            <CardHeader className="pb-2">
              <CardTitle className="text-sm flex items-center gap-1">
                <Eye className="h-3.5 w-3.5" /> Live preview
              </CardTitle>
            </CardHeader>
            <CardContent className="space-y-2">
              <Label className="text-xs">Interest ID</Label>
              <input
                type="text"
                value={previewInterestId}
                onChange={(e) => setPreviewInterestId(e.target.value)}
                placeholder="Paste an interest UUID"
                className="w-full rounded-md border bg-background px-2 py-1 text-xs"
              />
              <Button
                size="sm"
                variant="outline"
                onClick={generatePreview}
                disabled={previewLoading || !previewInterestId}
                className="w-full"
              >
                {previewLoading ? 'Rendering…' : 'Render preview'}
              </Button>
              {previewUrl ? (
                <a
                  href={previewUrl}
                  target="_blank"
                  rel="noreferrer"
                  className="block text-center text-xs text-primary hover:underline"
                >
                  Open preview PDF
                </a>
              ) : null}
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  );
}

function clamp(v: number, lo: number, hi: number): number {
  return Math.min(hi, Math.max(lo, v));
}

function MarkerOverlay({
  marker,
  onMouseDown,
  onResize,
  onContextMenu,
}: {
  marker: FieldMapEntry;
  onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => void;
  onResize: (kind: DragKind) => (e: React.MouseEvent<HTMLDivElement>) => void;
  onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void;
}) {
  const corners: Array<{ kind: DragKind; pos: string; cursor: string }> = [
    {
      kind: 'resize-nw',
      pos: 'top-0 left-0 -translate-x-1/2 -translate-y-1/2',
      cursor: 'nwse-resize',
    },
    {
      kind: 'resize-ne',
      pos: 'top-0 right-0 translate-x-1/2 -translate-y-1/2',
      cursor: 'nesw-resize',
    },
    {
      kind: 'resize-sw',
      pos: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2',
      cursor: 'nesw-resize',
    },
    {
      kind: 'resize-se',
      pos: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2',
      cursor: 'nwse-resize',
    },
  ];
  return (
    <div
      data-marker-handle="true"
      style={{
        position: 'absolute',
        left: `${marker.x * 100}%`,
        top: `${marker.y * 100}%`,
        width: `${(marker.w ?? DEFAULT_MARKER_W) * 100}%`,
        height: `${(marker.h ?? DEFAULT_MARKER_H) * 100}%`,
      }}
      className="cursor-move rounded border-2 border-primary/70 bg-primary/15 px-1 py-0.5 text-xs font-medium text-primary"
      onMouseDown={onMouseDown}
      onContextMenu={onContextMenu}
    >
      <span className="pointer-events-none select-none">{marker.token}</span>
      {corners.map((c) => (
        <div
          key={c.kind}
          data-marker-handle="true"
          onMouseDown={onResize(c.kind)}
          className={`absolute h-2 w-2 rounded-sm bg-primary ${c.pos}`}
          style={{ cursor: c.cursor }}
        />
      ))}
    </div>
  );
}
