// Square image cropper — modal overlay that takes an image File and lets
// the user pan + zoom inside a 1:1 mask. On confirm, returns a new File
// (webp, full crop area at native resolution capped to the cropper's
// display) via the onCrop callback.
//
// Pure React + canvas, no dependencies. Math is straight matrix transforms:
// the loaded image starts at "fit-cover" (smallest scale that fills the
// 1:1 mask), the user can drag to pan and pinch / wheel to zoom. On
// commit we draw the visible mask area into an offscreen canvas at the
// chosen output size and toBlob() it.

(function () {
  // Default crop output size — capped at 1024 to match the server's
  // MAX_IMG. Smaller sources are exported at their natural side length
  // (no upscaling).
  const TARGET_SIZE = 1024;
  const MIN_SIDE    = 256;  // matches server-side probeImage gate
  const PADDING     = 24;   // viewport padding inside the modal

  function ImageCropper({ file, onCrop, onCancel }) {
    const [src, setSrc] = React.useState(null);
    const [img, setImg] = React.useState(null);   // HTMLImageElement once loaded
    const [scale, setScale] = React.useState(1);
    const [minScale, setMinScale] = React.useState(1);
    const [pos, setPos] = React.useState({ x: 0, y: 0 });
    const [busy, setBusy] = React.useState(false);
    const [err, setErr] = React.useState('');

    const dragRef = React.useRef(null);   // { startX, startY, baseX, baseY }
    const containerRef = React.useRef(null);
    const [containerSize, setContainerSize] = React.useState(360);

    // Load file → object URL → HTMLImageElement, set initial fit-cover
    // scale + center the image inside the mask.
    React.useEffect(() => {
      if (!file) return;
      const url = URL.createObjectURL(file);
      setSrc(url);
      const im = new Image();
      im.onload = () => {
        if (im.naturalWidth < MIN_SIDE || im.naturalHeight < MIN_SIDE) {
          setErr(`Bild zu klein (${im.naturalWidth}×${im.naturalHeight}). Minimum: ${MIN_SIDE}×${MIN_SIDE}.`);
          return;
        }
        setImg(im);
      };
      im.onerror = () => setErr('Bild konnte nicht geladen werden.');
      im.src = url;
      return () => URL.revokeObjectURL(url);
    }, [file]);

    // Recompute the minimum scale (= fit-cover) whenever the container
    // resizes or a fresh image loads. Picks whichever axis has more
    // headroom so the smaller of width/height fully covers the mask.
    React.useLayoutEffect(() => {
      if (!img || !containerRef.current) return;
      const rect = containerRef.current.getBoundingClientRect();
      const size = Math.min(rect.width, rect.height) - PADDING * 2;
      setContainerSize(size);
      const m = Math.max(size / img.naturalWidth, size / img.naturalHeight);
      setMinScale(m);
      setScale(m);
      setPos({ x: 0, y: 0 });   // centered
    }, [img]);

    // Drag-to-pan. Position is stored in *image-pixel* offset from the
    // mask's center (so zoom in/out feels natural — the same image
    // point stays under the user's pointer).
    const onPointerDown = (e) => {
      if (!img) return;
      e.preventDefault();
      dragRef.current = {
        startX: e.clientX, startY: e.clientY,
        baseX: pos.x, baseY: pos.y,
      };
      e.currentTarget.setPointerCapture?.(e.pointerId);
    };
    const onPointerMove = (e) => {
      if (!dragRef.current) return;
      e.preventDefault();
      const dx = e.clientX - dragRef.current.startX;
      const dy = e.clientY - dragRef.current.startY;
      setPos(clampPos({
        x: dragRef.current.baseX + dx,
        y: dragRef.current.baseY + dy,
      }, scale));
    };
    const onPointerUp = (e) => {
      if (e.currentTarget.releasePointerCapture && e.pointerId != null) {
        try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
      }
      dragRef.current = null;
    };

    // Wheel = zoom around the center. Stays bounded to [minScale, 4×min]
    // so the user can't shrink the image past fit-cover (would expose
    // the mask edges) or zoom beyond a reasonable detail level.
    const onWheel = (e) => {
      if (!img) return;
      e.preventDefault();
      const delta = e.deltaY < 0 ? 1.08 : 1 / 1.08;
      const next = Math.max(minScale, Math.min(minScale * 4, scale * delta));
      setScale(next);
      setPos(p => clampPos(p, next));
    };

    // Constrain pan so the image always covers the mask. Position units
    // are screen px relative to the mask's center.
    const clampPos = (p, s) => {
      if (!img) return p;
      const halfW = (img.naturalWidth  * s - containerSize) / 2;
      const halfH = (img.naturalHeight * s - containerSize) / 2;
      return {
        x: Math.max(-halfW, Math.min(halfW, p.x)),
        y: Math.max(-halfH, Math.min(halfH, p.y)),
      };
    };

    // Render the cropped square to an offscreen canvas at TARGET_SIZE
    // (or smaller if the source can't reach that), encode as webp Blob,
    // wrap in a File, hand to the parent.
    const commit = async () => {
      if (!img) return;
      setBusy(true);
      try {
        const out = Math.min(TARGET_SIZE, Math.round(containerSize / scale));
        // The visible mask in image-pixel coordinates:
        const cx = img.naturalWidth  / 2 - pos.x / scale;
        const cy = img.naturalHeight / 2 - pos.y / scale;
        const half = (containerSize / scale) / 2;
        const sx = Math.max(0, cx - half);
        const sy = Math.max(0, cy - half);
        const sSize = Math.min(img.naturalWidth - sx, img.naturalHeight - sy, half * 2);

        const canvas = document.createElement('canvas');
        canvas.width = canvas.height = out;
        const cx2d = canvas.getContext('2d');
        cx2d.drawImage(img, sx, sy, sSize, sSize, 0, 0, out, out);

        const blob = await new Promise(res => canvas.toBlob(res, 'image/webp', 0.92));
        if (!blob) throw new Error('toBlob returned null');
        const cropped = new File([blob], 'cropped.webp', { type: 'image/webp' });
        onCrop(cropped);
      } catch (e) {
        setErr('Zuschneiden fehlgeschlagen: ' + e.message);
        setBusy(false);
      }
    };

    // Portal the overlay to <body> so it always lands in the root
    // stacking context. Without this, when the cropper is rendered as
    // a child of AdminMenu (which lives inside a Glass card with
    // backdrop-filter + overflow:hidden), some browsers will clip the
    // `position:fixed` element to the AdminMenu's container. The fix
    // is to physically lift it out of every ancestor that could form
    // a new stacking context.
    const overlay = (
      <div style={{
        position: 'fixed', inset: 0, zIndex: 2000,
        background: 'var(--bg-overlay)',
        backdropFilter: 'blur(12px)',
        WebkitBackdropFilter: 'blur(12px)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 20,
      }}>
        <div style={{
          width: 'min(560px, 92vw)', maxHeight: '92vh',
          background: 'rgba(28,14,55,0.95)',
          border: '1px solid var(--border-medium)',
          borderRadius: 20,
          color: 'var(--foam)',
          boxShadow: 'var(--shadow-pop)',
          display: 'flex', flexDirection: 'column',
        }}>
          <div style={{
            padding: '18px 22px',
            borderBottom: '1px solid var(--border-hairline)',
          }}>
            <div style={{ fontSize: 18, fontWeight: 700, letterSpacing: '-0.01em' }}>Bild zuschneiden</div>
            <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 4 }}>
              Ziehen zum Verschieben, scrollen zum Zoomen. Das Quadrat wird übernommen.
            </div>
          </div>

          {err ? (
            <div style={{ padding: 24, textAlign: 'center' }}>
              <ApIcon name="x" size={28} color="var(--danger)"/>
              <div style={{ fontSize: 14, marginTop: 10, color: 'var(--danger)' }}>{err}</div>
            </div>
          ) : (
            <div
              ref={containerRef}
              onPointerDown={onPointerDown}
              onPointerMove={onPointerMove}
              onPointerUp={onPointerUp}
              onPointerCancel={onPointerUp}
              onWheel={onWheel}
              style={{
                position: 'relative', flex: 1, minHeight: 320,
                aspectRatio: '1',
                cursor: dragRef.current ? 'grabbing' : 'grab',
                touchAction: 'none', userSelect: 'none',
                overflow: 'hidden',
                background: 'var(--lila-950)',
              }}
            >
              {img && (
                <img
                  src={src}
                  draggable={false}
                  alt=""
                  style={{
                    position: 'absolute',
                    left: '50%', top: '50%',
                    width:  img.naturalWidth  * scale,
                    height: img.naturalHeight * scale,
                    transform: `translate(calc(-50% + ${pos.x}px), calc(-50% + ${pos.y}px))`,
                    pointerEvents: 'none',
                  }}
                />
              )}
              {/* Mask — bright square in the middle, dimmed margins. */}
              <div style={{
                position: 'absolute', inset: 0, pointerEvents: 'none',
                boxShadow: `inset 0 0 0 ${PADDING}px rgba(17,7,36,0.6)`,
                outline: '2px solid var(--coral)',
                outlineOffset: -PADDING,
              }}/>
            </div>
          )}

          {/* Zoom slider so users without a scroll wheel still have
              control. Bounded to [minScale, 4×minScale]. */}
          {!err && img && (
            <div style={{ padding: '10px 22px', display: 'flex', alignItems: 'center', gap: 10 }}>
              <ApIcon name="image" size={16} color="var(--fg-3)"/>
              <input
                type="range"
                min={minScale} max={minScale * 4} step={(minScale * 3) / 100}
                value={scale}
                onChange={(e) => {
                  const next = parseFloat(e.target.value);
                  setScale(next);
                  setPos(p => clampPos(p, next));
                }}
                style={{ flex: 1 }}
              />
            </div>
          )}

          <div style={{
            display: 'flex', gap: 8, padding: '14px 22px 18px',
            borderTop: '1px solid var(--border-hairline)',
          }}>
            <Button variant="ghost" size="md" onClick={onCancel} disabled={busy}
              style={{ flex: 1 }}>
              Abbrechen
            </Button>
            <Button variant="reward" size="md" onClick={commit} disabled={busy || !img || !!err}
              style={{ flex: 2 }}>
              {busy ? 'Schneide…' : 'Übernehmen'}
            </Button>
          </div>
        </div>
      </div>
    );

    return ReactDOM.createPortal(overlay, document.body);
  }

  window.ImageCropper = ImageCropper;
})();
