// Processing + Result modals.
// Talks to /gradio_api/* directly via fetch — bypasses @gradio/client (which
// hardcodes /gradio/config on hf.space URLs and 404s against gradio.Server).
// Auth comes from the HF OAuth session cookie set after /login/huggingface;
// same-origin fetches forward it automatically with credentials: 'include'.

// Client-side resize: prevents huge phone photos from OOMing ZeroGPU on decode.
// Server still resizes to 1024 longest edge; this is an upstream safety net.
async function resizeImageFile(file, maxEdge = 2048, quality = 0.92) {
  if (!/^image\//.test(file.type)) return file;
  const bitmap = await createImageBitmap(file);
  const { width: w, height: h } = bitmap;
  if (Math.max(w, h) <= maxEdge) { bitmap.close?.(); return file; }
  const scale = maxEdge / Math.max(w, h);
  const tw = Math.round(w * scale);
  const th = Math.round(h * scale);
  const canvas = document.createElement('canvas');
  canvas.width = tw; canvas.height = th;
  canvas.getContext('2d').drawImage(bitmap, 0, 0, tw, th);
  bitmap.close?.();
  const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', quality));
  return new File([blob], (file.name || 'upload').replace(/\.[^.]+$/, '') + '.jpg', { type: 'image/jpeg' });
}

// Generate a URL-safe session id. Used to key the server-side result store
// so the frontend can recover after a tab-away/screen-lock.
function newSessionId() {
  if (window.crypto && crypto.randomUUID) return crypto.randomUUID().replace(/-/g, '');
  return Array.from(crypto.getRandomValues(new Uint8Array(16)))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}

function ProcessingModal({ open, file, onDone, onError, onClose }) {
  const [progress, setProgress] = React.useState(0);
  const [step, setStep] = React.useState(0);
  const [imageUrl, setImageUrl] = React.useState(null);
  const cancelRef = React.useRef({ cancelled: false });

  const steps = [
    { t: 'Reading the brief', s: 'STEP 1' },
    { t: 'Welding the joints', s: 'STEP 2' },
    { t: 'Casting in plastic', s: 'STEP 3' },
    { t: 'Out of the oven', s: 'STEP 4' },
    { t: 'Painting final details', s: 'STEP 5' },
  ];

  React.useEffect(() => {
    if (!open || !file) return;

    cancelRef.current = { cancelled: false };
    setProgress(0);
    setStep(0);
    const url = URL.createObjectURL(file);
    setImageUrl(url);

    // Stamp the URL with a session id so we can recover the result after
    // a tab-away. Cleared by the App component on modal close.
    const sessionId = newSessionId();
    const params = new URLSearchParams(window.location.search);
    params.set('session', sessionId);
    window.history.replaceState(null, '', window.location.pathname + '?' + params.toString());

    let p = 0;
    const id = setInterval(() => {
      if (cancelRef.current.cancelled) { clearInterval(id); return; }
      p += 0.6 + Math.random() * 1.2;
      if (p > 92) p = 92;
      setProgress(p);
      setStep(Math.min(4, Math.floor((p / 100) * 5)));
    }, 120);

    (async () => {
      try {
        const upload = await resizeImageFile(file, 2048, 0.92);

        // Use @gradio/client (loaded via CDN in index.html). It does the
        // ZeroGPU handshake — sends `supports-zerogpu-headers: 1` and exchanges
        // the iframe's __sign for a fresh proxy token. Manual fetch skips this
        // and the scheduler returns "Expired ZeroGPU proxy token".
        let tries = 0;
        while ((!window.__gradio || !window.__gradio.Client) && tries < 100) {
          await new Promise(r => setTimeout(r, 50));
          tries++;
        }
        if (!window.__gradio || !window.__gradio.Client) {
          throw new Error('Gradio client failed to load from CDN');
        }
        const { Client, handle_file } = window.__gradio;
        const client = await Client.connect(window.location.origin);
        const result = await client.predict('/action', {
          image_path: handle_file(upload),
          session_id: sessionId,
        });
        if (cancelRef.current.cancelled) return;

        clearInterval(id);
        setProgress(100);
        setStep(4);
        const out = result?.data?.[0];
        const outUrl = typeof out === 'string' ? out : (out?.url || out?.path);
        setTimeout(() => { if (!cancelRef.current.cancelled) onDone(outUrl); }, 400);
        return;
      } catch (err) {
        clearInterval(id);
        if (!cancelRef.current.cancelled) {
          console.error('Inference failed:', err);
          onError && onError(err);
        }
      }
    })();

    return () => {
      cancelRef.current.cancelled = true;
      clearInterval(id);
      URL.revokeObjectURL(url);
    };
  }, [open, file]);

  if (!open) return null;
  return (
    <div className={`modal ${open ? 'on' : ''}`}>
      <div className="panel">
        <div className="phead">
          <span>ACTION · PROCESSING · SESSION {Math.floor(Math.random() * 9999).toString(16).toUpperCase().padStart(4, '0')}</span>
          <span className="close" onClick={onClose}>CLOSE ×</span>
        </div>
        <div className="proc">
          <div className="preview">
            {imageUrl && <img src={imageUrl} alt="preview" />}
            <div className="scan" />
            <span className="corner tl"/><span className="corner tr"/>
            <span className="corner bl"/><span className="corner br"/>
          </div>
          <div className="right">
            <div className="label">── MOULDING</div>
            <h2 className="u-dot">{progress < 100 ? 'CASTING' : 'COMPLETING'}</h2>
            <div className="pmeta"><span>ACT/BF16 · 1:6</span><span>{Math.floor(progress)}%</span></div>
            <div className="bar"><span style={{ width: progress + '%' }} /></div>
            <div className="pmeta" style={{ marginTop: 8 }}><span>QWEN-EDIT-2511</span></div>
            <div className="steps">
              {steps.map((s, i) => (
                <div className={`step ${i < step ? 'done' : ''} ${i === step ? 'active' : ''}`} key={i}>
                  <span className="ic"/>
                  <span className="t">{s.t}</span>
                  <span className="s">{s.s}</span>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function ResultModal({ open, beforeUrl, afterUrl, onClose }) {
  const [split, setSplit] = React.useState(50);
  const [aspect, setAspect] = React.useState(null);
  // Before image rescaled to the after image's exact dimensions for
  // pixel-perfect slider alignment. Display only — doesn't affect download.
  const [alignedBeforeUrl, setAlignedBeforeUrl] = React.useState(null);
  const compareRef = React.useRef();

  // Read natural dimensions from the result image so the compare frame
  // matches its aspect ratio — avoids the 16:9 cover-crop on portraits.
  React.useEffect(() => {
    if (!afterUrl) { setAspect(null); return; }
    const img = new Image();
    img.onload = () => {
      if (img.naturalWidth && img.naturalHeight) {
        setAspect(img.naturalWidth / img.naturalHeight);
      }
    };
    img.src = afterUrl;
  }, [afterUrl]);

  // Rescale the before image (in a canvas) to match the after image's exact
  // dimensions. Pipeline outputs are at native ~1024² and don't match small
  // uploads pixel-for-pixel — without this, the slider drag would shimmer
  // on aspect-ratio differences from mult-of-32 rounding.
  React.useEffect(() => {
    if (!beforeUrl || !afterUrl) { setAlignedBeforeUrl(null); return; }
    let cancelled = false;
    let createdUrl = null;
    const before = new Image();
    const after = new Image();
    let beforeReady = false, afterReady = false;
    const tryDraw = () => {
      if (cancelled || !beforeReady || !afterReady) return;
      const w = after.naturalWidth, h = after.naturalHeight;
      if (!w || !h) return;
      const canvas = document.createElement('canvas');
      canvas.width = w; canvas.height = h;
      canvas.getContext('2d').drawImage(before, 0, 0, w, h);
      canvas.toBlob((blob) => {
        if (cancelled || !blob) return;
        createdUrl = URL.createObjectURL(blob);
        setAlignedBeforeUrl(createdUrl);
      }, 'image/png');
    };
    before.onload = () => { beforeReady = true; tryDraw(); };
    after.onload = () => { afterReady = true; tryDraw(); };
    before.src = beforeUrl;
    after.src = afterUrl;
    return () => {
      cancelled = true;
      if (createdUrl) URL.revokeObjectURL(createdUrl);
    };
  }, [beforeUrl, afterUrl]);

  const onMove = (clientX) => {
    const el = compareRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    const pct = Math.max(0, Math.min(100, ((clientX - r.left) / r.width) * 100));
    setSplit(pct);
  };

  const startDrag = (e) => {
    const move = (ev) => onMove(ev.touches ? ev.touches[0].clientX : ev.clientX);
    const up = () => {
      window.removeEventListener('mousemove', move);
      window.removeEventListener('mouseup', up);
      window.removeEventListener('touchmove', move);
      window.removeEventListener('touchend', up);
    };
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', up);
    window.addEventListener('touchmove', move, { passive: true });
    window.addEventListener('touchend', up);
    e.preventDefault();
  };

  const onDownload = async () => {
    if (!afterUrl) return;
    try {
      const res = await fetch(afterUrl);
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'systms_action.png';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (e) { console.error('Download failed:', e); }
  };

  if (!open) return null;

  return (
    <div className={`modal ${open ? 'on' : ''}`}>
      <div className="panel">
        <div className="phead">
          <span>ACTION · RESULT · QWEN-EDIT-2511</span>
          <span className="close" onClick={onClose}>CLOSE ×</span>
        </div>
        <div className="res">
          <div
            className="compare"
            ref={compareRef}
            style={{ '--split': split + '%', ...(aspect ? { aspectRatio: aspect } : {}) }}
            onMouseDown={(e) => { onMove(e.clientX); startDrag(e); }}
          >
            <div className="layer before"><img src={alignedBeforeUrl || beforeUrl} alt="before" /></div>
            <div className="layer after"><img src={afterUrl || beforeUrl} alt="after" /></div>
            <div className="tag l">SUBJECT · SOURCE</div>
            <div className="tag r">FIGURE · MINT</div>
            <div className="handle" onMouseDown={startDrag} onTouchStart={startDrag} />
            <div className="knob" onMouseDown={startDrag} onTouchStart={startDrag}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2">
                <path d="M8 6l-4 6 4 6"/><path d="M16 6l4 6-4 6"/>
              </svg>
            </div>
          </div>
          <div className="rbar">
            <div className="stats">
              <div className="s"><span className="n">MINT IN BOX</span></div>
              <div className="s"><span className="n">ACTION POSE</span></div>
              <div className="s"><span className="n">QWEN-EDIT-2511</span></div>
            </div>
            <div className="actions">
              <button className="btn btn-ghost" onClick={onClose}>Re-process</button>
              <button className="btn btn-primary" onClick={onDownload}>Download ↓</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { ProcessingModal, ResultModal });
