import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { PAPERS, PHOTO_SIZES, parseSizeMm, labelMm, mmToPx, paperById, photoById } from '../lib/paper';
import {
  layout, cutMarks, recommendedGap, dpiCheck,
  type SheetSpec, type PlaceSpec, type MarkMode,
} from '../lib/printlayout';

/* --------------------------------------------------------------------------
   Druck — Bilder ohne KI auf exakte Maße bringen und mehrere davon mit
   Schnittmarken auf einen Bogen setzen. Alles rechnet in Millimetern;
   Pixel entstehen erst beim Rendern auf dem Server.
   -------------------------------------------------------------------------- */

type SrcRef = { kind: 'upload'; path: string } | { kind: 'item'; id: string };
interface CropRel { x: number; y: number; w: number; h: number }

interface Cell {
  id: string;
  name: string;
  preview: string;             // Data-URL (Upload) oder /api/items/…/file
  src?: SrcRef;
  uploading?: boolean;
  error?: string;
  natW: number; natH: number;  // echte Quellpixel (für den dpi-Hinweis)
  sizeId: string;              // Schlüssel aus PHOTO_SIZES oder 'custom'
  customSize: string;
  landscape: boolean;
  count: number;
  allowRotate: boolean;
  crop: CropRel;
}

const uid = () => (crypto.randomUUID ? crypto.randomUUID() : String(Math.random()).slice(2));

/** Größter Ausschnitt mit dem Zielseitenverhältnis, mittig („Bild füllen"). */
function coverCrop(natW: number, natH: number, aspect: number, around?: CropRel): CropRel {
  const imgA = natW / natH;
  let w = 1, h = 1;
  if (imgA > aspect) { h = 1; w = (aspect / imgA); } else { w = 1; h = (imgA / aspect); }
  const cx = around ? around.x + around.w / 2 : 0.5;
  const cy = around ? around.y + around.h / 2 : 0.5;
  return clampCrop({ x: cx - w / 2, y: cy - h / 2, w, h });
}
function clampCrop(c: CropRel): CropRel {
  const w = Math.min(1, Math.max(0.02, c.w));
  const h = Math.min(1, Math.max(0.02, c.h));
  return { w, h, x: Math.min(1 - w, Math.max(0, c.x)), y: Math.min(1 - h, Math.max(0, c.y)) };
}

const sizeOfCell = (c: Cell): { w: number; h: number } | null => {
  const base = c.sizeId === 'custom' ? parseSizeMm(c.customSize) : photoById(c.sizeId);
  if (!base) return null;
  const s = { w: base.w, h: base.h };
  return c.landscape ? { w: s.h, h: s.w } : s;
};

