// AvatarCapture.jsx — take a crew member's photo on the iPad and make it
// their avatar (v18.05; in-app camera with a framing guide, v18.09).
//
// Tapping an avatar in the Training roster opens a camera INSIDE the app
// rather than handing off to iOS. That's the whole point: the native camera
// can't draw anything, and without a guide people shoot from wherever they
// happen to be standing, so half the avatars come back as a distant torso
// and half as a nostril. The overlay dims everything outside a circle and
// puts a head-and-shoulders outline in it, so the framing is decided before
// the shutter rather than after.
//
// What lands in the circle is exactly what gets saved — the crop is taken
// from the circle's bounding box, so the guide is a promise, not a hint.
//
// Pipeline once captured: square crop -> 200px -> `compress-image` edge
// function (TinyPNG, stored with the service role) -> `staff.avatar_url`.
// The iPad never needs write access to the avatars bucket; the only thing
// anon can write is that one column, via a COLUMN-level grant, so an UPDATE
// touching pin/role/name is refused by the database itself.
//
// Falls back to the old `<input capture>` hand-off when getUserMedia is
// unavailable or the camera permission is denied — a kiosk that can't open
// the camera should still be able to set a photo.
//
// Idents are av/Av/AV_-prefixed — Babel-standalone shares one global scope
// across every .jsx in the app (HANDBOOK §9).

const { useState: useAv, useRef: useAvRef, useEffect: useAvEffect } = React;

const AV_SIZE = 200;         // stored square, in px
const AV_QUALITY = 0.9;      // pre-TinyPNG JPEG quality
const AV_BUCKET = 'avatars';

// Overlay geometry, in the 0-100 space of the square viewfinder.
const AV_CIRCLE_R = 39;      // circle radius => 78% of the frame
const AV_HEAD_PCT = 0.60;    // head height as a share of the CIRCLE diameter
const AV_HEAD_RY = (AV_HEAD_PCT * AV_CIRCLE_R * 2) / 2;
const AV_HEAD_RX = AV_HEAD_RY * 0.72;   // a head is taller than it is wide

// Centre-crop to a square and downscale, returning bare base64 (no data:
// prefix — that's what the edge function wants). Used by the fallback path.
const avToSquareJpeg = (file) => new Promise((resolve, reject) => {
  const url = URL.createObjectURL(file);
  const img = new Image();
  img.onload = () => {
    try {
      const side = Math.min(img.width, img.height);
      const canvas = document.createElement('canvas');
      canvas.width = AV_SIZE;
      canvas.height = AV_SIZE;
      const ctx = canvas.getContext('2d');
      ctx.imageSmoothingQuality = 'high';
      ctx.drawImage(img, (img.width - side) / 2, (img.height - side) / 2, side, side, 0, 0, AV_SIZE, AV_SIZE);
      const dataUrl = canvas.toDataURL('image/jpeg', AV_QUALITY);
      URL.revokeObjectURL(url);
      resolve(dataUrl.slice(dataUrl.indexOf(',') + 1));
    } catch (e) { URL.revokeObjectURL(url); reject(e); }
  };
  img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('image_decode_failed')); };
  img.src = url;
});

// Grab the circle's bounding box out of a live <video>. The element is a
// square showing the stream with object-fit: cover, so the visible region is
// the centre crop of the source — undo that first, then take the circle's
// box out of what's actually on screen.
const avCaptureFromVideo = (video) => {
  const vw = video.videoWidth, vh = video.videoHeight;
  if (!vw || !vh) throw new Error('video_not_ready');
  const shown = Math.min(vw, vh);              // side of the visible square
  const offX = (vw - shown) / 2;
  const offY = (vh - shown) / 2;
  const frac = (AV_CIRCLE_R * 2) / 100;        // circle box as a share of the square
  const side = shown * frac;
  const sx = offX + (shown - side) / 2;
  const sy = offY + (shown - side) / 2;

  const canvas = document.createElement('canvas');
  canvas.width = AV_SIZE;
  canvas.height = AV_SIZE;
  const ctx = canvas.getContext('2d');
  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(video, sx, sy, side, side, 0, 0, AV_SIZE, AV_SIZE);
  const dataUrl = canvas.toDataURL('image/jpeg', AV_QUALITY);
  return dataUrl.slice(dataUrl.indexOf(',') + 1);
};

