// Label Designer — QSO labels for QSL cards and address labels for envelopes.
//
// Same architecture as the QSL card designer, stripped to what a monochrome
// sticker needs: saved templates on the left, a mm-true canvas on the right,
// click to select, drag to move. The canvas renderer (labelRender) is shared
// with the future print path, so what the preview shows is what the PDF gets.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { X, Plus, Trash2, Star, Tag, MapPin } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import {
  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import {
  LabelListStocks, LabelSaveStock, LabelDeleteStock,
  LabelListTemplates, LabelGetTemplate, LabelSaveTemplate, LabelDeleteTemplate,
  LabelSetDefaultTemplate, LabelSampleQSOs, GetActiveProfile,
} from '../../../wailsjs/go/main/App';
import type { LabelElement, LabelSample, LabelStock, LabelTemplate } from './labelTypes';
import { LABEL_VARS, TABLE_FIELDS, starterTemplate } from './labelTypes';
import { drawMargins, render, type ElementBox } from './labelRender';

interface Props { open: boolean; onClose: () => void }

interface TplInfo {
  id: number; name: string; kind: string; stock_id: number;
  is_default: boolean; updated_at: string;
}

// The preview fills whatever room the centre pane offers — a label cut off at
// the edge cannot be judged, and 90 mm across a fixed 720 px wasted half the
// window. Measured live so a resize re-fits.
const PREVIEW_MIN_SCALE = 2;

