/* global React, ReactDOM */
const { useState, useEffect, useRef, useCallback, useMemo } = React;

// ============================================================
// CONFIG
// ============================================================
const ACCENT = "#3b82f6"; // blue
const POLL_INTERVAL_MS = 1500;

const FORMAT_OPTIONS = [
  { value: "png",  label: "PNG — sans perte (gros fichiers)",     ext: "png",  desc: "Sans perte (gros fichiers)" },
  { value: "webp", label: "WebP — qualité 95 (compact)",          ext: "webp", desc: "Qualité 95 (compact)" },
  { value: "jpg",  label: "JPEG — qualité 92 (compact)",          ext: "jpg",  desc: "Qualité 92 (compact)" },
  { value: "avif", label: "AVIF — qualité 80 (très compact)",     ext: "avif", desc: "Qualité 80 (très compact)" },
];

const FALLBACK_GRADIENTS = [
  "linear-gradient(135deg, #1e3a8a 0%, #7c3aed 50%, #ec4899 100%)",
  "linear-gradient(135deg, #064e3b 0%, #0891b2 50%, #67e8f9 100%)",
  "linear-gradient(135deg, #7c2d12 0%, #ea580c 50%, #fbbf24 100%)",
  "linear-gradient(135deg, #581c87 0%, #be185d 50%, #fb7185 100%)",
  "linear-gradient(135deg, #134e4a 0%, #14b8a6 50%, #a7f3d0 100%)",
  "linear-gradient(135deg, #1e1b4b 0%, #3730a3 50%, #818cf8 100%)",
  "linear-gradient(135deg, #831843 0%, #db2777 50%, #fbcfe8 100%)",
  "linear-gradient(135deg, #14532d 0%, #16a34a 50%, #bbf7d0 100%)",
  "linear-gradient(135deg, #422006 0%, #92400e 50%, #fde68a 100%)",
  "linear-gradient(135deg, #0c4a6e 0%, #0284c7 50%, #7dd3fc 100%)",
  "linear-gradient(135deg, #4c1d95 0%, #7e22ce 50%, #c4b5fd 100%)",
  "linear-gradient(135deg, #18181b 0%, #404040 50%, #a3a3a3 100%)",
];

// ============================================================
// HELPERS
// ============================================================
function fmtBytes(b) {
  if (!b) return "—";
  if (b < 1024) return b + " B";
  if (b < 1024 * 1024) return (b / 1024).toFixed(1) + " KB";
  if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + " MB";
  return (b / (1024 * 1024 * 1024)).toFixed(2) + " GB";
}

function fmtTime(s) {
  if (s == null || isNaN(s)) return "—";
  if (s < 60) return Math.round(s) + "s";
  const m = Math.floor(s / 60);
  const r = Math.round(s - m * 60);
  return `${m}m ${r.toString().padStart(2, "0")}s`;
}

function gradientFor(seed) {
  let h = 0;
  for (let i = 0; i < (seed || "").length; i++) h = (h * 31 + seed.charCodeAt(i)) >>> 0;
  return FALLBACK_GRADIENTS[h % FALLBACK_GRADIENTS.length];
}

// ============================================================
// ICONS
// ============================================================
const Icon = {
  Upload: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 16V4M12 4l-5 5M12 4l5 5" />
      <path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
    </svg>
  ),
  Image: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="3" width="18" height="18" rx="2" />
      <circle cx="8.5" cy="8.5" r="1.5" />
      <path d="M21 15l-5-5L5 21" />
    </svg>
  ),
  Archive: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="3" width="18" height="4" rx="1" />
      <path d="M5 7v13a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V7" />
      <path d="M10 11h4M10 15h4" />
    </svg>
  ),
  Download: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 4v12M12 16l-5-5M12 16l5-5" />
      <path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
    </svg>
  ),
  Check: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M5 12l5 5L20 7" />
    </svg>
  ),
  X: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M6 6l12 12M18 6L6 18" />
    </svg>
  ),
  Sparkles: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8" />
    </svg>
  ),
  Refresh: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 12a9 9 0 0 1 15-6.7L21 8" />
      <path d="M21 3v5h-5" />
      <path d="M21 12a9 9 0 0 1-15 6.7L3 16" />
      <path d="M3 21v-5h5" />
    </svg>
  ),
  Cpu: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="6" y="6" width="12" height="12" rx="1" />
      <path d="M9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3" />
      <rect x="9" y="9" width="6" height="6" />
    </svg>
  ),
  Trash: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
      <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
    </svg>
  ),
  Eye: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
      <circle cx="12" cy="12" r="3" />
    </svg>
  ),
};

