// Image Converter — client-side image format/quality/resize converter, styled to match Shot List.
const { useState: useStateIC, useRef: useRefIC, useEffect: useEffectIC } = React;

const FORMATS = [
  { id: 'image/jpeg', label: 'JPG', ext: 'jpg' },
  { id: 'image/png',  label: 'PNG', ext: 'png' },
  { id: 'image/webp', label: 'WebP', ext: 'webp' },
];

const RESIZE_MODES = [
  { id: 'none',    label: "Don't resize" },
  { id: 'long',    label: 'Long edge' },
  { id: 'short',   label: 'Short edge' },
  { id: 'width',   label: 'Width' },
  { id: 'height',  label: 'Height' },
  { id: 'fit',     label: 'Width & height (fit)' },
  { id: 'longshort', label: 'Long edge & short edge' },
  { id: 'mp',      label: 'Megapixels' },
  { id: 'pct',     label: 'Percentage' },
];

const STATUSES_IC = {
  ready:    { label: 'Ready',     cls: 'ready' },
  queued:   { label: 'Queued',    cls: 'draft' },
  working:  { label: 'Converting…', cls: 'review' },
  done:     { label: 'Done',      cls: 'approved' },
  error:    { label: 'Error',     cls: 'error' },
};

function bytes(n) {
  if (!n && n !== 0) return '—';
  if (n < 1024) return n + ' B';
  if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
  return (n / 1024 / 1024).toFixed(2) + ' MB';
}

function isTiff(file) {
  return file.type === 'image/tiff' || /\.tiff?$/i.test(file.name);
}

function readImage(file) {
  // TIFF goes through UTIF — browsers can't decode it natively.
  // The decoded pixels are written to a canvas, then fed into the
  // standard <img> pipeline as a PNG data URL so the rest of the
  // converter (resize, format, target-size) works unchanged.
  if (isTiff(file)) {
    return new Promise((res, rej) => {
      file.arrayBuffer().then(buf => {
        try {
          if (typeof UTIF === 'undefined') throw new Error('UTIF library not loaded');
          const ifds = UTIF.decode(buf);
          if (!ifds.length) throw new Error('No images in TIFF');
          UTIF.decodeImage(buf, ifds[0]);
          const w = ifds[0].width, h = ifds[0].height;
          const rgba = UTIF.toRGBA8(ifds[0]);
          const c = document.createElement('canvas');
          c.width = w; c.height = h;
          c.getContext('2d').putImageData(
            new ImageData(new Uint8ClampedArray(rgba.buffer), w, h), 0, 0
          );
          const dataUrl = c.toDataURL('image/png');
          const img = new Image();
          img.onload = () => res({ img, dataUrl });
          img.onerror = rej;
          img.src = dataUrl;
        } catch (e) { rej(e); }
      }).catch(rej);
    });
  }
  return new Promise((res, rej) => {
    const fr = new FileReader();
    fr.onload = () => {
      const img = new Image();
      img.onload = () => res({ img, dataUrl: fr.result });
      img.onerror = rej;
      img.src = fr.result;
    };
    fr.onerror = rej;
    fr.readAsDataURL(file);
  });
}

function computeTargetDims(w, h, mode, a, b) {
  // a, b are numeric inputs in px / %
  const A = +a || 0;
  const B = +b || 0;
  switch (mode) {
    case 'long': {
      if (!A) return [w, h];
      const s = A / Math.max(w, h);
      return [Math.round(w * s), Math.round(h * s)];
    }
    case 'short': {
      if (!A) return [w, h];
      const s = A / Math.min(w, h);
      return [Math.round(w * s), Math.round(h * s)];
    }
    case 'width': {
      if (!A) return [w, h];
      const s = A / w;
      return [A, Math.round(h * s)];
    }
    case 'height': {
      if (!A) return [w, h];
      const s = A / h;
      return [Math.round(w * s), A];
    }
    case 'fit': {
      if (!A || !B) return [w, h];
      const s = Math.min(A / w, B / h);
      return [Math.round(w * s), Math.round(h * s)];
    }
    case 'longshort': {
      if (!A || !B) return [w, h];
      const longTarget = A, shortTarget = B;
      const longCur = Math.max(w, h), shortCur = Math.min(w, h);
      const s = Math.min(longTarget / longCur, shortTarget / shortCur);
      return [Math.round(w * s), Math.round(h * s)];
    }
    case 'mp': {
      if (!A) return [w, h];
      const target = A * 1_000_000;
      const cur = w * h;
      const s = Math.sqrt(target / cur);
      return [Math.round(w * s), Math.round(h * s)];
    }
    case 'pct': {
      if (!A) return [w, h];
      const s = A / 100;
      return [Math.round(w * s), Math.round(h * s)];
    }
    default: return [w, h];
  }
}

