// ═════════════════════════════════════════════════════════════════════════════
// MindForge · Layer 3 · ui/pdf-region.jsx
// ─────────────────────────────────────────────────────────────────────────────
// Role     : PDF "region" custom field — editor input (URL + page picker +
//            drag-rectangle canvas) and reveal renderer (re-renders the same
//            rectangle at higher DPI on demand).
// Loaded   : #6
// Reads    : window.{ React, MF, pdfjsLib (CDN) }
//            Network: GET /proxy?url=<encoded> fallback when direct CORS-fetch
//            of the PDF fails (server.cjs in dev, api/proxy.js on Vercel).
// Exposes  : window.{ PDFRegionFieldInput, RevealPdfRegion, normalizePdfRegion }
// See also : docs/ARCHITECTURE.md §"Proxy & CORS"
//            docs/DATA-MODEL.md §"Custom field types · pdf_region"
// ═════════════════════════════════════════════════════════════════════════════
//
// MindForge — PDF region custom field (legacy header preserved below)
//
// A `pdf_region` value is an object:
//   { url: string, page: number (1-based), rect: { x, y, w, h } }
// where rect is expressed in PDF user-space units (1 unit = 1/72 inch),
// origin top-left of the page, so it survives re-rendering at any DPI.
//
// PDF.js is loaded from a CDN in index.html — `window.pdfjsLib` is available
// by the time these components mount.

// ─── Shared document cache ───────────────────────────────────────────────
// PDF.js loadingTask is cheap to instantiate but re-fetching the file on
// every component mount is wasteful. Key by URL.
//
// Cross-origin PDFs (Drive, Dropbox, personal servers without CORS) blow up
// the direct fetch — fine on desktop with a CORS-bypass extension, fatal on
// Android. We retry the same URL through the dev server's /proxy endpoint
// (server.cjs ships a tiny static + CORS proxy combo) and only surface the
// error when both routes fail.
const _pdfCache = new Map();
const _viaProxy = (target) => '/proxy?url=' + encodeURIComponent(target);

const _loadPdf = (url) => {
  if (!window.pdfjsLib) return Promise.reject(new Error('PDF.js non chargé'));
  if (_pdfCache.has(url)) return _pdfCache.get(url);

  const tryFetch = (sourceUrl) =>
    window.pdfjsLib.getDocument({ url: sourceUrl }).promise;

  const promise = tryFetch(url).catch((directErr) =>
    // Direct fetch failed — could be CORS, 404, or network. Try the local
    // proxy. If it succeeds, we're done; otherwise we re-throw the *original*
    // error so the user sees the real cause instead of "proxy 502".
    tryFetch(_viaProxy(url)).catch(() => { throw directErr; })
  ).catch((finalErr) => {
    _pdfCache.delete(url);   // don't cache failures
    throw finalErr;
  });

  _pdfCache.set(url, promise);
  return promise;
};

// Coerce / sanitize a stored value into the canonical shape.
const _normalizeRegion = (v) => {
  if (!v || typeof v !== 'object') return null;
  if (typeof v.url !== 'string' || !v.url.trim()) return null;
  const page = Math.max(1, Number(v.page) || 1);
  const r = v.rect || {};
  const rect = {
    x: Math.max(0, Number(r.x) || 0),
    y: Math.max(0, Number(r.y) || 0),
    w: Math.max(0, Number(r.w) || 0),
    h: Math.max(0, Number(r.h) || 0),
  };
  return { url: v.url.trim(), page, rect };
};

// ─── Editor input ────────────────────────────────────────────────────────
// URL + page navigator + canvas preview with click-drag rectangle.