const IMAGE_EXTS = /\.(jpe?g|png|webp|avif)$/i;
const ARCHIVE_EXTS = /\.zip$/i;

// ============================================================
// DROP ZONE
// ============================================================
function DropZone({ onFiles, accent }) {
  const [dragOver, setDragOver] = useState(false);
  const inputRef = useRef(null);

  const handle = (filesList) => {
    const files = Array.from(filesList || []).filter(
      (f) => IMAGE_EXTS.test(f.name) || ARCHIVE_EXTS.test(f.name)
    );
    if (!files.length) return;
    onFiles(files);
  };

  return (
    <div
      className={`dropzone ${dragOver ? "is-over" : ""}`}
      onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
      onDragLeave={() => setDragOver(false)}
      onDrop={(e) => { e.preventDefault(); setDragOver(false); handle(e.dataTransfer.files); }}
      onClick={() => inputRef.current?.click()}
      role="button"
      tabIndex={0}
    >
      <input
        ref={inputRef}
        type="file"
        accept="image/jpeg,image/png,image/webp,image/avif,.zip"
        multiple
        style={{ display: "none" }}
        onChange={(e) => handle(e.target.files)}
      />

      <div className="dropzone-grid" aria-hidden="true">
        {Array.from({ length: 24 }).map((_, i) => (
          <div key={i} className="dropzone-cell" style={{ animationDelay: `${i * 30}ms` }} />
        ))}
      </div>

      <div className="dropzone-inner">
        <div className="dropzone-icon-stack">
          <div className="dropzone-icon dropzone-icon--back">
            <Icon.Archive width="32" height="32" />
          </div>
          <div className="dropzone-icon dropzone-icon--front" style={{ background: accent }}>
            <Icon.Upload width="32" height="32" />
          </div>
        </div>

        <div className="dropzone-headline">
          {dragOver ? "Déposez pour démarrer" : "Glissez vos fichiers ici"}
        </div>
        <div className="dropzone-sub">
          ou <span className="dropzone-link">parcourir</span> — images ou archive ZIP (jusqu'à 100 images)
        </div>

        <div className="dropzone-formats">
          <span className="format-chip">JPEG</span>
          <span className="format-chip">PNG</span>
          <span className="format-chip">WebP</span>
          <span className="format-chip">AVIF</span>
          <span className="format-chip format-chip--accent">ZIP</span>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// SETUP PANEL
// ============================================================
function SetupPanel({ files, accent, format, setFormat, onStart, onReset, isUploading, error }) {
  const isArchive = files.length === 1 && ARCHIVE_EXTS.test(files[0].name);
  const totalSize = files.reduce((s, f) => s + f.size, 0);

  // Object URLs for thumbnails (image files only)
  const thumbs = useMemo(() => {
    return files.map((f) => (IMAGE_EXTS.test(f.name) ? URL.createObjectURL(f) : null));
  }, [files]);
  useEffect(() => () => thumbs.forEach((u) => u && URL.revokeObjectURL(u)), [thumbs]);

  return (
    <div className="setup-panel">
      <div className="setup-summary">
        <div className="setup-summary-icon" style={{ background: accent }}>
          {isArchive ? <Icon.Archive width="22" height="22" /> : <Icon.Image width="22" height="22" />}
        </div>
        <div className="setup-summary-text">
          <div className="setup-summary-title">
            {isArchive
              ? `Archive ZIP — ${files[0].name}`
              : `${files.length} image${files.length > 1 ? "s" : ""} prête${files.length > 1 ? "s" : ""}`}
          </div>
          <div className="setup-summary-detail mono">
            {fmtBytes(totalSize)}
          </div>
        </div>
        <button className="btn-ghost" onClick={onReset} aria-label="Retirer">
          <Icon.Trash width="14" height="14" />
        </button>
      </div>

      {!isArchive && (
        <div className="thumbstrip">
          {files.slice(0, 8).map((f, i) => (
            <div
              key={i}
              className="thumbstrip-item"
              style={{
                background: thumbs[i] ? `center/cover no-repeat url(${thumbs[i]})` : gradientFor(f.name),
              }}
              title={f.name}
            />
          ))}
          {files.length > 8 && (
            <div className="thumbstrip-item thumbstrip-more mono">+{files.length - 8}</div>
          )}
        </div>
      )}

      <div className="setup-controls">
        <div className="setup-field">
          <label className="setup-label mono">FORMAT DE SORTIE</label>
          <div className="setup-format-grid">
            {FORMAT_OPTIONS.map((opt) => (
              <button
                key={opt.value}
                className={`format-card ${format === opt.value ? "is-active" : ""}`}
                onClick={() => setFormat(opt.value)}
                style={format === opt.value ? { borderColor: accent, background: `${accent}14` } : {}}
              >
                <div className="format-card-name">.{opt.ext.toUpperCase()}</div>
                <div className="format-card-desc">{opt.desc}</div>
              </button>
            ))}
          </div>
        </div>

        <div className="setup-field">
          <label className="setup-label mono">MODÈLE</label>
          <div className="model-card">
            <div className="model-card-icon" style={{ color: accent }}>
              <Icon.Cpu width="22" height="22" />
            </div>
            <div className="model-card-text">
              <div className="model-card-name mono">4xRealWebPhoto_v4 (DAT-2)</div>
              <div className="model-card-desc">Optimisé photographie produit & web · ×4 par défaut</div>
            </div>
            <div className="model-card-tag mono" style={{ borderColor: `${accent}55`, color: accent }}>×4</div>
          </div>
        </div>
      </div>

      {error && (
        <div className="err mono" style={{ fontSize: 12 }}>
          {error}
        </div>
      )}

      <button
        className="btn-launch"
        onClick={onStart}
        disabled={isUploading}
        style={{ background: accent, opacity: isUploading ? 0.7 : 1 }}
      >
        <Icon.Sparkles width="16" height="16" />
        {isUploading ? "Téléversement…" : "Lancer l'upscaling"}
        <span className="btn-launch-meta mono">
          {isArchive ? "ZIP" : `${files.length} fichier${files.length > 1 ? "s" : ""}`} · ×4
        </span>
      </button>
    </div>
  );
}

// ============================================================
// QUEUE ROW (during processing) — driven by API state
// ============================================================
function QueueRow({ item, scale, accent, files, jobId }) {
  const isProcessing = item.status === "processing";
  const isDone = item.status === "done";
  const isFailed = item.status === "failed";

  // Local file thumbnail (URL.createObjectURL) — only for direct file uploads, not ZIP-extracted
  const localThumb = useMemo(() => {
    if (!files || item.idx >= files.length) return null;
    const f = files[item.idx];
    if (!f || !IMAGE_EXTS.test(f.name)) return null;
    return URL.createObjectURL(f);
  }, [files, item.idx]);
  useEffect(() => () => localThumb && URL.revokeObjectURL(localThumb), [localThumb]);

  const thumbBg = localThumb
    ? `center/cover no-repeat url(${localThumb})`
    : gradientFor(item.filename);

  const inW = item.input_w, inH = item.input_h;
  const outW = inW ? inW * scale : null, outH = inH ? inH * scale : null;
  const duration = item.duration_ms != null ? item.duration_ms / 1000 : null;

  // Indeterminate progress for "processing" — we don't have sub-step granularity from PyTorch,
  // so we show an animated stripe. For done, full bar. For pending/failed, empty.
  const barWidth = isDone ? "100%" : isProcessing ? "100%" : "0%";

  return (
    <div className={`queue-row queue-row--${item.status}`}>
      <div className="queue-thumb" style={{ background: thumbBg }}>
        {isDone && (
          <div className="queue-thumb-check" style={{ background: accent }}>
            <Icon.Check width="14" height="14" />
          </div>
        )}
      </div>
      <div className="queue-meta">
        <div className="queue-name">{item.filename}</div>
        <div className="queue-detail">
          {inW && (
            <>
              <span className="mono">{inW}×{inH}</span>
              <span className="queue-arrow">→</span>
              <span className="mono mono--accent">{outW}×{outH}</span>
            </>
          )}
          {isDone && (
            <>
              <span className="queue-dot">·</span>
              <span className="mono">{fmtTime(duration)}</span>
              {item.output_size && (
                <>
                  <span className="queue-dot">·</span>
                  <span className="mono">{fmtBytes(item.output_size)}</span>
                </>
              )}
            </>
          )}
          {isFailed && item.error && (
            <>
              <span className="queue-dot">·</span>
              <span className="mono" style={{ color: "#f87171" }}>{item.error}</span>
            </>
          )}
        </div>
        <div className="queue-bar">
          <div
            className={`queue-bar-fill ${isProcessing ? "is-indeterminate" : ""}`}
            style={{
              width: barWidth,
              background: isDone
                ? accent
                : isProcessing
                ? `linear-gradient(90deg, ${accent}66, ${accent}, ${accent}66)`
                : "transparent",
              backgroundSize: isProcessing ? "200% 100%" : undefined,
            }}
          />
        </div>
      </div>
      <div className="queue-status mono">
        {item.status === "pending" && "EN ATTENTE"}
        {isProcessing && "EN COURS"}
        {isFailed && "ÉCHEC"}
        {isDone && <Icon.Check width="16" height="16" style={{ color: accent }} />}
      </div>
    </div>
  );
}

// ============================================================
// PROCESSING VIEW (driven by API polling)
// ============================================================
function ProcessingView({ jobState, files, accent, onCancel }) {
  const [startTime] = useState(Date.now());
  const [elapsed, setElapsed] = useState(0);

  useEffect(() => {
    const t = setInterval(() => setElapsed((Date.now() - startTime) / 1000), 250);
    return () => clearInterval(t);
  }, [startTime]);

  const items = jobState.items || [];
  const total = items.length || jobState.n_images || 1;
  const doneCount = items.filter((it) => it.status === "done").length;
  const remaining = total - doneCount;
  const overallPct = total > 0 ? (doneCount / total) * 100 : 0;
  const avgPerFile = doneCount > 0 ? elapsed / doneCount : 12;
  const eta = remaining > 0 ? remaining * avgPerFile : 0;

  return (
    <div className="processing-view">
      <div className="processing-header">
        <div className="processing-header-left">
          <div className="processing-pill">
            <span className="processing-pulse" style={{ background: accent }} />
            <span className="mono">EN COURS</span>
          </div>
          <div className="processing-title">
            Upscaling x4 · <span className="mono">{doneCount}/{total}</span>
          </div>
        </div>
        <button className="btn-ghost" onClick={onCancel}>
          <Icon.X width="14" height="14" /> Annuler
        </button>
      </div>

      <div className="processing-stats">
        <div className="stat">
          <div className="stat-label mono">PROGRESSION</div>
          <div className="stat-value">{Math.round(overallPct)}<span className="stat-unit">%</span></div>
        </div>
        <div className="stat">
          <div className="stat-label mono">ÉCOULÉ</div>
          <div className="stat-value mono-num">{fmtTime(elapsed)}</div>
        </div>
        <div className="stat">
          <div className="stat-label mono">ETA</div>
          <div className="stat-value mono-num">{eta > 0 ? fmtTime(eta) : "—"}</div>
        </div>
        <div className="stat">
          <div className="stat-label mono">MODÈLE</div>
          <div className="stat-value stat-value--small mono">4xRealWebPhoto</div>
        </div>
      </div>

      <div className="processing-bigbar">
        <div
          className="processing-bigbar-fill"
          style={{
            width: `${overallPct}%`,
            background: `linear-gradient(90deg, ${accent}, ${accent}cc, ${accent})`,
          }}
        />
        <div className="processing-bigbar-shimmer" style={{ left: `${overallPct - 10}%` }} />
      </div>

      <div className="queue-list">
        {items.map((it) => (
          <QueueRow key={it.idx} item={it} scale={4} accent={accent} files={files} jobId={jobState.id} />
        ))}
      </div>
    </div>
  );
}

// ============================================================
// COMPARE PANEL — real input vs upscaled output
// ============================================================
function ComparePanel({ jobId, item, accent, onClose }) {
  const [pos, setPos] = useState(50);
  const inputUrl = `/jobs/${jobId}/items/${item.idx}/input`;
  const outputUrl = `/jobs/${jobId}/items/${item.idx}/download`;
  return (
    <div className="compare-overlay" onClick={onClose}>
      <div className="compare-panel" onClick={(e) => e.stopPropagation()}>
        <div className="compare-header">
          <div className="compare-name">{item.filename}</div>
          <button className="btn-ghost" onClick={onClose}><Icon.X width="14" height="14" /></button>
        </div>
        <div
          className="compare-stage"
          onMouseMove={(e) => {
            const r = e.currentTarget.getBoundingClientRect();
            setPos(Math.max(0, Math.min(100, ((e.clientX - r.left) / r.width) * 100)));
          }}
        >
          <img
            src={inputUrl}
            alt="avant"
            className="compare-side"
            style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" }}
          />
          <img
            src={outputUrl}
            alt="après"
            className="compare-side compare-side--after"
            style={{
              position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover",
              clipPath: `inset(0 0 0 ${pos}%)`,
            }}
          />
          <div className="compare-tag mono">AVANT · {item.input_w}×{item.input_h}</div>
          <div
            className="compare-tag compare-tag--right mono"
            style={{ background: accent }}
          >
            APRÈS · {item.input_w * 4}×{item.input_h * 4}
          </div>
          <div className="compare-handle" style={{ left: `${pos}%`, background: accent }}>
            <div className="compare-handle-knob" style={{ borderColor: accent }}>
              <span style={{ color: accent }}>‹›</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// RESULTS VIEW
// ============================================================
function ResultsView({ jobState, files, accent, onReset }) {
  const [compareIdx, setCompareIdx] = useState(null);
  const items = jobState.items || [];
  const totalIn = items.reduce((s, it) => s + (it.input_size || 0), 0);
  const totalOut = items.reduce((s, it) => s + (it.output_size || 0), 0);
  const totalDuration = items.reduce((s, it) => s + (it.duration_ms || 0), 0) / 1000;
  const fmt = FORMAT_OPTIONS.find((o) => o.value === jobState.output_format) || FORMAT_OPTIONS[0];
  const bundleUrl = `/jobs/${jobState.id}/download`;

  return (
    <div className="results-view">
      <div className="results-header">
        <div className="results-header-left">
          <div className="results-success">
            <div className="results-success-icon" style={{ background: accent }}>
              <Icon.Check width="20" height="20" />
            </div>
            <div>
              <div className="results-title">Traitement terminé</div>
              <div className="results-sub">
                <span className="mono">{items.length}</span> image{items.length > 1 ? "s" : ""} upscalée{items.length > 1 ? "s" : ""} en <span className="mono">{fmtTime(totalDuration)}</span>
              </div>
            </div>
          </div>
        </div>
        <div className="results-header-right">
          <button className="btn-ghost" onClick={onReset}>
            <Icon.Refresh width="14" height="14" /> Nouveau lot
          </button>
          <a className="btn-primary" href={bundleUrl} style={{ background: accent }}>
            <Icon.Download width="14" height="14" />
            Télécharger {items.length > 1 ? "tout (.zip)" : `(.${fmt.ext})`}
          </a>
        </div>
      </div>

      <div className="results-stats">
        <div className="rstat">
          <div className="rstat-label mono">FICHIERS</div>
          <div className="rstat-value">{items.length}</div>
        </div>
        <div className="rstat">
          <div className="rstat-label mono">ENTRÉE</div>
          <div className="rstat-value mono-num">{fmtBytes(totalIn)}</div>
        </div>
        <div className="rstat">
          <div className="rstat-label mono">SORTIE</div>
          <div className="rstat-value mono-num" style={{ color: accent }}>{fmtBytes(totalOut)}</div>
        </div>
        <div className="rstat">
          <div className="rstat-label mono">FORMAT</div>
          <div className="rstat-value rstat-value--small mono">.{fmt.ext.toUpperCase()}</div>
        </div>
        <div className="rstat">
          <div className="rstat-label mono">FACTEUR</div>
          <div className="rstat-value">×4</div>
        </div>
      </div>

      <div className="results-grid">
        {items.map((it) => (
          <ResultCard
            key={it.idx}
            jobId={jobState.id}
            item={it}
            files={files}
            accent={accent}
            onCompare={() => setCompareIdx(it.idx)}
          />
        ))}
      </div>

      {compareIdx !== null && (
        <ComparePanel
          jobId={jobState.id}
          item={items.find((it) => it.idx === compareIdx)}
          accent={accent}
          onClose={() => setCompareIdx(null)}
        />
      )}
    </div>
  );
}

function ResultCard({ jobId, item, files, accent, onCompare }) {
  const outputUrl = `/jobs/${jobId}/items/${item.idx}/download`;
  const localThumb = useMemo(() => {
    if (!files || item.idx >= files.length) return null;
    const f = files[item.idx];
    if (!f || !IMAGE_EXTS.test(f.name)) return null;
    return URL.createObjectURL(f);
  }, [files, item.idx]);
  useEffect(() => () => localThumb && URL.revokeObjectURL(localThumb), [localThumb]);

  // Use the upscaled output as thumbnail (browser scales it down)
  const thumbBg = item.status === "done"
    ? `center/cover no-repeat url(${outputUrl})`
    : (localThumb ? `center/cover no-repeat url(${localThumb})` : gradientFor(item.filename));

  return (
    <div className="result-card">
      <div className="result-thumb" style={{ background: thumbBg }}>
        <div className="result-thumb-overlay">
          <button className="result-action" onClick={onCompare}>
            <Icon.Eye width="14" height="14" /> Aperçu
          </button>
          <a
            className="result-action result-action--primary"
            style={{ background: accent }}
            href={outputUrl}
          >
            <Icon.Download width="14" height="14" />
          </a>
        </div>
        <div className="result-badge mono">×4</div>
      </div>
      <div className="result-info">
        <div className="result-name">{item.filename}</div>
        <div className="result-detail mono">
          {item.input_w && `${item.input_w * 4}×${item.input_h * 4}`}
          {item.output_size && ` · ${fmtBytes(item.output_size)}`}
        </div>
      </div>
    </div>
  );
}

// ============================================================
// AMBIENT BG
// ============================================================
function AmbientBg({ accent }) {
  return (
    <div className="ambient" aria-hidden="true">
      <div className="ambient-blob ambient-blob--1" style={{ background: accent }} />
      <div className="ambient-blob ambient-blob--2" style={{ background: accent }} />
      <div className="ambient-grid" />
    </div>
  );
}

function PhaseStep({ n, label, active, done, accent }) {
  return (
    <div className={`pstep ${active ? "is-active" : ""} ${done ? "is-done" : ""}`}>
      <div className="pstep-num mono" style={active || done ? { borderColor: accent, color: accent } : {}}>
        {done ? <Icon.Check width="12" height="12" /> : n.toString().padStart(2, "0")}
      </div>
      <div className="pstep-label">{label}</div>
    </div>
  );
}
function PhaseConnector({ done, accent }) {
  return (
    <div className="pconn">
      <div className="pconn-fill" style={{ width: done ? "100%" : "0%", background: accent }} />
    </div>
  );
}

// ============================================================
// ROOT APP
// ============================================================
function App() {
  const accent = ACCENT;

  // phase: upload | setup | processing | results
  const [phase, setPhase] = useState("upload");
  const [files, setFiles] = useState([]); // local File[] selected by user
  const [format, setFormat] = useState("png");
  const [jobId, setJobId] = useState(null);
  const [jobState, setJobState] = useState(null);
  const [isUploading, setIsUploading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    document.documentElement.dataset.density = "comfortable";
    document.documentElement.dataset.ambient = "on";
  }, []);

  const handleFiles = (selected) => {
    setFiles(selected);
    setError(null);
    setPhase("setup");
  };

  const handleStart = async () => {
    setIsUploading(true);
    setError(null);
    try {
      const fd = new FormData();
      for (const f of files) fd.append("files", f);
      fd.append("output_format", format);
      const resp = await fetch("/upload", { method: "POST", body: fd });
      if (!resp.ok) {
        let msg = `Erreur HTTP ${resp.status}`;
        try { const j = await resp.json(); if (j.detail) msg = j.detail; } catch {}
        throw new Error(msg);
      }
      const data = await resp.json();
      setJobId(data.job_id);
      setPhase("processing");
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setIsUploading(false);
    }
  };

  // Polling while processing
  useEffect(() => {
    if (!jobId || (phase !== "processing" && phase !== "results")) return;
    let alive = true;
    let timer;
    const tick = async () => {
      try {
        const r = await fetch(`/jobs/${jobId}/status`);
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        const data = await r.json();
        if (!alive) return;
        setJobState(data);
        if (data.status === "done" || data.status === "failed") {
          setPhase(data.status === "done" ? "results" : "processing");
          if (data.status === "failed") setError(data.error || "Le traitement a échoué");
          return;
        }
      } catch (e) {
        if (!alive) return;
        setError(e.message || String(e));
      }
      timer = setTimeout(tick, POLL_INTERVAL_MS);
    };
    tick();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, [jobId, phase]);

  const handleReset = () => {
    setFiles([]);
    setJobId(null);
    setJobState(null);
    setError(null);
    setPhase("upload");
  };

  return (
    <div className="app" style={{ "--accent": accent }}>
      <AmbientBg accent={accent} />

      <header className="topbar">
        <div className="topbar-left">
          <div className="logo">
            <span className="logo-mark" style={{ background: accent }}>
              <Icon.Sparkles width="14" height="14" />
            </span>
            <span className="logo-text">IMG<span className="logo-plus" style={{ color: accent }}>+</span></span>
            <span className="logo-divider" />
            <span className="logo-tag mono">UPSCALING ×4</span>
          </div>
        </div>
        <div className="topbar-right">
          <div className="topbar-status">
            <span className="topbar-dot" style={{ background: accent }} />
            <span className="mono">SERVICE EN LIGNE</span>
          </div>
        </div>
      </header>

      <main className="main">
        <div className="phase-indicator">
          <PhaseStep n={1} label="Upload" active={phase === "upload"} done={phase !== "upload"} accent={accent} />
          <PhaseConnector done={phase !== "upload"} accent={accent} />
          <PhaseStep n={2} label="Configuration" active={phase === "setup"} done={phase === "processing" || phase === "results"} accent={accent} />
          <PhaseConnector done={phase === "processing" || phase === "results"} accent={accent} />
          <PhaseStep n={3} label="Traitement" active={phase === "processing"} done={phase === "results"} accent={accent} />
          <PhaseConnector done={phase === "results"} accent={accent} />
          <PhaseStep n={4} label="Téléchargement" active={phase === "results"} done={false} accent={accent} />
        </div>

        <div className="stage">
          {phase === "upload" && (
            <div className="phase phase--upload">
              <div className="phase-headline">
                <h1>Agrandissez vos images en <span style={{ color: accent }}>×4</span> sans perte de qualité</h1>
                <p>Upscaling IA local via le modèle <span className="mono">4xRealWebPhoto_v4</span>. Images individuelles ou archive ZIP — jusqu'à 100 images par lot.</p>
              </div>

              <DropZone onFiles={handleFiles} accent={accent} />

              <div className="hints">
                <div className="hint">
                  <span className="hint-num mono">01</span>
                  <div>
                    <div className="hint-title">Formats acceptés</div>
                    <div className="hint-desc mono">JPEG · PNG · WebP · AVIF · ZIP</div>
                  </div>
                </div>
                <div className="hint">
                  <span className="hint-num mono">02</span>
                  <div>
                    <div className="hint-title">Sortie</div>
                    <div className="hint-desc mono">×4 résolution — ex. 800×800 → 3200×3200</div>
                  </div>
                </div>
                <div className="hint">
                  <span className="hint-num mono">03</span>
                  <div>
                    <div className="hint-title">Limite</div>
                    <div className="hint-desc mono">100 images max par lot</div>
                  </div>
                </div>
              </div>
            </div>
          )}

          {phase === "setup" && (
            <SetupPanel
              files={files}
              accent={accent}
              format={format}
              setFormat={setFormat}
              onStart={handleStart}
              onReset={handleReset}
              isUploading={isUploading}
              error={error}
            />
          )}

          {phase === "processing" && jobState && (
            <ProcessingView
              jobState={jobState}
              files={files}
              accent={accent}
              onCancel={handleReset}
            />
          )}

          {phase === "processing" && !jobState && (
            <div className="setup-panel" style={{ textAlign: "center" }}>
              <div className="mono" style={{ color: "var(--text-dim)" }}>Initialisation…</div>
            </div>
          )}

          {phase === "results" && jobState && (
            <ResultsView
              jobState={jobState}
              files={files}
              accent={accent}
              onReset={handleReset}
            />
          )}
        </div>
      </main>
    </div>
  );
}

// Mount
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