async function convertOne(item, settings) {
  const { format, quality, resize, a, b, dontEnlarge, targetBytes } = settings;
  const { img } = await readImage(item.file);
  let [tw, th] = computeTargetDims(img.naturalWidth, img.naturalHeight, resize, a, b);
  if (dontEnlarge && (tw > img.naturalWidth || th > img.naturalHeight)) {
    tw = img.naturalWidth; th = img.naturalHeight;
  }

  const draw = (w, h) => {
    const c = document.createElement('canvas');
    c.width = w; c.height = h;
    const ctx = c.getContext('2d');
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';
    ctx.drawImage(img, 0, 0, w, h);
    return c;
  };

  const toBlob = (canvas, q) => new Promise(res => canvas.toBlob(b => res(b), format, q));

  let canvas = draw(tw, th);
  let blob = await toBlob(canvas, quality);

  // Target file size logic
  if (targetBytes && blob && format !== 'image/png') {
    // Binary-search the quality range to find the LARGEST blob that
    // still fits under target. Without this we under-shoot — e.g. a
    // 1 MB target landing at 300 KB because q=0.9 already fit and the
    // old loop only ever decreased.
    const MIN_Q = 0.05, MAX_Q = 0.98;

    const findBestFit = async (cv) => {
      const atMax = await toBlob(cv, MAX_Q);
      if (!atMax) return null;
      // Whole quality range fits — use max quality
      if (atMax.size <= targetBytes) return atMax;
      const atMin = await toBlob(cv, MIN_Q);
      if (!atMin || atMin.size > targetBytes) return null; // need to shrink dims
      // Binary search — 9 iters → ~0.2% quality resolution
      let lo = MIN_Q, hi = MAX_Q, best = atMin;
      for (let i = 0; i < 9; i++) {
        const q = (lo + hi) / 2;
        const test = await toBlob(cv, q);
        if (!test) break;
        if (test.size > targetBytes) {
          hi = q;
        } else {
          lo = q;
          if (test.size > best.size) best = test;
        }
      }
      return best;
    };

    let attempt = await findBestFit(canvas);
    // Even q=0.05 too big (rare) — fall back to dimension shrinking
    let scale = 1, tries = 0;
    while (!attempt && scale > 0.3 && tries < 6) {
      scale *= 0.85;
      canvas = draw(Math.max(1, Math.round(tw * scale)), Math.max(1, Math.round(th * scale)));
      attempt = await findBestFit(canvas);
      tries += 1;
    }
    if (attempt) blob = attempt;
  } else if (targetBytes && format === 'image/png') {
    // PNG is lossless — only lever is dimensions. Can only shrink (we
    // can't make a PNG bigger to "use up" the budget).
    let scale = 1;
    let tries = 0;
    while (blob.size > targetBytes && scale > 0.3 && tries < 6) {
      scale *= 0.85;
      canvas = draw(Math.max(1, Math.round(tw * scale)), Math.max(1, Math.round(th * scale)));
      blob = await toBlob(canvas, quality);
      tries += 1;
    }
  }

  return { blob, width: canvas.width, height: canvas.height };
}

function downloadBlob(blob, name) {
  const u = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = u; a.download = name;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(u), 1000);
}

function makeName(originalName, prefix, suffix, ext) {
  const dot = originalName.lastIndexOf('.');
  const base = dot > 0 ? originalName.slice(0, dot) : originalName;
  return `${prefix || ''}${base}${suffix || ''}.${ext}`;
}

function pad(n) { return String(n).padStart(3, '0'); }