export default function PrintApp() {
  const [cells, setCells] = useState<Cell[]>([]);
  const [paperId, setPaperId] = useState('A4');
  const [paperCustom, setPaperCustom] = useState('');
  const [paperLandscape, setPaperLandscape] = useState(false);
  const [marginMm, setMarginMm] = useState(5);
  const [autoGap, setAutoGap] = useState(true);
  const [gapMm, setGapMm] = useState(12);
  const [bleedMm, setBleedMm] = useState(0);
  const [marks, setMarks] = useState<MarkMode>('corner');
  const [markLen, setMarkLen] = useState(4);
  const [markOff, setMarkOff] = useState(3);
  const [dpi, setDpi] = useState(300);
  const [ext, setExt] = useState<'jpg' | 'png'>('jpg');
  const [center, setCenter] = useState(true);
  const [footer, setFooter] = useState(true);

  const [editing, setEditing] = useState<string | null>(null);
  const [picker, setPicker] = useState(false);
  const [presets, setPresets] = useState<any[]>([]);
  const [presetSel, setPresetSel] = useState('');
  const [busy, setBusy] = useState<null | 'pdf' | 'single'>(null);
  const [toast, setToast] = useState<string | null>(null);
  const [dragging, setDragging] = useState(false);
  const fileRef = useRef<HTMLInputElement>(null);

  const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 3000); };

  /* ---------------- Bogen ---------------- */
  const paper = useMemo(() => {
    const p = paperId === 'custom' ? parseSizeMm(paperCustom) : paperById(paperId);
    if (!p) return null;
    return paperLandscape ? { w: p.h, h: p.w } : { w: p.w, h: p.h };
  }, [paperId, paperCustom, paperLandscape]);

  const gapEff = autoGap ? Math.max(recommendedGap(marks, bleedMm, markLen, markOff), 2) : gapMm;

  const sheet: SheetSpec | null = paper
    ? { wMm: paper.w, hMm: paper.h, marginMm, gapMm: gapEff, bleedMm, center }
    : null;

  const specs: PlaceSpec[] = useMemo(() => cells.flatMap((c) => {
    const s = sizeOfCell(c);
    if (!s || c.error || !c.src) return [];
    return [{ id: c.id, wMm: s.w, hMm: s.h, count: c.count, allowRotate: c.allowRotate }];
  }), [cells]);

  const plan = useMemo(() => (sheet && specs.length ? layout(specs, sheet) : null), [sheet, specs]);
  const marksPerPage = useMemo(() => (plan && sheet)
    ? plan.pages.map((pg) => cutMarks(pg, sheet, { mode: marks, lengthMm: markLen, offsetMm: markOff, bleedMm }))
    : [], [plan, sheet, marks, markLen, markOff, bleedMm]);

  /* ---------------- Upload ---------------- */
  const addFiles = useCallback(async (files: FileList | File[]) => {
    const arr = Array.from(files)
      .filter((f) => f.type.startsWith('image/') || /\.(heic|heif)$/i.test(f.name))
      .slice(0, 30);
    for (const f of arr) {
      const id = uid();
      const reader = new FileReader();
      reader.onload = () => {
        const url = reader.result as string;
        const probe = new Image();
        probe.onload = () => setCells((p) => p.map((x) => x.id === id
          ? { ...x, preview: url, natW: probe.naturalWidth, natH: probe.naturalHeight,
              crop: coverCrop(probe.naturalWidth, probe.naturalHeight, aspectOf(x)) }
          : x));
        probe.src = url;
      };
      reader.readAsDataURL(f);
      setCells((p) => [...p, newCell(id, f.name)]);
      const fd = new FormData(); fd.append('files', f);
      try {
        const j = await fetch('/api/uploads', { method: 'POST', body: fd }).then((r) => r.json());
        const info = j.files?.[0];
        setCells((p) => p.map((x) => x.id === id ? {
          ...x, uploading: false, error: info?.error,
          src: info?.source_path ? { kind: 'upload', path: info.source_path } : undefined,
          natW: info?.width || x.natW, natH: info?.height || x.natH,
        } : x));
      } catch {
        setCells((p) => p.map((x) => x.id === id ? { ...x, uploading: false, error: 'Upload fehlgeschlagen' } : x));
      }
    }
  }, []);

  useEffect(() => {
    const onPaste = (e: ClipboardEvent) => {
      const imgs = Array.from(e.clipboardData?.items || []).filter((i) => i.type.startsWith('image/'));
      if (imgs.length) addFiles(imgs.map((i) => i.getAsFile()!).filter(Boolean));
    };
    window.addEventListener('paste', onPaste);
    return () => window.removeEventListener('paste', onPaste);
  }, [addFiles]);

  useEffect(() => { loadPresets(); }, []);
  const loadPresets = () => fetch('/api/print/presets').then((r) => r.json())
    .then((j) => setPresets(j.presets || [])).catch(() => {});

  const addFromLibrary = (it: any) => {
    const [w, h] = String(it.output_px || '').split(/[x×]/).map((n: string) => parseInt(n, 10) || 0);
    const id = uid();
    const c: Cell = {
      ...newCell(id, it.filename || 'Bild'),
      uploading: false,
      src: { kind: 'item', id: it.id },
      preview: `/api/items/${it.id}/file?preview=1`,
      natW: w || 2000, natH: h || 3000,
    };
    c.crop = coverCrop(c.natW, c.natH, aspectOf(c));
    setCells((p) => [...p, c]);
  };

  const patch = (id: string, up: Partial<Cell>) => setCells((p) => p.map((c) => {
    if (c.id !== id) return c;
    const next = { ...c, ...up };
    // Formatwechsel → Ausschnitt auf das neue Seitenverhältnis nachziehen.
    if (up.sizeId !== undefined || up.customSize !== undefined || up.landscape !== undefined) {
      const a = aspectOf(next);
      if (a) next.crop = coverCrop(next.natW, next.natH, a, c.crop);
    }
    return next;
  }));

  /* ---------------- Ausgabe ---------------- */
  const body = () => ({
    paper: paperId === 'custom' ? { size: paperCustom } : { id: paperId },
    landscape: paperLandscape,
    marginMm, gapMm: gapEff, bleedMm, center, dpi, ext, footer,
    marks: { mode: marks, lengthMm: markLen, offsetMm: markOff },
    cells: cells.filter((c) => c.src && !c.error && sizeOfCell(c)).map((c) => {
      const s = sizeOfCell(c)!;
      return { id: c.id, src: c.src, crop: c.crop, wMm: s.w, hMm: s.h, count: c.count, allowRotate: c.allowRotate };
    }),
  });

  const makePdf = async () => {
    if (!plan?.pages.length) return;
    setBusy('pdf');
    try {
      const res = await fetch('/api/print/sheet', {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body()),
      });
      if (!res.ok) { notify((await res.json().catch(() => ({}))).error || 'Fehlgeschlagen.'); return; }
      const blob = await res.blob();
      download(blob, `klarbild-druckbogen-${new Date().toISOString().slice(0, 10)}.pdf`);
      notify(`PDF erzeugt · ${res.headers.get('X-Klarbild-Pages') || '?'} Bogen. Beim Drucken „Tatsächliche Größe / 100 %" wählen.`);
    } catch { notify('Netzwerkfehler.'); } finally { setBusy(null); }
  };

  const makeSingle = async (c: Cell) => {
    const s = sizeOfCell(c); if (!s || !c.src) return;
    setBusy('single');
    try {
      const res = await fetch('/api/print/single', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ src: c.src, crop: c.crop, wMm: s.w, hMm: s.h, dpi, ext, name: c.name }),
      });
      if (!res.ok) { notify((await res.json().catch(() => ({}))).error || 'Fehlgeschlagen.'); return; }
      const blob = await res.blob();
      download(blob, `${(c.name || 'bild').replace(/\.[^.]+$/, '')}_${labelMm(s.w, s.h).replace(/[^0-9]+/g, 'x')}.${ext}`);
      const real = res.headers.get('X-Klarbild-Real-Dpi');
      notify(res.headers.get('X-Klarbild-Dpi-Ok') === '0'
        ? `Heruntergeladen — Achtung: die Quelle reicht nur für ca. ${real} dpi.`
        : `Heruntergeladen · ${res.headers.get('X-Klarbild-Px')} px bei ${dpi} dpi.`);
    } catch { notify('Netzwerkfehler.'); } finally { setBusy(null); }
  };

  const savePreset = async () => {
    const name = prompt('Name der Vorlage? (z. B. „Kita-Satz Felix")');
    if (!name?.trim()) return;
    const config = {
      paperId, paperCustom, paperLandscape, marginMm, autoGap, gapMm, bleedMm,
      marks, markLen, markOff, dpi, ext, center, footer,
      formats: cells.map((c) => ({ sizeId: c.sizeId, customSize: c.customSize, landscape: c.landscape, count: c.count, allowRotate: c.allowRotate })),
    };
    await fetch('/api/print/presets', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: name.trim(), config }),
    });
    notify('Vorlage gespeichert.'); loadPresets();
  };

  const applyPreset = (p: any) => {
    const c = p?.config; if (!c) return;
    setPaperId(c.paperId ?? 'A4'); setPaperCustom(c.paperCustom ?? ''); setPaperLandscape(!!c.paperLandscape);
    setMarginMm(c.marginMm ?? 5); setAutoGap(c.autoGap !== false); setGapMm(c.gapMm ?? 12);
    setBleedMm(c.bleedMm ?? 0); setMarks(c.marks ?? 'corner'); setMarkLen(c.markLen ?? 4); setMarkOff(c.markOff ?? 3);
    setDpi(c.dpi ?? 300); setExt(c.ext === 'png' ? 'png' : 'jpg'); setCenter(c.center !== false); setFooter(c.footer !== false);
    // Formate auf die vorhandenen Bilder anwenden (der Reihe nach).
    if (Array.isArray(c.formats) && c.formats.length) {
      setCells((prev) => prev.map((cell, i) => {
        const f = c.formats[Math.min(i, c.formats.length - 1)];
        const next = { ...cell, sizeId: f.sizeId, customSize: f.customSize || '', landscape: !!f.landscape, count: f.count || 1, allowRotate: f.allowRotate !== false };
        const a = aspectOf(next); if (a) next.crop = coverCrop(next.natW, next.natH, a, cell.crop);
        return next;
      }));
    }
    notify('Vorlage geladen.');
  };

  const deletePreset = async (id: string) => {
    if (!id || !confirm('Vorlage löschen?')) return;
    await fetch(`/api/print/presets?id=${id}`, { method: 'DELETE' });
    setPresetSel(''); loadPresets();
  };

  const editCell = cells.find((c) => c.id === editing) || null;
  const ready = cells.filter((c) => c.src && !c.error && sizeOfCell(c));
  const totalPieces = ready.reduce((n, c) => n + c.count, 0);

  return (
    <div className="druck">
      {/* ---------- 1 · Bilder ---------- */}
      <section className="karte">
        <div className="kopfzeile">
          <span className="mono-label">1 · Bilder</span>
          <div className="kopf-tools">
            <button className="link" onClick={() => setPicker(true)}>Aus Bibliothek</button>
            {cells.length > 0 && <button className="link" onClick={() => setCells([])}>Alle entfernen</button>}
          </div>
        </div>
        <div className="buehne">
          {cells.length === 0 ? (
            <div className={`drop ${dragging ? 'drag' : ''}`}
              onClick={() => fileRef.current?.click()}
              onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
              onDragLeave={() => setDragging(false)}
              onDrop={(e) => { e.preventDefault(); setDragging(false); addFiles(e.dataTransfer.files); }}>
              <span className="regmark" />
              <p>Bilder hierher ziehen, <span className="kbd">⌘V</span> einfügen oder klicken.<br />
                Kein KI-Schritt — nur zuschneiden, skalieren, setzen.</p>
            </div>
          ) : (
            <div className="zellen">
              {cells.map((c) => <CellCard key={c.id} cell={c} dpi={dpi}
                onPatch={(u) => patch(c.id, u)}
                onEdit={() => setEditing(c.id)}
                onSingle={() => makeSingle(c)}
                onRemove={() => setCells((p) => p.filter((x) => x.id !== c.id))} />)}
              <button className="zelle-add" onClick={() => fileRef.current?.click()}>＋ Bild</button>
            </div>
          )}
          <input ref={fileRef} type="file" accept="image/*,.heic,.heif" multiple hidden
            onChange={(e) => e.target.files && addFiles(e.target.files)} />
        </div>
      </section>

      <div className="raster">
        {/* ---------- 2 · Bogen ---------- */}
        <section className="karte">
          <div className="kopfzeile"><span className="mono-label">2 · Bogen &amp; Schnitt</span>
            <button className="link" onClick={savePreset}>Als Vorlage sichern</button>
          </div>
          <div className="steuer">
            <div className="feld">
              <label>Vorlagen</label>
              <div className="reihe">
                <select className="select" value={presetSel}
                  onChange={(e) => { setPresetSel(e.target.value); applyPreset(presets.find((p) => p.id === e.target.value)); }}>
                  <option value="">Vorlage wählen …</option>
                  {presets.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
                </select>
                {presetSel && <button className="pbtn" onClick={() => deletePreset(presetSel)}>✕</button>}
              </div>
            </div>

            <div className="feld">
              <label>Papierformat</label>
              <select className="select" value={paperId} onChange={(e) => setPaperId(e.target.value)}>
                {groupBy(PAPERS, 'group').map(([g, list]) => (
                  <optgroup key={g} label={g}>
                    {list.map((p) => <option key={p.id} value={p.id}>{p.label} · {p.w} × {p.h} mm</option>)}
                  </optgroup>
                ))}
                <option value="custom">Eigenes Papiermaß …</option>
              </select>
              {paperId === 'custom' && (
                <input className="input" value={paperCustom} onChange={(e) => setPaperCustom(e.target.value)}
                  placeholder="z. B. 32,9x48,3 (cm) oder 329x483mm" />
              )}
              <div className="schalter">
                <button className={!paperLandscape ? 'an' : ''} onClick={() => setPaperLandscape(false)}>Hoch</button>
                <button className={paperLandscape ? 'an' : ''} onClick={() => setPaperLandscape(true)}>Quer</button>
              </div>
              {paper && <div className="fein">Bogen {labelMm(paper.w, paper.h)} — die PDF-Seite hat exakt dieses Maß.</div>}
            </div>

            <div className="feld">
              <label>Schnitthilfen</label>
              <div className="schalter">
                <button className={marks === 'none' ? 'an' : ''} onClick={() => setMarks('none')}>Keine</button>
                <button className={marks === 'corner' ? 'an' : ''} onClick={() => setMarks('corner')}>Eckmarken</button>
                <button className={marks === 'grid' ? 'an' : ''} onClick={() => setMarks('grid')}>Durchgehend</button>
              </div>
              <div className="fein">
                {marks === 'corner' ? 'Feine Marken an den vier Ecken jedes Bildes, außerhalb des Endformats — wie in Photoshop/InDesign.'
                  : marks === 'grid' ? 'Durchgehende Hilfslinien über den ganzen Bogen — für Schneidelineal und Schlagschere.'
                  : 'Keine Linien — Kanten selbst anlegen.'}
              </div>
              {marks === 'corner' && (
                <div className="reihe zwei">
                  <label className="mini">Länge (mm)
                    <input className="input" type="number" min={1} max={20} step={0.5} value={markLen}
                      onChange={(e) => setMarkLen(Number(e.target.value))} /></label>
                  <label className="mini">Abstand (mm)
                    <input className="input" type="number" min={0} max={20} step={0.5} value={markOff}
                      onChange={(e) => setMarkOff(Number(e.target.value))} /></label>
                </div>
              )}
            </div>

            <div className="feld">
              <label>Ränder &amp; Abstände</label>
              <div className="reihe zwei">
                <label className="mini">Rand zum Papier (mm)
                  <input className="input" type="number" min={0} max={40} step={0.5} value={marginMm}
                    onChange={(e) => setMarginMm(Number(e.target.value))} /></label>
                <label className="mini">Beschnittzugabe (mm)
                  <input className="input" type="number" min={0} max={10} step={0.5} value={bleedMm}
                    onChange={(e) => setBleedMm(Number(e.target.value))} /></label>
              </div>
              <label className="check">
                <input type="checkbox" checked={autoGap} onChange={(e) => setAutoGap(e.target.checked)} />
                <span>Abstand automatisch ({gapEff.toFixed(1)} mm — passend zu den Marken)</span>
              </label>
              {!autoGap && (
                <input className="input" type="number" min={0} max={60} step={0.5} value={gapMm}
                  onChange={(e) => setGapMm(Number(e.target.value))} />
              )}
              <label className="check">
                <input type="checkbox" checked={center} onChange={(e) => setCenter(e.target.checked)} />
                <span>Auf dem Bogen zentrieren</span>
              </label>
              <div className="fein">Randlos druckbare Drucker: Rand 0 setzen. Sonst 3–5 mm — sonst schneidet der Drucker Marken weg.</div>
            </div>

            <div className="feld">
              <label>Qualität</label>
              <div className="reihe zwei">
                <label className="mini">Auflösung (dpi)
                  <select className="select" value={dpi} onChange={(e) => setDpi(Number(e.target.value))}>
                    <option value={150}>150 — Entwurf</option>
                    <option value={300}>300 — Fotodruck (Standard)</option>
                    <option value={600}>600 — sehr fein</option>
                  </select></label>
                <label className="mini">Dateiformat im PDF
                  <select className="select" value={ext} onChange={(e) => setExt(e.target.value as any)}>
                    <option value="jpg">JPG — kleiner</option>
                    <option value="png">PNG — verlustfrei</option>
                  </select></label>
              </div>
              <label className="check">
                <input type="checkbox" checked={footer} onChange={(e) => setFooter(e.target.checked)} />
                <span>Fußzeile mit Maßen &amp; Druckhinweis</span>
              </label>
            </div>

            <button className="knopf" disabled={!plan?.pages.length || busy === 'pdf'} onClick={makePdf}>
              {busy === 'pdf' ? <><span className="spin" />PDF wird gebaut …</> : 'Druck-PDF erzeugen'}
            </button>
            <div className="fein mitte">Im Druckdialog <b>„Tatsächliche Größe" / 100 %</b> wählen — nicht „an Seitengröße anpassen".</div>
          </div>
        </section>

        {/* ---------- 3 · Vorschau ---------- */}
        <section className="karte">
          <div className="kopfzeile">
            <span className="mono-label">3 · Vorschau</span>
            {plan && <span className="fein nopad">{plan.pages.length} Bogen · {totalPieces} Bilder</span>}
          </div>
          <div className="vorschau">
            {!sheet ? <p className="leer">Bitte ein gültiges Papiermaß angeben.</p>
              : !plan || !plan.pages.length ? <p className="leer">Noch nichts zu zeigen — Bilder hinzufügen und Formate wählen.</p>
              : plan.pages.map((pg, i) => (
                <div key={i} className="bogen-halter">
                  <div className="bogen" style={{ aspectRatio: `${sheet.wMm} / ${sheet.hMm}` }}>
                    {pg.placements.map((p, k) => {
                      const c = cells.find((x) => x.id === p.specId);
                      if (!c) return null;
                      return (
                        <div key={k} className="slot" style={{
                          left: `${(p.x / sheet.wMm) * 100}%`, top: `${(p.y / sheet.hMm) * 100}%`,
                          width: `${(p.w / sheet.wMm) * 100}%`, height: `${(p.h / sheet.hMm) * 100}%`,
                        }}>
                          <div className="rotor" style={p.rotated ? {
                            width: `${(p.h / p.w) * 100}%`, height: `${(p.w / p.h) * 100}%`,
                            transform: 'translate(-50%, -50%) rotate(90deg)',
                          } : undefined}>
                            <img src={c.preview} alt="" style={cropStyle(c.crop)} />
                          </div>
                        </div>
                      );
                    })}
                    <svg className="marken" viewBox={`0 0 ${sheet.wMm} ${sheet.hMm}`} preserveAspectRatio="none">
                      {(marksPerPage[i] || []).map((l, k) => (
                        <line key={k} x1={l.x1} y1={l.y1} x2={l.x2} y2={l.y2}
                          stroke="#16150F" strokeWidth={0.25} vectorEffect="non-scaling-stroke" />
                      ))}
                    </svg>
                  </div>
                  <div className="bogen-fuss">Bogen {i + 1} · {pg.placements.length} Bilder</div>
                </div>
              ))}
            {plan?.unplaced?.length ? (
              <p className="warnung">Nicht platzierbar: {plan.unplaced.map((u) => {
                const c = cells.find((x) => x.id === u.specId);
                return `${c?.name || u.specId} (${u.reason})`;
              }).join(', ')}</p>
            ) : null}
          </div>
        </section>
      </div>

      {editCell && <CropModal cell={editCell} onClose={() => setEditing(null)}
        onChange={(crop) => patch(editCell.id, { crop })} />}
      {picker && <LibraryPicker onClose={() => setPicker(false)} onPick={(it) => { addFromLibrary(it); setPicker(false); }} />}
      {toast && <div className="toast">{toast}</div>}
      <PrintStyles />
    </div>
  );
}