const PDFRegionFieldInput = ({ value, onChange }) => {
  const v = _normalizeRegion(value) || { url: '', page: 1, rect: { x:0, y:0, w:0, h:0 } };

  const [url, setUrl]         = React.useState(v.url);
  const [pdfDoc, setPdfDoc]   = React.useState(null);
  const [pageNum, setPageNum] = React.useState(v.page);
  const [pageObj, setPageObj] = React.useState(null);
  const [pageSize, setPageSize] = React.useState(null); // { width, height } in PDF units
  const [renderScale, setRenderScale] = React.useState(1);
  const [error, setError]     = React.useState('');
  const [busy, setBusy]       = React.useState(false);

  const canvasRef = React.useRef(null);
  const wrapRef   = React.useRef(null);

  // Local rectangle in PDF coordinates — pushed up to `onChange` on every
  // commit (mouse-up / page change / URL change).
  const [rect, setRect] = React.useState(v.rect);

  // Drag state (in pixel coords on the preview canvas).
  const [drag, setDrag] = React.useState(null); // { startX, startY, x, y }

  const pushChange = React.useCallback((next) => {
    onChange(_normalizeRegion(next));
  }, [onChange]);

  // ── Load the document when the URL changes ──
  React.useEffect(() => {
    if (!url.trim()) { setPdfDoc(null); setError(''); return; }
    setBusy(true); setError('');
    let cancelled = false;
    _loadPdf(url.trim())
      .then(doc => { if (!cancelled) { setPdfDoc(doc); setBusy(false); } })
      .catch(e => {
        if (cancelled) return;
        setPdfDoc(null); setBusy(false);
        // The most common failure mode is CORS. Steer the user toward a
        // host known to serve PDFs with Access-Control-Allow-Origin: *.
        setError(
          `Impossible de charger le PDF : ${e.message}\n\n` +
          `Cause la plus probable : le host ne renvoie pas les en-têtes CORS. ` +
          `Utilise un host compatible — par ex. GitHub raw, jsDelivr (gh), ` +
          `un bucket Cloudflare R2 / Backblaze B2 public, ou une passerelle IPFS.`
        );
      });
    return () => { cancelled = true; };
  }, [url]);

  // Clamp page number when document changes.
  React.useEffect(() => {
    if (!pdfDoc) return;
    if (pageNum > pdfDoc.numPages) setPageNum(pdfDoc.numPages);
    if (pageNum < 1) setPageNum(1);
  }, [pdfDoc, pageNum]);

  // ── Render the page to the preview canvas ──
  React.useEffect(() => {
    if (!pdfDoc || !canvasRef.current || !wrapRef.current) return;
    let cancelled = false;
    pdfDoc.getPage(pageNum).then(page => {
      if (cancelled) return;
      const v1 = page.getViewport({ scale: 1 });
      const wrapW = wrapRef.current.clientWidth || 600;
      const scale = Math.max(0.2, wrapW / v1.width);
      const viewport = page.getViewport({ scale });
      const canvas = canvasRef.current;
      canvas.width  = viewport.width;
      canvas.height = viewport.height;
      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      page.render({ canvasContext: ctx, viewport }).promise.then(() => {
        if (cancelled) return;
        setPageObj(page);
        setPageSize({ width: v1.width, height: v1.height });
        setRenderScale(scale);
      });
    });
    return () => { cancelled = true; };
  }, [pdfDoc, pageNum]);

  // ── Drag handlers (mouse + touch via pointer events) ──
  const onPointerDown = (e) => {
    if (!canvasRef.current) return;
    e.currentTarget.setPointerCapture(e.pointerId);
    const rectEl = canvasRef.current.getBoundingClientRect();
    const x = e.clientX - rectEl.left;
    const y = e.clientY - rectEl.top;
    setDrag({ startX: x, startY: y, x, y });
  };
  const onPointerMove = (e) => {
    if (!drag || !canvasRef.current) return;
    const rectEl = canvasRef.current.getBoundingClientRect();
    const x = Math.max(0, Math.min(rectEl.width,  e.clientX - rectEl.left));
    const y = Math.max(0, Math.min(rectEl.height, e.clientY - rectEl.top));
    setDrag(d => ({ ...d, x, y }));
  };
  const onPointerUp = () => {
    if (!drag || !canvasRef.current || !pageSize) { setDrag(null); return; }
    const rectEl = canvasRef.current.getBoundingClientRect();
    const x0 = Math.min(drag.startX, drag.x);
    const y0 = Math.min(drag.startY, drag.y);
    const x1 = Math.max(drag.startX, drag.x);
    const y1 = Math.max(drag.startY, drag.y);
    // Convert pixel → PDF units. Canvas is rendered at renderScale; CSS
    // size may differ from canvas pixel size (DPR aside, the canvas is
    // sized in CSS px == canvas px in this layout).
    const cssToCanvas = canvasRef.current.width / rectEl.width;
    const toPdf = (v) => (v * cssToCanvas) / renderScale;
    const nextRect = {
      x: toPdf(x0),
      y: toPdf(y0),
      w: toPdf(x1 - x0),
      h: toPdf(y1 - y0),
    };
    setDrag(null);
    if (nextRect.w > 4 && nextRect.h > 4) {
      setRect(nextRect);
      pushChange({ url: url.trim(), page: pageNum, rect: nextRect });
    }
  };

  const clearRect = () => {
    setRect({ x:0, y:0, w:0, h:0 });
    pushChange({ url: url.trim(), page: pageNum, rect: { x:0, y:0, w:0, h:0 } });
  };

  // Current rect overlay (in canvas px) — either from the live drag or the
  // committed rect converted back through renderScale.
  const overlay = (() => {
    if (drag) {
      const x0 = Math.min(drag.startX, drag.x);
      const y0 = Math.min(drag.startY, drag.y);
      return { x: x0, y: y0, w: Math.abs(drag.x - drag.startX), h: Math.abs(drag.y - drag.startY) };
    }
    if (rect && rect.w > 0 && rect.h > 0) {
      return {
        x: rect.x * renderScale,
        y: rect.y * renderScale,
        w: rect.w * renderScale,
        h: rect.h * renderScale,
      };
    }
    return null;
  })();

  const setPage = (n) => {
    if (!pdfDoc) return;
    const clamped = Math.max(1, Math.min(pdfDoc.numPages, n));
    setPageNum(clamped);
    pushChange({ url: url.trim(), page: clamped, rect });
  };

  // Tracks the last paste outcome so we can give the user a quick visual
  // confirmation when a JSON blob is recognised and unpacked.
  const [jsonHint, setJsonHint] = React.useState(null);  // null | 'ok' | 'bad'

  const commitUrl = (next) => {
    const trimmed = (next || '').trim();
    // Smart paste: if the user drops a full pdf_region JSON
    // ({"url":…,"page":…,"rect":…}) — the exact shape exported / round-tripped
    // through CSV — restore the whole region (URL + page + rectangle) at once
    // instead of treating the blob as a URL. Anything that isn't valid region
    // JSON falls through to the plain-URL path below.
    if (trimmed.startsWith('{')) {
      try {
        const region = _normalizeRegion(JSON.parse(trimmed));
        if (region) {
          setUrl(region.url);          // the input now shows just the clean URL
          setPageNum(region.page);
          setRect(region.rect);
          pushChange(region);
          setJsonHint('ok');
          return;
        }
        setJsonHint('bad');            // parsed but missing url/page/rect
      } catch {
        setJsonHint('bad');            // looked like JSON but didn't parse
      }
      // Fall through — store what the user typed verbatim so they can fix it.
      setUrl(trimmed);
      return;
    }
    // Plain URL: reset page + rectangle so a new document starts clean.
    setJsonHint(null);
    setUrl(trimmed);
    pushChange({ url: trimmed, page: 1, rect: { x:0, y:0, w:0, h:0 } });
    setRect({ x:0, y:0, w:0, h:0 });
    setPageNum(1);
  };

  return (
    <div style={{display:'flex', flexDirection:'column', gap: 10}}>
      <input
        // type="text" (not "url") so HTML5 validation doesn't mark a pasted
        // JSON blob as :invalid and so mobile keyboards stay general-purpose.
        type="text"
        inputMode="url"
        value={url}
        onChange={(e) => commitUrl(e.target.value)}
        placeholder='URL https://… OU JSON {"url":…,"page":…,"rect":…}'
        spellCheck={false}
        autoComplete="off"
        autoCorrect="off"
        autoCapitalize="off"
        style={{
          padding:'10px 12px',
          background: MF.card,
          border:`1px solid ${jsonHint === 'ok' ? MF.good : jsonHint === 'bad' ? MF.bad : MF.line}`,
          borderRadius: 10, color: MF.text, fontSize: 13,
          outline:'none', fontFamily: 'inherit',
          transition: 'border-color .15s ease',
        }}
      />
      {jsonHint === 'ok' && (
        <div style={{fontSize: 11, color: MF.good, lineHeight: 1.5}}>
          ✓ JSON détecté — URL, page et zone restaurées d'un seul coup.
        </div>
      )}
      {jsonHint === 'bad' && (
        <div style={{fontSize: 11, color: MF.bad, lineHeight: 1.5}}>
          ✕ JSON invalide — la forme attendue est <code style={{fontFamily: MF.mono}}>{`{"url":"…","page":1,"rect":{"x":…,"y":…,"w":…,"h":…}}`}</code>.
        </div>
      )}
      <div style={{fontSize: 11, color: MF.textMute, lineHeight: 1.5}}>
        Deux façons de remplir ce champ :
        <ul style={{margin:'4px 0 0 16px', padding:0}}>
          <li><b style={{color: MF.textDim}}>Coller une URL</b> de PDF — puis trace la zone sur la prévisualisation ci-dessous.</li>
          <li><b style={{color: MF.textDim}}>Coller un JSON complet</b> <code style={{fontFamily: MF.mono}}>{`{url,page,rect}`}</code> — restaure URL, page et rectangle en un seul geste (utile pour ré-importer une zone exportée en CSV).</li>
        </ul>
        Hosts compatibles (CORS ouvert) :
        {' '}<span style={{color: MF.textDim}}>GitHub raw</span>,
        {' '}<span style={{color: MF.textDim}}>jsDelivr (gh)</span>,
        {' '}<span style={{color: MF.textDim}}>Cloudflare R2 / Backblaze B2 public</span>,
        {' '}<span style={{color: MF.textDim}}>IPFS gateway</span>.
        {' '}Pour Drive / Dropbox / serveurs perso le serveur de dev local
        relaie automatiquement via <code style={{fontFamily: MF.mono}}>/proxy</code>
        (npm run dev) — fonctionne aussi sur mobile.
      </div>

      {error && (
        <div style={{
          padding:'10px 12px', borderRadius: 8,
          background:'rgba(244,138,138,.08)', border:`1px solid ${MF.bad}`,
          color: MF.bad, fontSize: 12, lineHeight: 1.5,
          whiteSpace:'pre-line',
        }}>{error}</div>
      )}

      {busy && !pdfDoc && (
        <div style={{
          padding:'12px', textAlign:'center',
          color: MF.textMute, fontSize: 12,
        }}>Chargement du PDF…</div>
      )}

      {pdfDoc && (
        <>
          {/* Page navigator */}
          <div style={{
            display:'flex', alignItems:'center', gap: 8,
            fontSize: 12, color: MF.textDim,
          }}>
            <button
              onClick={() => setPage(pageNum - 1)}
              disabled={pageNum <= 1}
              className="mf-tap"
              style={{
                padding:'6px 12px', borderRadius: 8,
                background: MF.card, border:`1px solid ${MF.line}`,
                color: pageNum <= 1 ? MF.textMute : MF.textDim,
                cursor: pageNum <= 1 ? 'not-allowed' : 'pointer',
                fontFamily:'inherit',
              }}
            >←</button>
            <span style={{fontVariantNumeric:'tabular-nums', minWidth: 80, textAlign:'center'}}>
              Page <b style={{color: MF.text}}>{pageNum}</b> / {pdfDoc.numPages}
            </span>
            <button
              onClick={() => setPage(pageNum + 1)}
              disabled={pageNum >= pdfDoc.numPages}
              className="mf-tap"
              style={{
                padding:'6px 12px', borderRadius: 8,
                background: MF.card, border:`1px solid ${MF.line}`,
                color: pageNum >= pdfDoc.numPages ? MF.textMute : MF.textDim,
                cursor: pageNum >= pdfDoc.numPages ? 'not-allowed' : 'pointer',
                fontFamily:'inherit',
              }}
            >→</button>
            <div style={{flex:1}}/>
            {overlay && (
              <button
                onClick={clearRect}
                className="mf-tap"
                style={{
                  padding:'6px 10px', borderRadius: 8,
                  background:'transparent', border:`1px dashed ${MF.line2}`,
                  color: MF.textMute, fontSize: 11, fontWeight: 600,
                  cursor:'pointer', fontFamily:'inherit',
                }}
              >× Effacer le cadre</button>
            )}
          </div>

          <div style={{fontSize: 11, color: MF.textMute}}>
            Glisse pour tracer la zone à afficher dans le reveal.
          </div>

          {/* Preview canvas with overlay */}
          <div
            ref={wrapRef}
            style={{
              position:'relative', width:'100%',
              background:'#000', border:`1px solid ${MF.line}`,
              borderRadius: 10, overflow:'hidden',
              touchAction: 'none',
            }}
          >
            <canvas
              ref={canvasRef}
              onPointerDown={onPointerDown}
              onPointerMove={onPointerMove}
              onPointerUp={onPointerUp}
              onPointerCancel={onPointerUp}
              style={{
                display:'block', width:'100%', height:'auto',
                cursor: drag ? 'crosshair' : 'crosshair',
                userSelect:'none',
              }}
            />
            {overlay && (
              <div style={{
                position:'absolute',
                left:   `${(overlay.x / (canvasRef.current?.width || 1)) * 100}%`,
                top:    `${(overlay.y / (canvasRef.current?.height || 1)) * 100}%`,
                width:  `${(overlay.w / (canvasRef.current?.width || 1)) * 100}%`,
                height: `${(overlay.h / (canvasRef.current?.height || 1)) * 100}%`,
                border:`2px solid ${MF.purple}`,
                background:'rgba(167,139,250,.18)',
                pointerEvents:'none',
                boxShadow:'0 0 0 9999px rgba(0,0,0,.45)',
              }}/>
            )}
          </div>
        </>
      )}
    </div>
  );
};