function buildName({ mode, custom, n, original, ext }) {
  const dot = original.lastIndexOf('.');
  const base = dot > 0 ? original.slice(0, dot) : original;
  const num = pad(n);
  const c = (custom || 'image').trim() || 'image';
  switch (mode) {
    case 'custom-prefix':   return `${num}-${c}.${ext}`;
    case 'custom-suffix':   return `${c}-${num}.${ext}`;
    case 'original-prefix': return `${num}-${base}.${ext}`;
    case 'original-suffix': return `${base}-${num}.${ext}`;
    default: return `${base}.${ext}`;
  }
}

function Switch({ on, onChange }) {
  return (
    <button
      type="button"
      role="switch"
      aria-checked={on}
      className={`ic-switch ${on ? 'on' : ''}`}
      onClick={() => onChange(!on)}
    >
      <span className="ic-switch-thumb" />
    </button>
  );
}

function StatusPillIC({ status }) {
  const s = STATUSES_IC[status] || STATUSES_IC.ready;
  return (
    <span className={`status-pill ${s.cls}`}>
      <span className="pulse" />
      {s.label}
    </span>
  );
}

function DropZone({ onFiles, hasItems }) {
  const fileRef = useRefIC(null);
  const folderRef = useRefIC(null);
  const [over, setOver] = useStateIC(false);

  const handle = (filelist) => {
    const files = Array.from(filelist || []).filter(f =>
      f.type.startsWith('image/') ||
      /\.(png|jpe?g|webp|tiff?|gif|bmp|avif)$/i.test(f.name)
    );
    if (files.length) onFiles(files);
  };

  return (
    <div
      className={`ic-drop ${over ? 'over' : ''} ${hasItems ? 'compact' : ''}`}
      onDragOver={(e) => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={(e) => { e.preventDefault(); setOver(false); handle(e.dataTransfer.files); }}
    >
      <div className="ic-drop-icon"><IconUpload /></div>
      <div className="ic-drop-title">{hasItems ? 'Drop more images to add' : 'Drop images here'}</div>
      <div className="ic-drop-sub">PNG · JPG · WebP · TIFF</div>
      <div className="ic-drop-actions">
        <button className="btn" onClick={() => fileRef.current?.click()}>
          <IconUpload /> Choose files
        </button>
        <button className="btn" onClick={() => folderRef.current?.click()}>
          <IconFolder /> Choose folder
        </button>
      </div>
      <input ref={fileRef} type="file" accept="image/*" multiple style={{display:'none'}}
        onChange={(e) => { handle(e.target.files); e.target.value = ''; }} />
      <input ref={folderRef} type="file" multiple webkitdirectory="" directory="" style={{display:'none'}}
        onChange={(e) => { handle(e.target.files); e.target.value = ''; }} />
    </div>
  );
}

function NumberInput({ value, onChange, placeholder, suffix }) {
  return (
    <div className="ic-num-wrap">
      <input
        className="ic-input"
        type="number"
        min="0"
        value={value === '' || value == null ? '' : value}
        placeholder={placeholder}
        onChange={(e) => onChange(e.target.value)}
      />
      {suffix && <span className="ic-num-suffix">{suffix}</span>}
    </div>
  );
}

function ResizeFields({ resize, a, b, setA, setB }) {
  if (resize === 'none') return null;
  if (resize === 'fit' || resize === 'longshort') {
    const labelA = resize === 'fit' ? 'Width' : 'Long edge';
    const labelB = resize === 'fit' ? 'Height' : 'Short edge';
    return (
      <div className="ic-row-2">
        <label className="ic-field">
          <span>{labelA}</span>
          <NumberInput value={a} onChange={setA} placeholder="0" suffix="px" />
        </label>
        <label className="ic-field">
          <span>{labelB}</span>
          <NumberInput value={b} onChange={setB} placeholder="0" suffix="px" />
        </label>
      </div>
    );
  }
  const single = {
    long:   { label: 'Long edge',  suffix: 'px' },
    short:  { label: 'Short edge', suffix: 'px' },
    width:  { label: 'Width',      suffix: 'px' },
    height: { label: 'Height',     suffix: 'px' },
    mp:     { label: 'Megapixels', suffix: 'MP' },
    pct:    { label: 'Percentage', suffix: '%'  },
  }[resize];
  return (
    <label className="ic-field">
      <span>{single.label}</span>
      <NumberInput value={a} onChange={setA} placeholder="0" suffix={single.suffix} />
    </label>
  );
}

function ImageConverter() {
  const [theme, setTheme] = useStateIC(() => document.documentElement.dataset.theme || 'dark');
  const [expanded, setExpanded] = useStateIC(false);
  useEffectIC(() => { document.documentElement.dataset.theme = theme; }, [theme]);

  const [items, setItems] = useStateIC([]);
  // settings
  const [format, setFormat]   = useStateIC('image/jpeg');
  const quality = 0.9;
  const [targetSize, setTargetSize] = useStateIC('');
  const [targetUnit, setTargetUnit] = useStateIC('MB'); // 'MB' | 'KB'

  // sizing — default ON, long edge
  const [sizingOn, setSizingOn] = useStateIC(true);
  const [resize, setResize]   = useStateIC('long');
  const [rA, setRA]           = useStateIC('');
  const [rB, setRB]           = useStateIC('');
  const [dontEnlarge, setDontEnlarge] = useStateIC(true);

  // naming
  const [namingOn, setNamingOn] = useStateIC(false);
  const [nameMode, setNameMode] = useStateIC('custom-suffix'); // custom-prefix | custom-suffix | original-prefix | original-suffix
  const [customName, setCustomName] = useStateIC('image');
  const [startNum, setStartNum]     = useStateIC(1);

  // output destination
  const [outDir, setOutDir]   = useStateIC(null); // FileSystemDirectoryHandle | null
  const supportsFs = typeof window.showDirectoryPicker === 'function';

  const [busy, setBusy] = useStateIC(false);
  const [toast, setToast] = useStateIC(null);
  useEffectIC(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 2200); return () => clearTimeout(t); }, [toast]);

  // Auth state — gate the post-export signup prompt on it
  const { user: authUser } = useAuth();
  const [signupPromptOpen, setSignupPromptOpen] = useStateIC(false);
  const [signupPromptMode, setSignupPromptMode] = useStateIC('nudge'); // 'nudge' | 'block'
  // Auto-close the prompt the moment a sign-in completes
  useEffectIC(() => { if (authUser) setSignupPromptOpen(false); }, [authUser]);

  const fmt = FORMATS.find(f => f.id === format);

  const addFiles = async (files) => {
    const next = [];
    for (const f of files) {
      try {
        const { dataUrl, img } = await readImage(f);
        next.push({
          id: 'img-' + Math.random().toString(36).slice(2, 9),
          file: f,
          name: f.name,
          size: f.size,
          width: img.naturalWidth,
          height: img.naturalHeight,
          preview: dataUrl,
          status: 'ready',
          out: null,
        });
      } catch (e) { /* skip */ }
    }
    setItems(prev => [...prev, ...next]);
  };

  const remove = (id) => setItems(prev => prev.filter(x => x.id !== id));
  const clearAll = () => setItems([]);
  const reset = () => {
    setItems([]);
    setFormat('image/jpeg'); setTargetSize(''); setTargetUnit('MB');
    setSizingOn(true); setResize('long'); setRA(''); setRB(''); setDontEnlarge(true);
    setNamingOn(false); setNameMode('custom-suffix'); setCustomName('image'); setStartNum(1);
    setOutDir(null);
  };

  const pickOutDir = async () => {
    if (!supportsFs) return;
    try {
      const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
      setOutDir(handle);
    } catch (e) { /* user cancelled */ }
  };

  const writeToDir = async (dirHandle, name, blob) => {
    const fileHandle = await dirHandle.getFileHandle(name, { create: true });
    const w = await fileHandle.createWritable();
    await w.write(blob);
    await w.close();
  };

  const exportAll = async () => {
    if (items.length === 0 || busy) return;

    // GATE: signed-out users get one free conversion run. Every Export
    // click after that is blocked until they sign up. Authenticated
    // users always pass; if auth isn't configured, we don't gate.
    if (!authUser && window.JUMP_AUTH_READY) {
      let prevCount = 0;
      try { prevCount = parseInt(localStorage.getItem('jump.exportCount') || '0', 10) || 0; }
      catch (_) { /* private mode — fall through, don't gate */ }
      if (prevCount >= 1) {
        // Force-show the prompt even if it was session-dismissed earlier;
        // the gate is the wall, not a soft nudge — and in block mode the
        // modal hides the "Maybe later" escape so sign-up is the only CTA.
        try { sessionStorage.removeItem('jump.signupPromptDismissed'); } catch (_) {}
        setSignupPromptMode('block');
        setSignupPromptOpen(true);
        return; // export pipeline never runs
      }
    }

    setBusy(true);
    const settings = {
      format, quality,
      resize: sizingOn ? resize : 'none',
      a: rA, b: rB, dontEnlarge,
      targetBytes: targetSize ? (+targetSize) * (targetUnit === 'MB' ? 1024 * 1024 : 1024) : 0,
    };
    // mark all queued
    setItems(prev => prev.map(it => ({ ...it, status: 'queued' })));
    let counter = +startNum || 1;
    let anySuccess = false;
    for (const item of items) {
      setItems(prev => prev.map(it => it.id === item.id ? { ...it, status: 'working' } : it));
      try {
        const { blob, width, height } = await convertOne(item, settings);
        const name = namingOn
          ? buildName({ mode: nameMode, custom: customName, n: counter, original: item.name, ext: fmt.ext })
          : makeName(item.name, '', '', fmt.ext);
        counter += 1;
        if (outDir && supportsFs) {
          try { await writeToDir(outDir, name, blob); }
          catch (e) { downloadBlob(blob, name); }
        } else {
          downloadBlob(blob, name);
        }
        anySuccess = true;
        setItems(prev => prev.map(it => it.id === item.id
          ? { ...it, status: 'done', out: { name, size: blob.size, width, height } }
          : it));
      } catch (err) {
        setItems(prev => prev.map(it => it.id === item.id
          ? { ...it, status: 'error', error: String(err.message || err) }
          : it));
      }
    }

    // Signup nudge — only count export runs that produced at least one
    // file, only for signed-out users, only when auth is wired up, and
    // only on the very first export. Dismissal is session-scoped via
    // sessionStorage; reloading and exporting again will surface it.
    if (anySuccess && !authUser && window.JUMP_AUTH_READY) {
      try {
        const prev = parseInt(localStorage.getItem('jump.exportCount') || '0', 10) || 0;
        const next = prev + 1;
        localStorage.setItem('jump.exportCount', String(next));
        if (next === 1 && sessionStorage.getItem('jump.signupPromptDismissed') !== '1') {
          setSignupPromptMode('nudge');
          setSignupPromptOpen(true);
        }
      } catch (_) { /* private mode / disabled storage — silently skip */ }
    }

    setBusy(false);
    setToast('Export complete');
  };

  const total = items.length;
  const done = items.filter(i => i.status === 'done').length;

  // Output destination card — rendered twice (once in each column) and
  // toggled via CSS so it can sit under the queue on desktop and below
  // image sizing on mobile, without fragile display:contents tricks.
  const renderOutputDest = (extraClass) => (
    <div className={`ic-card ${extraClass}`}>
      <div className="ic-card-head"><h3>Output destination</h3></div>
      <div className="ic-card-body">
        <div className="ic-dest-group">
          <button
            type="button"
            className={`ic-dest-option ${!outDir ? 'active' : ''}`}
            onClick={() => setOutDir(null)}
          >
            <span className="ic-dest-radio" />
            <span className="ic-dest-body">
              <span className="ic-dest-title"><IconFolder /> Downloads folder <span className="ic-dest-default">default</span></span>
              <span className="ic-dest-sub">Files save to your browser's default Downloads location.</span>
            </span>
          </button>

          <button
            type="button"
            className={`ic-dest-option ${outDir ? 'active' : ''} ${!supportsFs ? 'disabled' : ''}`}
            onClick={supportsFs ? pickOutDir : undefined}
            disabled={!supportsFs}
          >
            <span className="ic-dest-radio" />
            <span className="ic-dest-body">
              <span className="ic-dest-title">
                <IconFolder /> {outDir ? `Custom: ${outDir.name}` : 'Choose custom folder…'}
              </span>
              <span className="ic-dest-sub">
                {supportsFs
                  ? 'Save directly to a folder you pick on disk.'
                  : 'Custom folder selection only works in Chrome and Edge — your browser will use Downloads.'}
              </span>
            </span>
          </button>
        </div>
        {supportsFs && (
          <p className="ic-hint">
            Tip: custom folder destinations only work in Chrome and Edge. Other browsers will fall back to Downloads.
          </p>
        )}
      </div>
    </div>
  );

  return (
    <div className={`app ${expanded ? 'rail-expanded' : ''}`}>
      <Rail expanded={expanded} setExpanded={setExpanded} page="converter" minimal />

      <main className="main">
        <Header theme={theme} setTheme={setTheme} crumbs={['More tools coming soon!']} minimal />

        <div className="subheader">
          <div className="subheader-left">
            <div className="subheader-eyebrow">
              <span className="dot" /> Tool
            </div>
            <h1>Image Converter</h1>
            <div className="subheader-meta">
              <span><strong>{total}</strong> {total === 1 ? 'image' : 'images'} loaded</span>
              <span className="sep">·</span>
              <span><strong>{done}</strong> exported</span>
              <span className="sep">·</span>
              <span>Output <strong>{fmt.label}</strong></span>
            </div>
          </div>
          <div className="subheader-right">
            <div className="progress-chip" title={`${done} of ${total} done`}>
              <div className="progress-track">
                <div className="progress-fill" style={{ width: `${total ? (done/total)*100 : 0}%` }} />
              </div>
              <span>{total ? Math.round((done/total)*100) : 0}%</span>
            </div>
          </div>
        </div>

        <div className="ic-layout">
          {/* LEFT: drop + image queue + output dest */}
          <section className="ic-col-main">
            <DropZone onFiles={addFiles} hasItems={items.length > 0} />

            {items.length === 0 ? (
              <div className="ic-empty">
                <div className="ic-empty-icon"><IconImageEmpty /></div>
                <div className="ic-empty-title">No images yet</div>
                <div className="ic-empty-sub">Drop images above to add them to the queue.</div>
              </div>
            ) : (
              <div className="ic-queue">
                <div className="ic-queue-head">
                  <div className="ic-queue-title">Queue · {items.length}</div>
                  <button className="btn btn-ghost" onClick={clearAll}><IconTrash /> Clear all</button>
                </div>
                <div className="ic-list">
                  {items.map(it => (
                    <div key={it.id} className="ic-item">
                      <div className="ic-thumb" style={{ backgroundImage: `url(${it.preview})` }} />
                      <div className="ic-meta">
                        <div className="ic-name" title={it.name}>{it.name}</div>
                        <div className="ic-sub">
                          <span className="mono">{it.width}×{it.height}</span>
                          <span className="sep">·</span>
                          <span className="mono">{bytes(it.size)}</span>
                          {it.out && (
                            <>
                              <span className="sep">→</span>
                              <span className="mono ic-out">{it.out.width}×{it.out.height} · {bytes(it.out.size)}</span>
                            </>
                          )}
                        </div>
                      </div>
                      <StatusPillIC status={it.status} />
                      <button className="icon-btn" title="Remove" onClick={() => remove(it.id)}><IconTrash /></button>
                    </div>
                  ))}
                </div>
              </div>
            )}

            {renderOutputDest('ic-out-desktop')}
          </section>

          {/* RIGHT: settings panel */}
          <aside className="ic-col-side">
            <div className="ic-card">
              <div className="ic-card-head"><h3>File settings</h3></div>
              <div className="ic-card-body">
                <label className="ic-field">
                  <span>Format</span>
                  <div className="seg ic-seg">
                    {FORMATS.map(f => (
                      <button key={f.id} className={format===f.id?'active':''} onClick={() => setFormat(f.id)}>{f.label}</button>
                    ))}
                  </div>
                </label>

                {format !== 'image/png' && (
                  <label className="ic-field">
                    <span>Target file size</span>
                    <div className="ic-target-size">
                      <NumberInput value={targetSize} onChange={setTargetSize} placeholder="optional" />
                      <div className="seg ic-unit-seg">
                        <button className={targetUnit==='MB'?'active':''} onClick={() => setTargetUnit('MB')}>MB</button>
                        <button className={targetUnit==='KB'?'active':''} onClick={() => setTargetUnit('KB')}>KB</button>
                      </div>
                    </div>
                  </label>
                )}
                {format === 'image/png' && (
                  <p className="ic-hint">
                    PNG is lossless — file size is a function of resolution only.
                  </p>
                )}
              </div>
            </div>

            <div className="ic-card">
              <div className="ic-card-head ic-card-head-toggle">
                <h3>Image sizing</h3>
                <Switch on={sizingOn} onChange={setSizingOn} />
              </div>
              {sizingOn && (
                <div className="ic-card-body">
                  <label className="ic-field">
                    <span>Resize mode</span>
                    <select className="ic-input ic-select" value={resize} onChange={(e) => setResize(e.target.value)}>
                      {RESIZE_MODES.filter(m => m.id !== 'none').map(m => <option key={m.id} value={m.id}>{m.label}</option>)}
                    </select>
                  </label>
                  <ResizeFields resize={resize} a={rA} b={rB} setA={setRA} setB={setRB} />
                  <label className="ic-toggle-row">
                    <input type="checkbox" checked={dontEnlarge} onChange={(e) => setDontEnlarge(e.target.checked)} />
                    <span>Don't enlarge — skip upscaling smaller images</span>
                  </label>
                </div>
              )}
            </div>

            {renderOutputDest('ic-out-mobile')}

            <div className="ic-card">
              <div className="ic-card-head ic-card-head-toggle">
                <h3>File naming</h3>
                <Switch on={namingOn} onChange={setNamingOn} />
              </div>
              {namingOn && (
                <div className="ic-card-body">
                  <label className="ic-field">
                    <span>Pattern</span>
                    <select className="ic-input ic-select" value={nameMode} onChange={(e) => setNameMode(e.target.value)}>
                      <option value="custom-prefix">Custom name + number prefix</option>
                      <option value="custom-suffix">Custom name + number suffix</option>
                      <option value="original-prefix">Original name + number prefix</option>
                      <option value="original-suffix">Original name + number suffix</option>
                    </select>
                  </label>
                  {(nameMode === 'custom-prefix' || nameMode === 'custom-suffix') && (
                    <label className="ic-field">
                      <span>Custom name</span>
                      <input className="ic-input" value={customName} onChange={(e) => setCustomName(e.target.value)} placeholder="e.g. aurora-hero" />
                    </label>
                  )}
                  <label className="ic-field">
                    <span>Number starts at</span>
                    <NumberInput value={startNum} onChange={(v) => setStartNum(v === '' ? '' : +v)} placeholder="1" suffix="#" />
                  </label>
                  <div className="ic-preview">
                    <span className="ic-preview-label">Preview</span>
                    <span className="ic-preview-name mono">
                      {buildName({
                        mode: nameMode,
                        custom: customName || 'image',
                        n: +startNum || 1,
                        original: items[0]?.name || 'photo.jpg',
                        ext: fmt.ext,
                      })}
                    </span>
                  </div>
                </div>
              )}
            </div>

            <div className="ic-actions-bar">
              <button className="btn btn-ghost" onClick={reset}>Reset</button>
              <button className="btn btn-primary" onClick={exportAll} disabled={items.length === 0 || busy}>
                <IconExport /> {busy ? 'Exporting…' : `Export ${items.length || ''}`.trim()}
              </button>
            </div>
          </aside>
        </div>

        {toast && <div className="toast">{toast}</div>}
        <SignupPromptModal
          isOpen={signupPromptOpen}
          mode={signupPromptMode}
          onClose={() => {
            setSignupPromptOpen(false);
            // Only mark session-dismissed when closing the soft nudge.
            // The block-mode modal has no "Maybe later" button — backdrop
            // click still closes it, but that's an escape, not a "remind
            // me later", so the gate should still fire on next attempt.
            if (signupPromptMode === 'nudge') {
              try { sessionStorage.setItem('jump.signupPromptDismissed', '1'); } catch (_) {}
            }
          }}
          onSignUp={() => {
            setSignupPromptOpen(false);
            // Don't set the dismissed flag — opening sign-in is engagement
            window.__jumpOpenAuth?.();
          }}
        />
      </main>
    </div>
  );
}

// Tiny memo helper without react/cache imports
function useMemoOrFallback(fn, deps) {
  return React.useMemo(fn, deps);
}

ReactDOM.createRoot(document.getElementById('root')).render(<ImageConverter />);