// ------------------------------------------------------------------
// The camera sheet
// ------------------------------------------------------------------
const AvCameraSheet = ({ staff, onCancel, onShoot, onFallback }) => {
  const videoRef = useAvRef(null);
  const streamRef = useAvRef(null);
  const [ready, setReady] = useAv(false);
  const [error, setError] = useAv(null);

  useAvEffect(() => {
    let cancelled = false;
    const md = navigator.mediaDevices;
    if (!md || !md.getUserMedia) { setError('unsupported'); return; }
    md.getUserMedia({
      video: { facingMode: 'user', width: { ideal: 1280 }, height: { ideal: 1280 } },
      audio: false,
    }).then(stream => {
      if (cancelled) { stream.getTracks().forEach(t => t.stop()); return; }
      streamRef.current = stream;
      if (videoRef.current) {
        videoRef.current.srcObject = stream;
        videoRef.current.play().catch(() => {});
      }
      setReady(true);
    }).catch(() => { if (!cancelled) setError('denied'); });
    // Always release the camera — a kiosk that leaves the light on gets
    // unplugged by someone who thinks it's watching them.
    return () => {
      cancelled = true;
      if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop());
      streamRef.current = null;
    };
  }, []);

  const shoot = () => {
    if (!ready || !videoRef.current) return;
    try { onShoot(avCaptureFromVideo(videoRef.current)); }
    catch (e) { console.error('avatar capture failed', e); onCancel(); }
  };

  return (
    <div style={{ position: 'absolute', inset: 0, zIndex: 90, background: '#0B0B0B', display: 'flex', flexDirection: 'column' }}>
      <div style={{ padding: '18px 22px 10px', color: '#fff', flexShrink: 0 }}>
        <div style={{ fontSize: 20, fontWeight: 600, letterSpacing: '-0.02em' }}>{staff.name}</div>
        <div style={{ fontSize: 13.5, color: 'rgba(255,255,255,0.7)', marginTop: 2 }}>
          Line their head up inside the circle
        </div>
      </div>

      <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 22px' }}>
        <div style={{ position: 'relative', width: '100%', maxWidth: 520, aspectRatio: '1 / 1', borderRadius: 20, overflow: 'hidden', background: '#000' }}>
          <video
            ref={videoRef}
            playsInline
            muted
            autoPlay
            /* Mirrored so moving left moves left — the guide is symmetric, so
               this changes nothing about framing. The SAVED frame is not
               mirrored, which is the true likeness. */
            style={{ width: '100%', height: '100%', objectFit: 'cover', transform: 'scaleX(-1)' }}
          />
          <svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
            <defs>
              <mask id="avHole">
                <rect width="100" height="100" fill="#fff" />
                <circle cx="50" cy="50" r={AV_CIRCLE_R} fill="#000" />
              </mask>
              <clipPath id="avClip">
                <circle cx="50" cy="50" r={AV_CIRCLE_R} />
              </clipPath>
            </defs>
            <rect width="100" height="100" fill="#0B0B0B" opacity="0.62" mask="url(#avHole)" />
            <circle cx="50" cy="50" r={AV_CIRCLE_R} fill="none" stroke="#fff" strokeWidth="0.6" opacity="0.9" />
            {/* Head is the target and is measured: AV_HEAD_PCT of the circle
                diameter, sharing its centre. The shoulders are decoration —
                they run off the circle the way a real portrait crop does, and
                exist only to say "this is a person, not a close-up". */}
            <g clipPath="url(#avClip)" fill="none" stroke="#fff" strokeWidth="0.6" strokeDasharray="2.2 2.2" opacity="0.85">
              <ellipse cx="50" cy="50" rx={AV_HEAD_RX} ry={AV_HEAD_RY} />
              <path d="M6 100 C 22 85, 35 80.5, 50 80.5 C 65 80.5, 78 85, 94 100" />
            </g>
          </svg>
          {error && (
            <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14, padding: 24, textAlign: 'center', background: 'rgba(11,11,11,0.78)' }}>
              <div style={{ color: '#fff', fontSize: 15, lineHeight: 1.5 }}>
                {error === 'denied'
                  ? 'The camera is blocked for this device. Allow it in Settings, or use the camera app instead.'
                  : 'This device can’t open the camera in the app.'}
              </div>
              <button onClick={onFallback} style={{
                height: 46, padding: '0 20px', borderRadius: 12, border: 'none',
                background: '#fff', color: '#0B0B0B', fontSize: 15, fontWeight: 600,
              }}>Use the camera app</button>
            </div>
          )}
        </div>
      </div>

      <div style={{
        flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '18px 26px calc(20px + env(safe-area-inset-bottom))',
      }}>
        <button onClick={onCancel} style={{
          background: 'transparent', border: 'none', color: 'rgba(255,255,255,0.85)',
          fontSize: 16, fontWeight: 500, padding: '10px 4px',
        }}>Cancel</button>

        <button onClick={shoot} disabled={!ready} aria-label="Take photo" style={{
          width: 74, height: 74, borderRadius: 999, border: 'none', padding: 0,
          background: 'transparent', opacity: ready ? 1 : 0.4,
        }}>
          <span style={{
            display: 'block', width: 74, height: 74, borderRadius: 999,
            background: '#fff', boxShadow: 'inset 0 0 0 4px #0B0B0B, inset 0 0 0 6px #fff',
          }} />
        </button>

        <span style={{ width: 56 }} />
      </div>
    </div>
  );
};