/* ---------------- Bausteine ---------------- */

function newCell(id: string, name: string): Cell {
  return {
    id, name, preview: '', uploading: true, natW: 1000, natH: 1500,
    sizeId: 'S10x15', customSize: '', landscape: false, count: 1, allowRotate: true,
    crop: { x: 0, y: 0, w: 1, h: 1 },
  };
}
function aspectOf(c: Cell): number {
  const s = sizeOfCell(c);
  return s ? s.w / s.h : 1;
}
function cropStyle(c: CropRel): React.CSSProperties {
  return {
    position: 'absolute',
    width: `${100 / c.w}%`, height: `${100 / c.h}%`,
    left: `${(-c.x / c.w) * 100}%`, top: `${(-c.y / c.h) * 100}%`,
    objectFit: 'fill',
  };
}
function groupBy<T extends { group: string }>(list: T[], _k: 'group'): [string, T[]][] {
  const m = new Map<string, T[]>();
  for (const x of list) { if (!m.has(x.group)) m.set(x.group, []); m.get(x.group)!.push(x); }
  return [...m.entries()];
}
function download(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 4000);
}

function CellCard({ cell, dpi, onPatch, onEdit, onSingle, onRemove }: {
  cell: Cell; dpi: number;
  onPatch: (u: Partial<Cell>) => void; onEdit: () => void; onSingle: () => void; onRemove: () => void;
}) {
  const s = sizeOfCell(cell);
  const check = s ? dpiCheck(cell.crop.w * cell.natW, s.w, dpi) : null;
  return (
    <div className={`zelle ${cell.error ? 'err' : ''}`}>
      <button className="zelle-x" onClick={onRemove} aria-label="Entfernen">✕</button>
      <div className="zelle-bild" style={{ aspectRatio: s ? `${s.w} / ${s.h}` : '3 / 4' }}
        onClick={() => !cell.uploading && onEdit()}>
        {cell.preview
          ? <img src={cell.preview} alt="" style={cropStyle(cell.crop)} />
          : <span className="lade" />}
        {!cell.uploading && <span className="zelle-lupe">Zuschnitt</span>}
      </div>
      <div className="zelle-steuer">
        <select className="select mini-select" value={cell.sizeId} onChange={(e) => onPatch({ sizeId: e.target.value })}>
          {groupBy(PHOTO_SIZES, 'group').map(([g, list]) => (
            <optgroup key={g} label={g}>
              {list.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
            </optgroup>
          ))}
          <option value="custom">Eigenes Maß …</option>
        </select>
        {cell.sizeId === 'custom' && (
          <input className="input mini-input" value={cell.customSize} onChange={(e) => onPatch({ customSize: e.target.value })}
            placeholder="12x15 · 35x45mm · 4:3/15" />
        )}
        <div className="zelle-reihe">
          <div className="schalter winzig">
            <button className={!cell.landscape ? 'an' : ''} onClick={() => onPatch({ landscape: false })}>Hoch</button>
            <button className={cell.landscape ? 'an' : ''} onClick={() => onPatch({ landscape: true })}>Quer</button>
          </div>
          <div className="anzahl">
            <button onClick={() => onPatch({ count: Math.max(1, cell.count - 1) })}>−</button>
            <input type="number" min={1} max={200} value={cell.count}
              onChange={(e) => onPatch({ count: Math.min(200, Math.max(1, Number(e.target.value) || 1)) })} />
            <button onClick={() => onPatch({ count: Math.min(200, cell.count + 1) })}>+</button>
          </div>
        </div>
        <label className="check winzig">
          <input type="checkbox" checked={cell.allowRotate} onChange={(e) => onPatch({ allowRotate: e.target.checked })} />
          <span>darf gedreht platziert werden</span>
        </label>
        {cell.error ? <div className="fein warn">{cell.error}</div> : s && (
          <div className={`fein ${check && !check.ok ? 'warn' : ''}`}>
            {labelMm(s.w, s.h)} = {mmToPx(s.w, dpi)} × {mmToPx(s.h, dpi)} px
            {check && (check.ok ? ' · Auflösung reicht' : ` · nur ca. ${check.dpi} dpi`)}
          </div>
        )}
        <button className="pbtn breit" disabled={!cell.src} onClick={onSingle}>Einzeln herunterladen</button>
      </div>
    </div>
  );
}

/** Zuschnitt: schieben und zoomen, Seitenverhältnis bleibt fest am Zielformat. */
function CropModal({ cell, onChange, onClose }: { cell: Cell; onChange: (c: CropRel) => void; onClose: () => void }) {
  const s = sizeOfCell(cell);
  const aspect = s ? s.w / s.h : 1;
  const base = coverCrop(cell.natW, cell.natH, aspect);
  const [crop, setCrop] = useState<CropRel>(cell.crop);
  const boxRef = useRef<HTMLDivElement>(null);
  const drag = useRef<{ x: number; y: number; crop: CropRel } | null>(null);

  const zoom = Math.min(20, Math.max(1, base.w / crop.w));
  const setZoom = (z: number) => {
    const zz = Math.min(20, Math.max(1, z));
    const cx = crop.x + crop.w / 2, cy = crop.y + crop.h / 2;
    const w = base.w / zz, h = base.h / zz;
    setCrop(clampCrop({ x: cx - w / 2, y: cy - h / 2, w, h }));
  };

  const onDown = (e: React.PointerEvent) => {
    (e.target as Element).setPointerCapture?.(e.pointerId);
    drag.current = { x: e.clientX, y: e.clientY, crop };
  };
  const onMove = (e: React.PointerEvent) => {
    if (!drag.current || !boxRef.current) return;
    const r = boxRef.current.getBoundingClientRect();
    const dx = (e.clientX - drag.current.x) / r.width;
    const dy = (e.clientY - drag.current.y) / r.height;
    const c = drag.current.crop;
    setCrop(clampCrop({ ...c, x: c.x - dx * c.w, y: c.y - dy * c.h }));
  };
  const onUp = () => { drag.current = null; };

  useEffect(() => {
    const esc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', esc);
    return () => window.removeEventListener('keydown', esc);
  }, [onClose]);

  const apply = () => { onChange(crop); onClose(); };

  return (
    <div className="modal" onClick={onClose}>
      <div className="modal-inhalt" onClick={(e) => e.stopPropagation()}>
        <div className="kopfzeile">
          <span className="mono-label">Zuschnitt · {s ? labelMm(s.w, s.h) : ''}</span>
          <button className="link" onClick={onClose}>Schließen</button>
        </div>
        <div className="crop-buehne">
          <div ref={boxRef} className="crop-box" style={{ aspectRatio: `${aspect}` }}
            onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}
            onWheel={(e) => { setZoom(zoom * (e.deltaY < 0 ? 1.08 : 1 / 1.08)); }}>
            <img src={cell.preview} alt="" draggable={false} style={cropStyle(crop)} />
            <div className="crop-gitter" />
          </div>
        </div>
        <div className="crop-steuer">
          <label className="mini breit">Zoom
            <input type="range" min={1} max={6} step={0.01} value={Math.min(6, zoom)}
              onChange={(e) => setZoom(Number(e.target.value))} />
          </label>
          <div className="reihe">
            <button className="pbtn" onClick={() => setCrop(base)}>Bild füllen</button>
            <button className="pbtn" onClick={() => setCrop(clampCrop({ ...crop, x: (1 - crop.w) / 2, y: (1 - crop.h) / 2 }))}>Zentrieren</button>
            <button className="knopf schmal" onClick={apply}>Übernehmen</button>
          </div>
          <div className="fein">Ziehen zum Verschieben, Mausrad oder Regler zum Zoomen. Das Seitenverhältnis bleibt exakt am Zielformat.</div>
        </div>
      </div>
    </div>
  );
}