// ─── Reveal renderer ────────────────────────────────────────────────────
// Re-renders the same rectangle from the same source PDF at a higher DPI,
// crops to the rect on a small canvas, displays it.

const RevealPdfRegion = ({ value }) => {
  const v = _normalizeRegion(value);
  const canvasRef = React.useRef(null);
  const wrapRef   = React.useRef(null);
  const [state, setState] = React.useState({ loading: true, error: '' });

  React.useEffect(() => {
    if (!v) { setState({ loading: false, error: 'Pas de zone définie.' }); return; }
    if (!window.pdfjsLib) { setState({ loading: false, error: 'PDF.js indisponible.' }); return; }
    if (!v.rect.w || !v.rect.h) { setState({ loading: false, error: 'Trace une zone dans l\'éditeur.' }); return; }

    let cancelled = false;
    setState({ loading: true, error: '' });

    _loadPdf(v.url)
      .then(doc => doc.getPage(Math.min(v.page, doc.numPages)))
      .then(page => {
        if (cancelled) return;
        // Choose a DPI that fills the wrapper width at decent quality.
        const wrapW  = wrapRef.current?.clientWidth || 480;
        const scale  = Math.max(0.5, wrapW / v.rect.w);
        const view   = page.getViewport({ scale });

        // Render the full page into an off-screen canvas, then copy just
        // the rect into the visible one. Off-screen could be wasteful for
        // huge pages; for typical reveal sizes it's fine.
        const off = document.createElement('canvas');
        off.width  = view.width;
        off.height = view.height;
        return page.render({ canvasContext: off.getContext('2d'), viewport: view }).promise
          .then(() => ({ off, scale }));
      })
      .then((res) => {
        if (cancelled || !res || !canvasRef.current) return;
        const { off, scale } = res;
        const cropX = v.rect.x * scale;
        const cropY = v.rect.y * scale;
        const cropW = v.rect.w * scale;
        const cropH = v.rect.h * scale;
        const out = canvasRef.current;
        out.width  = cropW;
        out.height = cropH;
        out.getContext('2d').drawImage(off, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);
        setState({ loading: false, error: '' });
      })
      .catch(e => {
        if (cancelled) return;
        setState({ loading: false, error: `PDF illisible : ${e.message}` });
      });

    return () => { cancelled = true; };
  }, [v?.url, v?.page, v?.rect?.x, v?.rect?.y, v?.rect?.w, v?.rect?.h]);

  return (
    <div ref={wrapRef} style={{
      width:'100%', borderRadius: 10, overflow:'hidden',
      background: MF.card, border:`1px solid ${MF.line}`,
      minHeight: 40,
      display:'flex', alignItems:'center', justifyContent:'center',
    }}>
      {state.loading && <span style={{padding: 12, fontSize: 12, color: MF.textMute}}>Rendu de la zone…</span>}
      {state.error && (
        <span style={{padding: 12, fontSize: 12, color: MF.textMute, textAlign:'center'}}>{state.error}</span>
      )}
      <canvas
        ref={canvasRef}
        style={{
          display: state.loading || state.error ? 'none' : 'block',
          maxWidth:'100%', height:'auto',
        }}
      />
    </div>
  );
};

Object.assign(window, { PDFRegionFieldInput, RevealPdfRegion, normalizePdfRegion: _normalizeRegion });