// ------------------------------------------------------------------
// Hook — returns { start, busyId, overlay }; render `overlay` in the tree.
// ------------------------------------------------------------------
const useAvatarCapture = (onDone) => {
  const [busyId, setBusyId] = useAv(null);
  const [shooting, setShooting] = useAv(null);   // staff being photographed
  const inputRef = useAvRef(null);
  const targetRef = useAvRef(null);

  const start = (staff) => {
    if (!staff || busyId) return;
    targetRef.current = staff;
    setShooting(staff);
  };

  const openNativeCamera = () => {
    setShooting(null);
    if (inputRef.current) inputRef.current.click();
  };

  // Shared tail: base64 -> compress-image -> staff.avatar_url -> patch the
  // in-memory roster so the new face shows without a reload.
  const store = async (staff, b64) => {
    setBusyId(staff.id);
    try {
      const key = window.RESTAURANT_ID + '/' + staff.id + '-' + Date.now().toString(36) + '.jpg';
      const { data, error } = await window.supa.functions.invoke('compress-image', {
        body: { bucket: AV_BUCKET, key, image_base64: b64, media_type: 'image/jpeg' },
      });
      if (error || !data || !data.url) throw new Error('upload_failed');
      const { error: upErr } = await window.supa
        .from('staff').update({ avatar_url: data.url }).eq('id', staff.id);
      if (upErr) throw upErr;
      const row = (window.SAMPLE_STAFF || []).find(s => s.id === staff.id);
      if (row) row.avatarUrl = data.url;
      if (onDone) onDone(staff, data.url);
    } catch (err) {
      console.error('avatar save failed', err);
      if (onDone) onDone(staff, null, err);
    }
    setBusyId(null);
  };

  const onShoot = (b64) => {
    const staff = targetRef.current;
    setShooting(null);
    if (staff) store(staff, b64);
  };

  const onFile = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = '';
    const staff = targetRef.current;
    targetRef.current = null;
    if (!file || !staff) return;
    try {
      // EXIF first on this path: <img> honours the rotate tag but a canvas
      // draw does not, so cropping the raw file banks a sideways face.
      const upright = window.snapUprightPhoto ? await window.snapUprightPhoto(file) : file;
      await store(staff, await avToSquareJpeg(upright));
    } catch (err) {
      console.error('avatar capture failed', err);
      setBusyId(null);
    }
  };

  const overlay = (
    <>
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        capture="user"
        onChange={onFile}
        style={{ display: 'none' }}
      />
      {shooting && (
        <AvCameraSheet
          staff={shooting}
          onCancel={() => { setShooting(null); targetRef.current = null; }}
          onShoot={onShoot}
          onFallback={openNativeCamera}
        />
      )}
    </>
  );

  return { start, busyId, overlay, input: overlay };
};

// The tappable avatar itself: the normal Avatar with a small camera badge,
// and a spinner while its photo is being processed.
const AvatarCaptureButton = ({ staff, size = 40, busy, onStart }) => (
  <span
    onClick={(e) => { e.stopPropagation(); onStart(staff); }}
    title={'Take a photo for ' + (staff.name || 'this person')}
    style={{ position: 'relative', display: 'inline-flex', flexShrink: 0, cursor: 'pointer' }}
  >
    <Avatar staff={staff} size={size} />
    {busy ? (
      <span style={{
        position: 'absolute', inset: 0, borderRadius: 999,
        background: 'rgba(0,0,0,0.55)', display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <span style={{
          width: size * 0.4, height: size * 0.4, borderRadius: 999,
          border: '2px solid rgba(255,255,255,0.35)', borderTopColor: '#fff',
          animation: 'avSpin 0.7s linear infinite',
        }} />
      </span>
    ) : (
      <span style={{
        position: 'absolute', right: -2, bottom: -2,
        width: size * 0.42, height: size * 0.42, borderRadius: 999,
        background: 'var(--fg-1)', color: '#fff',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        border: '2px solid var(--bg-surface, #fff)',
      }}>
        <i className="ri-camera-line" style={{ fontSize: size * 0.22, lineHeight: 1 }} />
      </span>
    )}
    <style>{'@keyframes avSpin { to { transform: rotate(360deg) } }'}</style>
  </span>
);

Object.assign(window, { useAvatarCapture, AvatarCaptureButton, avToSquareJpeg, avCaptureFromVideo });