function LibraryPicker({ onPick, onClose }: { onPick: (it: any) => void; onClose: () => void }) {
  const [items, setItems] = useState<any[]>([]);
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    fetch('/api/items').then((r) => r.json())
      .then((j) => setItems(j.items || [])).catch(() => {}).finally(() => setLoading(false));
  }, []);
  return (
    <div className="modal" onClick={onClose}>
      <div className="modal-inhalt breit" onClick={(e) => e.stopPropagation()}>
        <div className="kopfzeile">
          <span className="mono-label">Aus der Bibliothek wählen</span>
          <button className="link" onClick={onClose}>Schließen</button>
        </div>
        <div className="picker">
          {loading ? <p className="leer">Wird geladen …</p>
            : !items.length ? <p className="leer">Noch keine Bilder in der Bibliothek.</p>
            : items.map((it) => (
              <button key={it.id} className="pick" onClick={() => onPick(it)} title={it.filename}>
                <img src={`/api/items/${it.id}/file?thumb=1`} alt="" loading="lazy" />
                <span>{it.filename}</span>
              </button>
            ))}
        </div>
      </div>
    </div>
  );
}

function PrintStyles() {
  return <style>{`
    .druck{display:flex;flex-direction:column;gap:16px;}
    .raster{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px;align-items:start;}
    @media(max-width:900px){.raster{grid-template-columns:1fr;}}
    .karte{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);}
    .kopfzeile{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--line);}
    .kopf-tools{display:flex;gap:12px;}
    .link{background:none;border:none;cursor:pointer;color:var(--accent);font-family:var(--font-mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;padding:0;}
    .buehne{padding:16px;}
    .drop{width:100%;min-height:180px;border:1.5px dashed var(--line);border-radius:var(--radius-sm);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;text-align:center;padding:26px;cursor:pointer;}
    .drop.drag{border-color:var(--accent);background:var(--accent-bg);}
    .drop p{margin:0;color:var(--soft);font-size:14px;line-height:1.55;}
    .kbd{font-family:var(--font-mono);font-size:12px;background:var(--paper);border:1px solid var(--line);border-radius:3px;padding:1px 6px;}

    .zellen{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:12px;}
    .zelle{position:relative;border:1px solid var(--line);border-radius:var(--radius-sm);background:#fff;padding:9px;display:flex;flex-direction:column;gap:8px;}
    .zelle.err{border-color:var(--err);}
    .zelle-x{position:absolute;top:5px;right:5px;z-index:3;width:22px;height:22px;border:none;border-radius:50%;background:rgba(22,21,15,.72);color:#fff;cursor:pointer;font-size:13px;line-height:1;}
    .zelle-bild{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:3px;cursor:zoom-in;
      background:repeating-conic-gradient(oklch(90% 0.006 95) 0% 25%, oklch(96% 0.004 95) 0% 50%) 50%/14px 14px;}
    .zelle-bild img{display:block;}
    .zelle-lupe{position:absolute;left:0;right:0;bottom:0;background:rgba(22,21,15,.62);color:#fff;font-family:var(--font-mono);font-size:9px;letter-spacing:.14em;text-transform:uppercase;text-align:center;padding:3px 0;}
    .lade{position:absolute;inset:0;background:var(--paper);opacity:.6;}
    .zelle-steuer{display:flex;flex-direction:column;gap:6px;}
    .zelle-reihe{display:flex;gap:6px;align-items:center;}
    .zelle-add{border:1.5px dashed var(--line);background:none;border-radius:var(--radius-sm);cursor:pointer;font-size:14px;color:var(--soft);min-height:120px;font-family:inherit;}

    .anzahl{display:flex;align-items:center;border:1px solid var(--line);border-radius:3px;overflow:hidden;background:#fff;}
    .anzahl button{width:28px;height:30px;border:none;background:none;cursor:pointer;font-size:15px;color:var(--ink);}
    .anzahl input{width:44px;height:30px;border:none;text-align:center;font-family:var(--font-mono);font-size:13px;outline:none;-moz-appearance:textfield;}
    .anzahl input::-webkit-outer-spin-button,.anzahl input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0;}

    .steuer{padding:16px;display:flex;flex-direction:column;gap:15px;}
    .feld label{display:block;font-family:var(--font-mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--soft);margin-bottom:7px;}
    .input,.select{width:100%;background:#fff;border:1px solid var(--line);border-radius:3px;padding:9px 11px;font-family:inherit;font-size:14px;color:var(--ink);outline:none;}
    .mini-select,.mini-input{padding:6px 8px;font-size:12.5px;}
    .input:focus,.select:focus{border-color:var(--accent);}
    .reihe{display:flex;gap:6px;align-items:center;margin-top:6px;}
    .reihe .select{flex:1;}
    .reihe.zwei{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
    .mini{display:block;font-family:var(--font-mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--soft);}
    .mini .input,.mini .select{margin-top:4px;}
    .mini.breit{width:100%;}
    .mini input[type=range]{width:100%;accent-color:var(--accent);margin-top:6px;}
    .fein{font-size:11.5px;color:var(--soft);margin-top:6px;line-height:1.5;}
    .fein.nopad{margin:0;}
    .fein.mitte{text-align:center;}
    .fein.warn{color:var(--err);}
    .check{display:flex;gap:8px;align-items:flex-start;margin-top:8px;font-size:12.5px;color:var(--ink);text-transform:none;letter-spacing:0;font-family:inherit;}
    .check input{width:15px;height:15px;accent-color:var(--accent);margin-top:2px;flex:0 0 auto;}
    .check.winzig{font-size:11px;color:var(--soft);}
    .schalter{display:flex;gap:6px;margin-top:8px;}
    .schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
    .schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
    .schalter.winzig{margin:0;flex:1;}
    .schalter.winzig button{padding:5px 4px;font-size:11.5px;}
    .pbtn{background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px 10px;cursor:pointer;font-family:inherit;font-size:12px;color:var(--ink);white-space:nowrap;}
    .pbtn.breit{width:100%;}
    .pbtn:disabled{opacity:.5;cursor:not-allowed;}
    .knopf{width:100%;border:none;border-radius:3px;padding:13px;cursor:pointer;background:var(--accent);color:#fff;font-family:inherit;font-weight:600;font-size:15px;}
    .knopf.schmal{width:auto;padding:9px 16px;font-size:13.5px;}
    .knopf:disabled{background:#C9C8BF;cursor:not-allowed;}
    .spin{width:13px;height:13px;display:inline-block;border:2px solid #fff;border-top-color:transparent;border-radius:50%;vertical-align:-2px;margin-right:9px;animation:sp 1s linear infinite;}
    @keyframes sp{to{transform:rotate(360deg);}}

    .vorschau{padding:16px;display:flex;flex-direction:column;gap:16px;}
    .leer{color:var(--soft);font-size:13px;margin:0;text-align:center;padding:26px 0;}
    .bogen-halter{display:flex;flex-direction:column;gap:6px;}
    .bogen{position:relative;width:100%;background:#fff;border:1px solid var(--line);box-shadow:var(--shadow);overflow:hidden;}
    .slot{position:absolute;overflow:hidden;}
    .rotor{position:absolute;left:50%;top:50%;width:100%;height:100%;transform:translate(-50%,-50%);overflow:hidden;}
    .rotor img{position:absolute;}
    .marken{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;}
    .bogen-fuss{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--soft);}
    .warnung{color:var(--err);font-size:12px;margin:0;}

    .modal{position:fixed;inset:0;background:rgba(22,21,15,.55);display:flex;align-items:center;justify-content:center;z-index:70;padding:16px;}
    .modal-inhalt{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);width:min(560px,100%);max-height:90vh;overflow:auto;}
    .modal-inhalt.breit{width:min(880px,100%);}
    .crop-buehne{padding:16px;display:flex;justify-content:center;background:var(--paper);}
    .crop-box{position:relative;width:100%;max-width:420px;max-height:56vh;overflow:hidden;background:#fff;border:1px solid var(--line);cursor:grab;touch-action:none;user-select:none;}
    .crop-box:active{cursor:grabbing;}
    .crop-box img{position:absolute;pointer-events:none;}
    .crop-gitter{position:absolute;inset:0;pointer-events:none;
      background:linear-gradient(to right,transparent calc(33.33% - .5px),rgba(255,255,255,.55) 33.33%,rgba(255,255,255,.55) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgba(255,255,255,.55) 66.66%,rgba(255,255,255,.55) calc(66.66% + .5px),transparent calc(66.66% + .5px)),
                 linear-gradient(to bottom,transparent calc(33.33% - .5px),rgba(255,255,255,.55) 33.33%,rgba(255,255,255,.55) calc(33.33% + .5px),transparent calc(33.33% + .5px),transparent calc(66.66% - .5px),rgba(255,255,255,.55) 66.66%,rgba(255,255,255,.55) calc(66.66% + .5px),transparent calc(66.66% + .5px));
      box-shadow:inset 0 0 0 1px rgba(27,59,224,.35);}
    .crop-steuer{padding:14px 16px;display:flex;flex-direction:column;gap:8px;}

    .picker{padding:14px;display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px;}
    .pick{border:1px solid var(--line);background:#fff;border-radius:3px;padding:0;cursor:pointer;overflow:hidden;display:flex;flex-direction:column;font-family:inherit;}
    .pick img{width:100%;aspect-ratio:1;object-fit:cover;display:block;}
    .pick span{font-size:10px;color:var(--soft);padding:4px 5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
    .pick:hover{border-color:var(--accent);}

    .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%);background:var(--ink);color:#FBFBF7;padding:10px 18px;border-radius:3px;font-size:13.5px;z-index:80;max-width:min(92vw,520px);text-align:center;line-height:1.45;}
  `}</style>;
}