export function LabelDesignerModal({ open, onClose }: Props) {
  const { t } = useI18n();
  const [stocks, setStocks] = useState<LabelStock[]>([]);
  const [saved, setSaved] = useState<TplInfo[]>([]);
  const [samples, setSamples] = useState<LabelSample[]>([]);
  const [myVars, setMyVars] = useState<Record<string, string>>({});
  const [error, setError] = useState('');

  // The design being edited (null = nothing open yet).
  const [tplId, setTplId] = useState(0);
  const [tpl, setTpl] = useState<LabelTemplate | null>(null);
  const [tplName, setTplName] = useState('');
  const [forProfile, setForProfile] = useState(true);
  const [sel, setSel] = useState<number | null>(null);
  const [dirty, setDirty] = useState(false);
  const [deleteArm, setDeleteArm] = useState(0);

  // Stock editor (collapsed by default — geometry is set once per roll).
  const [stockOpen, setStockOpen] = useState(false);
  const [stockDraft, setStockDraft] = useState<LabelStock | null>(null);

  const canvasRef = useRef<HTMLCanvasElement>(null);
  const paneRef = useRef<HTMLDivElement>(null);
  const [paneSize, setPaneSize] = useState({ w: 720, h: 420 });
  useEffect(() => {
    const el = paneRef.current;
    if (!el) return;
    const ro = new ResizeObserver(() => {
      setPaneSize({ w: el.clientWidth, h: el.clientHeight });
    });
    ro.observe(el);
    return () => ro.disconnect();
  }, [open, tpl != null]);
  const boxesRef = useRef<ElementBox[]>([]);
  const dragRef = useRef<{ idx: number; dxMm: number; dyMm: number } | null>(null);

  const stock = useMemo(
    () => stocks.find((s) => s.id === tpl?.stock_id) ?? stocks[0],
    [stocks, tpl?.stock_id]);

  const refresh = useCallback(async () => {
    try {
      setStocks((await LabelListStocks()) as any as LabelStock[]);
      setSaved(((await LabelListTemplates()) ?? []) as any as TplInfo[]);
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }, []);

  useEffect(() => {
    if (!open) return;
    setError(''); setTpl(null); setTplId(0); setSel(null); setDirty(false);
    void refresh();
    LabelSampleQSOs(5).then((r: any) => setSamples(r ?? [])).catch(() => {});
    // The preview resolves <MY*> from the active profile so the address label
    // reads like the finished sticker, not like a form.
    GetActiveProfile().then((p: any) => setMyVars({
      MYCALL: p?.callsign ?? 'MYCALL', MYNAME: p?.op_name ?? p?.operator ?? '',
      MYSTREET: p?.my_street ?? '', MYZIP: p?.my_postal_code ?? '', MYCITY: p?.my_city ?? '',
      MYCOUNTRY: p?.my_country ?? '',
    })).catch(() => setMyVars({ MYCALL: 'MYCALL' }));
  }, [open, refresh]);

  // Preview variable values: the first sample QSO plays the DX station.
  const vars = useMemo(() => {
    const q = samples[0];
    return {
      CALL: q?.callsign ?? 'DL1ABC', NAME: q?.name ?? 'Hans', QTH: q?.qth ?? 'Berlin',
      COUNTRY: q?.country ?? 'Germany', VIA: 'DJ5XX',
      STREET: 'Funkerstrasse 12', ZIP: '10115', CITY: q?.qth || 'Berlin', STATE: '',
      ...myVars,
    } as Record<string, string>;
  }, [samples, myVars]);

  // ── canvas ────────────────────────────────────────────────────────────
  const scale = useMemo(() => {
    if (!stock) return 4;
    // Leave room for the pane's padding and the two control strips above and
    // below the canvas; never shrink below a scale where 8 pt is readable.
    const availW = Math.max(200, paneSize.w - 56);
    const availH = Math.max(120, paneSize.h - 130);
    return Math.max(PREVIEW_MIN_SCALE, Math.min(availW / stock.w_mm, availH / stock.h_mm));
  }, [stock, paneSize]);

  const paint = useCallback(() => {
    const cv = canvasRef.current;
    if (!cv || !tpl || !stock) return;
    const dpr = window.devicePixelRatio || 1;
    cv.width = Math.round(stock.w_mm * scale * dpr);
    cv.height = Math.round(stock.h_mm * scale * dpr);
    cv.style.width = `${stock.w_mm * scale}px`;
    cv.style.height = `${stock.h_mm * scale}px`;
    const ctx = cv.getContext('2d');
    if (!ctx) return;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    boxesRef.current = render(ctx, tpl, stock, { vars, qsos: samples }, scale);
    drawMargins(ctx, stock, scale);
    // Selection outline, drawn after everything: the editor's own chrome.
    if (sel != null && boxesRef.current[sel]) {
      const b = boxesRef.current[sel];
      ctx.save();
      ctx.scale(scale, scale);
      ctx.strokeStyle = 'rgba(255,140,0,0.9)';
      ctx.lineWidth = 0.3;
      ctx.setLineDash([1, 0.8]);
      ctx.strokeRect(b.x - 0.6, b.y - 0.6, b.w + 1.2, b.h + 1.2);
      ctx.restore();
    }
  }, [tpl, stock, vars, samples, scale, sel]);

  useEffect(() => { paint(); }, [paint]);

  const mmFromEvent = (ev: React.MouseEvent): { x: number; y: number } => {
    const r = canvasRef.current!.getBoundingClientRect();
    return { x: (ev.clientX - r.left) / scale, y: (ev.clientY - r.top) / scale };
  };

  const onCanvasDown = (ev: React.MouseEvent) => {
    if (!tpl) return;
    const p = mmFromEvent(ev);
    // Topmost element under the pointer wins — iterate backwards.
    for (let i = boxesRef.current.length - 1; i >= 0; i--) {
      const b = boxesRef.current[i];
      if (p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h) {
        setSel(i);
        dragRef.current = { idx: i, dxMm: p.x - tpl.elements[i].x_mm, dyMm: p.y - tpl.elements[i].y_mm };
        return;
      }
    }
    setSel(null);
  };
  const onCanvasMove = (ev: React.MouseEvent) => {
    const d = dragRef.current;
    if (!d || !tpl) return;
    const p = mmFromEvent(ev);
    // Snapped to 0.5 mm: free pixels look precise on screen and print ragged.
    const x = Math.round((p.x - d.dxMm) * 2) / 2;
    const y = Math.round((p.y - d.dyMm) * 2) / 2;
    patchEl(d.idx, { x_mm: x, y_mm: y });
  };
  const onCanvasUp = () => { dragRef.current = null; };

  // ── template edits ────────────────────────────────────────────────────
  function patchEl(idx: number, patch: Partial<LabelElement>) {
    setTpl((cur) => {
      if (!cur) return cur;
      const elements = cur.elements.map((e, i) => (i === idx ? { ...e, ...patch } : e));
      return { ...cur, elements };
    });
    setDirty(true);
  }
  function addElement(e: LabelElement) {
    setTpl((cur) => (cur ? { ...cur, elements: [...cur.elements, e] } : cur));
    setSel(tpl ? tpl.elements.length : 0);
    setDirty(true);
  }
  function removeSelected() {
    if (sel == null) return;
    setTpl((cur) => (cur ? { ...cur, elements: cur.elements.filter((_, i) => i !== sel) } : cur));
    setSel(null);
    setDirty(true);
  }

  function newTemplate(kind: 'qso' | 'address') {
    const s = stocks[0];
    if (!s) { setError(t('lbl.noStock')); return; }
    setTpl(starterTemplate(kind, s));
    setTplId(0);
    setTplName(kind === 'qso' ? t('lbl.newQsoName') : t('lbl.newAddrName'));
    setSel(null);
    setDirty(true);
  }

  async function openTemplate(info: TplInfo) {
    try {
      const doc = await LabelGetTemplate(info.id);
      setTpl(JSON.parse(doc) as LabelTemplate);
      setTplId(info.id);
      setTplName(info.name);
      setSel(null);
      setDirty(false);
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  async function save() {
    if (!tpl) return;
    try {
      const id = await LabelSaveTemplate(tplId, tplName.trim() || 'Label', JSON.stringify(tpl), forProfile);
      setTplId(id as number);
      setDirty(false);
      await refresh();
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  async function removeTemplate(id: number) {
    if (deleteArm !== id) { setDeleteArm(id); window.setTimeout(() => setDeleteArm(0), 2500); return; }
    try {
      await LabelDeleteTemplate(id);
      if (id === tplId) { setTpl(null); setTplId(0); }
      await refresh();
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  // ── stock editor ──────────────────────────────────────────────────────
  async function saveStock() {
    if (!stockDraft) return;
    try {
      await LabelSaveStock(stockDraft as any);
      setStockDraft(null);
      await refresh();
    } catch (e: any) { setError(String(e?.message ?? e)); }
  }

  if (!open) return null;
  const e = sel != null && tpl ? tpl.elements[sel] : undefined;

  const numField = (label: string, value: number, set: (v: number) => void, step = 0.5, w = 'w-20') => (
    <div className="space-y-0.5">
      <Label className="text-[10px] text-muted-foreground">{label}</Label>
      <Input type="number" step={step} value={value} className={cn('h-7 text-xs font-mono', w)}
        onChange={(ev) => set(parseFloat(ev.target.value) || 0)} />
    </div>
  );

  return (
    <div className="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
      <div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-6xl h-[92vh] flex flex-col overflow-hidden">
        {/* header */}
        <div className="flex items-center gap-3 px-4 py-2.5 border-b border-border shrink-0">
          <Tag className="size-4 text-primary" />
          <span className="font-semibold text-sm">{t('lbl.title')}</span>
          <div className="flex-1" />
          {tpl && (
            <>
              <Input className="h-8 w-56 text-sm" value={tplName} onChange={(ev) => { setTplName(ev.target.value); setDirty(true); }}
                placeholder={t('lbl.namePh')} />
              <label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer">
                <Checkbox checked={forProfile} onCheckedChange={(c) => setForProfile(!!c)} />
                {t('lbl.forProfile')}
              </label>
              <Button size="sm" className="h-8" onClick={save} disabled={!dirty && tplId !== 0}>
                {t('lbl.save')}
              </Button>
            </>
          )}
          <Button variant="ghost" size="sm" className="h-8" onClick={onClose}><X className="size-4" /></Button>
        </div>
        {error && <div className="px-4 py-1.5 text-xs text-danger border-b border-border/60">{error}</div>}

        <div className="flex-1 min-h-0 flex">
          {/* left: saved templates + stocks */}
          <div className="w-64 border-r border-border flex flex-col min-h-0 shrink-0">
            <div className="p-2 flex gap-1.5">
              <Button size="sm" variant="outline" className="h-8 flex-1" onClick={() => newTemplate('qso')}>
                <Plus className="size-3.5" /> {t('lbl.newQso')}
              </Button>
              <Button size="sm" variant="outline" className="h-8 flex-1" onClick={() => newTemplate('address')}>
                <Plus className="size-3.5" /> {t('lbl.newAddr')}
              </Button>
            </div>
            <div className="flex-1 overflow-y-auto px-2 pb-2 space-y-1">
              {saved.map((s) => (
                <div key={s.id}
                  className={cn('rounded-md border px-2 py-1.5 cursor-pointer text-xs',
                    s.id === tplId ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50')}
                  onClick={() => void openTemplate(s)}>
                  <div className="flex items-center gap-1.5">
                    {s.kind === 'qso' ? <Tag className="size-3 shrink-0 text-muted-foreground" /> : <MapPin className="size-3 shrink-0 text-muted-foreground" />}
                    <span className="font-medium truncate flex-1">{s.name}</span>
                    <button type="button" title={t('lbl.setDefault')}
                      onClick={(ev) => { ev.stopPropagation(); void LabelSetDefaultTemplate(s.id).then(refresh); }}>
                      <Star className={cn('size-3.5', s.is_default ? 'text-warning fill-warning' : 'text-muted-foreground/40')} />
                    </button>
                    <button type="button" title={t('lbl.delete')}
                      onClick={(ev) => { ev.stopPropagation(); void removeTemplate(s.id); }}>
                      <Trash2 className={cn('size-3.5', deleteArm === s.id ? 'text-danger' : 'text-muted-foreground/40')} />
                    </button>
                  </div>
                  <div className="text-[10px] text-muted-foreground pl-4">
                    {s.kind === 'qso' ? t('lbl.kindQso') : t('lbl.kindAddr')} · {s.updated_at}
                  </div>
                </div>
              ))}
              {saved.length === 0 && (
                <div className="text-xs text-muted-foreground px-1 py-3">{t('lbl.empty')}</div>
              )}
            </div>
            {/* stocks */}
            <div className="border-t border-border p-2 space-y-1.5">
              <button type="button" className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
                onClick={() => setStockOpen((v) => !v)}>
                {t('lbl.stocks')} {stockOpen ? '▾' : '▸'}
              </button>
              {stockOpen && (
                <div className="space-y-1.5">
                  <Select value={String(stockDraft?.id ?? '')} onValueChange={(v) => {
                    const s = stocks.find((x) => String(x.id) === v);
                    if (s) setStockDraft({ ...s });
                  }}>
                    <SelectTrigger className="h-7 text-xs"><SelectValue placeholder={t('lbl.pickStock')} /></SelectTrigger>
                    <SelectContent>
                      {stocks.map((s) => <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <div className="flex gap-1.5">
                    <Button size="sm" variant="outline" className="h-7 text-xs flex-1"
                      onClick={() => setStockDraft({ name: t('lbl.customStock'), w_mm: 90, h_mm: 29, margin_top_mm: 1.5, margin_right_mm: 3, margin_bottom_mm: 1.5, margin_left_mm: 3, dpi: 300 })}>
                      <Plus className="size-3" /> {t('lbl.newStock')}
                    </Button>
                    {stockDraft?.id ? (
                      <Button size="sm" variant="ghost" className="h-7 text-xs text-danger"
                        onClick={() => { void LabelDeleteStock(stockDraft.id!).then(() => { setStockDraft(null); return refresh(); }); }}>
                        <Trash2 className="size-3" />
                      </Button>
                    ) : null}
                  </div>
                  {stockDraft && (
                    <div className="space-y-1.5">
                      <Input className="h-7 text-xs" value={stockDraft.name}
                        onChange={(ev) => setStockDraft({ ...stockDraft, name: ev.target.value })} />
                      <div className="grid grid-cols-2 gap-1.5">
                        {numField(t('lbl.widthMm'), stockDraft.w_mm, (v) => setStockDraft({ ...stockDraft, w_mm: v }), 0.5, 'w-full')}
                        {numField(t('lbl.heightMm'), stockDraft.h_mm, (v) => setStockDraft({ ...stockDraft, h_mm: v }), 0.5, 'w-full')}
                      </div>
                      <div className="grid grid-cols-4 gap-1.5">
                        {numField('↑', stockDraft.margin_top_mm, (v) => setStockDraft({ ...stockDraft, margin_top_mm: v }), 0.5, 'w-full')}
                        {numField('→', stockDraft.margin_right_mm, (v) => setStockDraft({ ...stockDraft, margin_right_mm: v }), 0.5, 'w-full')}
                        {numField('↓', stockDraft.margin_bottom_mm, (v) => setStockDraft({ ...stockDraft, margin_bottom_mm: v }), 0.5, 'w-full')}
                        {numField('←', stockDraft.margin_left_mm, (v) => setStockDraft({ ...stockDraft, margin_left_mm: v }), 0.5, 'w-full')}
                      </div>
                      <Button size="sm" className="h-7 text-xs w-full" onClick={() => void saveStock()}>{t('lbl.saveStock')}</Button>
                    </div>
                  )}
                </div>
              )}
            </div>
          </div>

          {/* centre: preview */}
          <div ref={paneRef} className="flex-1 min-w-0 flex flex-col items-center justify-center bg-muted/30 overflow-auto p-6 gap-3">
            {tpl && stock ? (
              <>
                <div className="flex items-center gap-2 text-xs text-muted-foreground">
                  <span>{t('lbl.stock')}:</span>
                  <Select value={String(tpl.stock_id || stock.id)} onValueChange={(v) => {
                    setTpl({ ...tpl, stock_id: parseInt(v, 10) }); setDirty(true);
                  }}>
                    <SelectTrigger className="h-7 text-xs w-72"><SelectValue /></SelectTrigger>
                    <SelectContent>
                      {stocks.map((s) => <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>)}
                    </SelectContent>
                  </Select>
                  <span className="font-mono">{stock.w_mm}×{stock.h_mm} mm</span>
                </div>
                <canvas ref={canvasRef} className="shadow-lg rounded-sm cursor-move"
                  onMouseDown={onCanvasDown} onMouseMove={onCanvasMove}
                  onMouseUp={onCanvasUp} onMouseLeave={onCanvasUp} />
                <div className="flex items-center gap-1.5">
                  <Button size="sm" variant="outline" className="h-7 text-xs"
                    onClick={() => addElement({ type: 'text', x_mm: stock.margin_left_mm, y_mm: stock.h_mm / 2, text: t('lbl.newText'), size_pt: 9 })}>
                    <Plus className="size-3" /> {t('lbl.addText')}
                  </Button>
                  <Button size="sm" variant="outline" className="h-7 text-xs"
                    onClick={() => addElement({ type: 'line', x_mm: stock.margin_left_mm, y_mm: stock.h_mm / 2, w_mm: stock.w_mm - stock.margin_left_mm - stock.margin_right_mm, thickness_mm: 0.3 })}>
                    <Plus className="size-3" /> {t('lbl.addLine')}
                  </Button>
                  {tpl.kind === 'address' && (
                    <Button size="sm" variant="outline" className="h-7 text-xs"
                      onClick={() => addElement({ type: 'addr_block', x_mm: stock.margin_left_mm + 2, y_mm: stock.margin_top_mm + 2, lines: ['<NAME>', '<STREET>', '<ZIP> <CITY>', '<COUNTRY>'], size_pt: 11, line_gap_mm: 1.6 })}>
                      <Plus className="size-3" /> {t('lbl.addAddr')}
                    </Button>
                  )}
                  {tpl.kind === 'qso' && !tpl.elements.some((x) => x.type === 'qso_table') && (
                    <Button size="sm" variant="outline" className="h-7 text-xs"
                      onClick={() => addElement({ type: 'qso_table', x_mm: stock.margin_left_mm, y_mm: stock.margin_top_mm + 7, w_mm: stock.w_mm - stock.margin_left_mm - stock.margin_right_mm, rows_max: 4, row_h_mm: 4, header: true, size_pt: 7.5, columns: TABLE_FIELDS.slice(0, 6).map((c) => ({ ...c })) })}>
                      <Plus className="size-3" /> {t('lbl.addTable')}
                    </Button>
                  )}
                  {sel != null && (
                    <Button size="sm" variant="ghost" className="h-7 text-xs text-danger" onClick={removeSelected}>
                      <Trash2 className="size-3" /> {t('lbl.removeEl')}
                    </Button>
                  )}
                </div>
              </>
            ) : (
              <div className="text-sm text-muted-foreground text-center max-w-sm leading-relaxed">
                {t('lbl.intro')}
              </div>
            )}
          </div>

          {/* right: properties of the selection */}
          {tpl && (
            <div className="w-72 border-l border-border overflow-y-auto p-3 space-y-3 shrink-0">
              {!e ? (
                <div className="text-xs text-muted-foreground leading-relaxed">{t('lbl.selectHint')}</div>
              ) : (
                <>
                  <div className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
                    {t(`lbl.el_${e.type}` as any)}
                  </div>
                  <div className="flex gap-2">
                    {numField('X mm', e.x_mm, (v) => patchEl(sel!, { x_mm: v }))}
                    {numField('Y mm', e.y_mm, (v) => patchEl(sel!, { y_mm: v }))}
                    {(e.type === 'text' || e.type === 'line' || e.type === 'qso_table') &&
                      numField('W mm', e.w_mm ?? 0, (v) => patchEl(sel!, { w_mm: v }))}
                  </div>

                  {e.type === 'text' && (
                    <>
                      <div className="space-y-0.5">
                        <Label className="text-[10px] text-muted-foreground">{t('lbl.text')}</Label>
                        <Input className="h-7 text-xs" value={e.text ?? ''} onChange={(ev) => patchEl(sel!, { text: ev.target.value })} />
                      </div>
                      <VarPicker onPick={(v) => patchEl(sel!, { text: `${e.text ?? ''}<${v}>` })} t={t} />
                      <div className="flex gap-2 items-end">
                        {numField('pt', e.size_pt ?? 9, (v) => patchEl(sel!, { size_pt: v }), 0.5, 'w-16')}
                        <label className="flex items-center gap-1 text-xs cursor-pointer pb-1.5">
                          <Checkbox checked={!!e.bold} onCheckedChange={(c) => patchEl(sel!, { bold: !!c })} /> B
                        </label>
                        <label className="flex items-center gap-1 text-xs italic cursor-pointer pb-1.5">
                          <Checkbox checked={!!e.italic} onCheckedChange={(c) => patchEl(sel!, { italic: !!c })} /> I
                        </label>
                        <Select value={e.align ?? 'left'} onValueChange={(v) => patchEl(sel!, { align: v as any })}>
                          <SelectTrigger className="h-7 text-xs w-24"><SelectValue /></SelectTrigger>
                          <SelectContent>
                            <SelectItem value="left">{t('lbl.alignLeft')}</SelectItem>
                            <SelectItem value="center">{t('lbl.alignCenter')}</SelectItem>
                            <SelectItem value="right">{t('lbl.alignRight')}</SelectItem>
                          </SelectContent>
                        </Select>
                      </div>
                    </>
                  )}

                  {e.type === 'line' && numField(t('lbl.thickness'), e.thickness_mm ?? 0.3, (v) => patchEl(sel!, { thickness_mm: v }), 0.1)}

                  {e.type === 'addr_block' && (
                    <>
                      <div className="space-y-0.5">
                        <Label className="text-[10px] text-muted-foreground">{t('lbl.addrLines')}</Label>
                        <textarea
                          className="w-full h-28 rounded-md border border-input bg-background px-2 py-1 text-xs font-mono"
                          value={(e.lines ?? []).join('\n')}
                          onChange={(ev) => patchEl(sel!, { lines: ev.target.value.split('\n') })} />
                        <div className="text-[10px] text-muted-foreground">{t('lbl.addrHint')}</div>
                      </div>
                      <VarPicker onPick={(v) => patchEl(sel!, { lines: [...(e.lines ?? []), `<${v}>`] })} t={t} />
                      <div className="flex gap-2">
                        {numField('pt', e.size_pt ?? 11, (v) => patchEl(sel!, { size_pt: v }), 0.5, 'w-16')}
                        {numField(t('lbl.lineGap'), e.line_gap_mm ?? 1.5, (v) => patchEl(sel!, { line_gap_mm: v }), 0.1, 'w-20')}
                      </div>
                    </>
                  )}

                  {e.type === 'qso_table' && (
                    <>
                      <div className="flex gap-2">
                        {numField(t('lbl.rows'), e.rows_max ?? 4, (v) => patchEl(sel!, { rows_max: Math.max(1, Math.round(v)) }), 1, 'w-16')}
                        {numField(t('lbl.rowH'), e.row_h_mm ?? 4, (v) => patchEl(sel!, { row_h_mm: v }), 0.1, 'w-20')}
                        {numField('pt', e.size_pt ?? 7.5, (v) => patchEl(sel!, { size_pt: v }), 0.5, 'w-16')}
                      </div>
                      <label className="flex items-center gap-1.5 text-xs cursor-pointer">
                        <Checkbox checked={!!e.header} onCheckedChange={(c) => patchEl(sel!, { header: !!c })} />
                        {t('lbl.header')}
                      </label>
                      <div className="space-y-1">
                        <Label className="text-[10px] text-muted-foreground">{t('lbl.columns')}</Label>
                        {(e.columns ?? []).map((c, ci) => (
                          <div key={ci} className="flex items-center gap-1">
                            <Select value={c.field} onValueChange={(v) => {
                              const f = TABLE_FIELDS.find((x) => x.field === v);
                              const columns = e.columns!.map((x, i) => (i === ci ? { ...x, field: v, label: f?.label ?? v } : x));
                              patchEl(sel!, { columns });
                            }}>
                              <SelectTrigger className="h-6 text-[11px] flex-1"><SelectValue /></SelectTrigger>
                              <SelectContent>
                                {TABLE_FIELDS.map((f) => <SelectItem key={f.field} value={f.field}>{f.label}</SelectItem>)}
                              </SelectContent>
                            </Select>
                            <Input type="number" step={0.5} className="h-6 w-14 text-[11px] font-mono" value={c.w_mm}
                              onChange={(ev) => {
                                const columns = e.columns!.map((x, i) => (i === ci ? { ...x, w_mm: parseFloat(ev.target.value) || 1 } : x));
                                patchEl(sel!, { columns });
                              }} />
                            <button type="button" onClick={() => patchEl(sel!, { columns: e.columns!.filter((_, i) => i !== ci) })}>
                              <Trash2 className="size-3 text-muted-foreground/60" />
                            </button>
                          </div>
                        ))}
                        <Button size="sm" variant="outline" className="h-6 text-[11px] w-full"
                          onClick={() => patchEl(sel!, { columns: [...(e.columns ?? []), { ...TABLE_FIELDS[0] }] })}>
                          <Plus className="size-3" /> {t('lbl.addColumn')}
                        </Button>
                      </div>
                    </>
                  )}
                </>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// VarPicker inserts a <VARIABLE> — a menu beats remembering the exact spelling.
function VarPicker({ onPick, t }: { onPick: (v: string) => void; t: (k: string) => string }) {
  return (
    <Select value="" onValueChange={(v) => { if (v) onPick(v); }}>
      <SelectTrigger className="h-7 text-xs"><SelectValue placeholder={t('lbl.insertVar')} /></SelectTrigger>
      <SelectContent>
        {LABEL_VARS.map((v) => <SelectItem key={v} value={v}>{`<${v}>`}</SelectItem>)}
      </SelectContent>
    </Select>
  );
}
