// bug-catcher.jsx — Theryo's canonical Bug Catcher widget. Hosted at
// https://dashboard.k2oc.xyz/widgets/bug-catcher.jsx and consumed by review
// sites across the org. Update once here → all consumers pick it up on next
// page load.
//
// Integration on a consumer site:
//   <div id="bug-catcher-root" data-bugcatcher-ignore="true"></div>
//   <script>
//     window.BUG_CATCHER = { project: 'aims2', source: 'design-review' };
//   </script>
//   <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
//   <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
//   <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" crossorigin="anonymous"></script>
//   <script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
//   <script type="text/babel" data-presets="env,react"
//           src="https://dashboard.k2oc.xyz/widgets/bug-catcher.jsx"></script>
//
// The consumer site must also expose its OWN /api/feedback endpoint as a
// same-origin proxy that forwards to dashboard.k2oc.xyz with x-app-key. The
// dashboard's /api/feedback enforces same-origin CORS, so the proxy is
// required (and is the seam where FEEDBACK_API_KEY stays server-side).
//
// UX:
//   1. Click 🐞 FAB (bottom-right)
//   2. Pick "Box — drag a region" or "Pin — click a spot"
//   3. AnnotateOverlay covers the viewport at z=199
//   4. On complete: html2canvas captures the page → cropped snippet AND
//      annotated full page are both attached
//   5. Panel opens (anchored to the marker on desktop) — describe the problem
//   6. Submit → POST /api/feedback. Optimistic "Thanks!" fires immediately;
//      the network call runs in the background.
//
// Tickets land in theryo_intelligence.tasks tagged with the configured
// project + source. metadata.skip_n8n + metadata.no_prefix are set so the
// dashboard uses its local-insert path (attachments land synchronously) and
// skips the "[Bug]" title prefix.

(function () {
  const { useState, useRef, useCallback, useEffect } = React;

  // ─── Per-consumer config (set window.BUG_CATCHER before this script) ───
  const cfg = (typeof window !== 'undefined' && window.BUG_CATCHER) || {};
  const PROJECT_ID   = cfg.project || cfg.project_id || 'unknown';
  const SOURCE       = cfg.source  || 'design-review';
  const API_ENDPOINT = cfg.endpoint || '/api/feedback';
  // Theme override: 'dark' (default) or 'light'. The FAB sits over arbitrary
  // page content so the dark chrome works on most backgrounds; switch to
  // 'light' on sites whose design language is fully light.
  const THEME   = cfg.theme === 'light' ? 'light' : 'dark';
  const PRIMARY = cfg.primaryColor || '#3B82F6';

  if (PROJECT_ID === 'unknown') {
    console.warn('[bug-catcher] window.BUG_CATCHER.project is not set — tickets will be tagged "unknown".');
  }

  // ─── Color tokens (theme-driven) ───────────────────────────────────────
  const C = THEME === 'light' ? {
    brand:    PRIMARY,
    critical: '#EF4444',
    bg:       '#FFFFFF',
    surface:  '#F7F8FA',
    border:   'rgba(14,18,25,0.10)',
    t_primary:   '#0E1219',
    t_secondary: 'rgba(14,18,25,0.70)',
    t_tertiary:  'rgba(14,18,25,0.50)',
  } : {
    brand:    PRIMARY,
    critical: '#EF4444',
    bg:       '#0E1219',
    surface:  '#161B25',
    border:   'rgba(255,255,255,0.10)',
    t_primary:   '#E7EAF0',
    t_secondary: 'rgba(231,234,240,0.70)',
    t_tertiary:  'rgba(231,234,240,0.50)',
  };

  const BUG_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB — matches server
  const BUG_MAX_FILES = 10;
  const BUG_ALLOWED_MIMES = [
    'image/png', 'image/jpeg', 'image/gif', 'image/webp',
    'application/pdf', 'text/plain', 'text/csv', 'application/json',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  ];

  const OVERLAY_Z = 199;
  const MARKER_COLOR = '#FF3B6B';
  const MARKER_BG = 'rgba(255, 59, 107, 0.18)';

  // ─── useScreenshotAttachments hook ──────────────────────────────────────
  function useScreenshotAttachments(opts) {
    const maxFiles    = (opts && opts.maxFiles)    || 10;
    const maxSize     = (opts && opts.maxSize)     || 5 * 1024 * 1024;
    const allowedMimes = (opts && opts.allowedMimes) || BUG_ALLOWED_MIMES;

    const [files, setFiles] = useState([]);
    const [attachError, setAttachError] = useState('');
    const fileInputRef = useRef(null);

    const addFiles = useCallback((incoming) => {
      const list = Array.from(incoming || []);
      if (!list.length) return;
      let error = '';
      setFiles((prev) => {
        const next = [...prev];
        for (const f of list) {
          if (next.length >= maxFiles) {
            error = `Max ${maxFiles} attachments`;
            break;
          }
          if (f.size > maxSize) {
            error = `${f.name}: exceeds ${Math.round(maxSize / 1024 / 1024)} MB limit`;
            continue;
          }
          if (allowedMimes.length && !allowedMimes.includes(f.type)) {
            error = `${f.name}: type ${f.type || 'unknown'} not allowed`;
            continue;
          }
          next.push({
            id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
            file: f,
            previewUrl: f.type.startsWith('image/') ? URL.createObjectURL(f) : null,
          });
        }
        return next;
      });
      setAttachError(error);
    }, [maxFiles, maxSize, allowedMimes]);

    const removeFile = useCallback((id) => {
      setFiles((prev) => {
        const target = prev.find((x) => x.id === id);
        if (target && target.previewUrl) URL.revokeObjectURL(target.previewUrl);
        return prev.filter((x) => x.id !== id);
      });
    }, []);

    const handlePaste = useCallback((e) => {
      const items = e.clipboardData && e.clipboardData.items;
      if (!items) return;
      const pastedFiles = [];
      for (const it of items) {
        if (it.kind === 'file') {
          const f = it.getAsFile();
          if (f) pastedFiles.push(f);
        }
      }
      if (pastedFiles.length) {
        e.preventDefault();
        addFiles(pastedFiles);
      }
    }, [addFiles]);

    const reset = useCallback(() => {
      setFiles((prev) => {
        prev.forEach((f) => f.previewUrl && URL.revokeObjectURL(f.previewUrl));
        return [];
      });
      setAttachError('');
    }, []);

    return { files, attachError, setAttachError, fileInputRef, addFiles, removeFile, handlePaste, reset };
  }

  // ─── AnnotateOverlay ────────────────────────────────────────────────────
  function AnnotateOverlay({ tool, onComplete, onCancel }) {
    const [drag, setDrag] = useState(null);
    const containerRef = useRef(null);

    useEffect(() => {
      const onKey = (e) => { if (e.key === 'Escape') onCancel && onCancel(); };
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [onCancel]);

    const handlePointerDown = (e) => {
      if (tool === 'pin') {
        const geo = {
          type: 'pin',
          xPx: e.clientX,
          yPx: e.clientY,
          widthPx: 0,
          heightPx: 0,
          x: e.clientX / window.innerWidth,
          y: e.clientY / window.innerHeight,
          width: 0,
          height: 0,
        };
        onComplete && onComplete(geo);
        return;
      }
      setDrag({ x0: e.clientX, y0: e.clientY, x1: e.clientX, y1: e.clientY });
    };

    const handlePointerMove = (e) => {
      if (!drag) return;
      setDrag((d) => (d ? { ...d, x1: e.clientX, y1: e.clientY } : d));
    };

    const handlePointerUp = (e) => {
      if (!drag) return;
      const xPx = Math.min(drag.x0, e.clientX);
      const yPx = Math.min(drag.y0, e.clientY);
      const widthPx = Math.abs(e.clientX - drag.x0);
      const heightPx = Math.abs(e.clientY - drag.y0);
      setDrag(null);
      if (widthPx < 6 || heightPx < 6) return;
      onComplete && onComplete({
        type: 'box',
        xPx, yPx, widthPx, heightPx,
        x: xPx / window.innerWidth,
        y: yPx / window.innerHeight,
        width: widthPx / window.innerWidth,
        height: heightPx / window.innerHeight,
      });
    };

    const previewRect = drag ? {
      left:   Math.min(drag.x0, drag.x1),
      top:    Math.min(drag.y0, drag.y1),
      width:  Math.abs(drag.x1 - drag.x0),
      height: Math.abs(drag.y1 - drag.y0),
    } : null;

    return (
      <div
        ref={containerRef}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        style={{
          position: 'fixed', inset: 0, zIndex: OVERLAY_Z,
          cursor: 'crosshair',
          background: 'rgba(0, 0, 0, 0.04)',
          userSelect: 'none',
        }}
        data-bugcatcher-overlay="true"
        role="presentation"
        aria-label={tool === 'box' ? 'Drag to mark a region' : 'Click to drop a pin'}
      >
        {previewRect && (
          <div style={{
            position: 'absolute',
            left: previewRect.left, top: previewRect.top,
            width: previewRect.width, height: previewRect.height,
            border: `2px solid ${MARKER_COLOR}`,
            background: MARKER_BG,
            pointerEvents: 'none',
          }} />
        )}
        <div style={{
          position: 'fixed', top: 16, left: '50%', transform: 'translateX(-50%)',
          background: '#1E3A5F', color: '#E4E4E7',
          padding: '8px 14px', borderRadius: 8,
          fontSize: 13, fontWeight: 500,
          fontFamily: 'system-ui, sans-serif',
          boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
          pointerEvents: 'none',
        }}>
          {tool === 'box' ? 'Drag to mark a region' : 'Click to drop a pin'} · Esc to cancel
        </div>
      </div>
    );
  }

  function MarkerVisual({ geometry }) {
    if (!geometry) return null;
    if (geometry.type === 'pin') {
      return (
        <div aria-hidden="true" style={{
          position: 'fixed',
          left: geometry.xPx - 12, top: geometry.yPx - 24,
          width: 24, height: 24, zIndex: 198,
          fontSize: 24, lineHeight: 1, pointerEvents: 'none',
          filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.4))',
        }}>📍</div>
      );
    }
    return (
      <div aria-hidden="true" style={{
        position: 'fixed',
        left: geometry.xPx, top: geometry.yPx,
        width: geometry.widthPx, height: geometry.heightPx,
        zIndex: 198,
        border: `2px solid ${MARKER_COLOR}`,
        background: MARKER_BG,
        pointerEvents: 'none',
        boxShadow: '0 0 0 9999px rgba(0,0,0,0.18)',
      }} />
    );
  }

  // ─── BugCatcher (FAB + flow controller) ─────────────────────────────────
  function BugCatcher() {
    const [open, setOpen] = useState(false);
    const [flow, setFlow] = useState('picker'); // picker | box-active | pin-active | panel
    const [geometry, setGeometry] = useState(null);
    const [problem, setProblem] = useState('');
    const [thanks, setThanks] = useState(false);
    const [submitting, setSubmitting] = useState(false);
    const [capturing, setCapturing] = useState(false);

    // Mobile = phone-width viewport. Box-drag is awkward on touch; mobile
    // skips the Box/Pin picker entirely (taps the FAB → pin overlay) and
    // hides the Box option if the picker is ever shown (e.g. after Cancel).
    const [isMobile, setIsMobile] = useState(false);
    useEffect(() => {
      if (typeof window === 'undefined' || !window.matchMedia) return;
      const mq = window.matchMedia('(max-width: 768px)');
      const update = () => setIsMobile(mq.matches);
      update();
      if (mq.addEventListener) mq.addEventListener('change', update);
      else mq.addListener(update);
      return () => {
        if (mq.removeEventListener) mq.removeEventListener('change', update);
        else mq.removeListener(update);
      };
    }, []);

    const {
      files, attachError, setAttachError, fileInputRef,
      addFiles, removeFile, handlePaste, reset: resetAttachments,
    } = useScreenshotAttachments({
      maxFiles: BUG_MAX_FILES,
      maxSize: BUG_MAX_FILE_SIZE,
      allowedMimes: BUG_ALLOWED_MIMES,
    });

    const closeAndReset = () => {
      setOpen(false);
      setFlow('picker');
      setGeometry(null);
      setProblem('');
      resetAttachments();
      setThanks(false);
    };

    const handleMarkComplete = useCallback(async (geo) => {
      setGeometry(geo);
      setFlow('panel');
      setCapturing(true);
      try {
        await new Promise((r) => requestAnimationFrame(() => r()));
        if (typeof window.html2canvas !== 'function') {
          setAttachError("Couldn't capture screenshot — html2canvas not loaded");
          return;
        }
        const fullCanvas = await window.html2canvas(document.body, {
          x: window.scrollX,
          y: window.scrollY,
          width: window.innerWidth,
          height: window.innerHeight,
          windowWidth: window.innerWidth,
          windowHeight: window.innerHeight,
          backgroundColor: null,
          useCORS: true,
          logging: false,
          ignoreElements: (el) => el && el.dataset && el.dataset.bugcatcherIgnore === 'true',
        });

        // Crop to the marked region. For a box, use its rect; for a pin, use a
        // 320×220 window centered on the pin so reviewers see what surrounded
        // the click. html2canvas captures at devicePixelRatio, so scale our
        // CSS-pixel coords by canvas.width / window.innerWidth.
        const PIN_W = 320, PIN_H = 220;
        let cropX, cropY, cropW, cropH;
        if (geo.type === 'box') {
          cropX = geo.xPx;
          cropY = geo.yPx;
          cropW = geo.widthPx;
          cropH = geo.heightPx;
        } else {
          cropW = PIN_W;
          cropH = PIN_H;
          cropX = geo.xPx - PIN_W / 2;
          cropY = geo.yPx - PIN_H / 2;
        }
        const scale = fullCanvas.width / window.innerWidth;
        let sx = Math.round(cropX * scale);
        let sy = Math.round(cropY * scale);
        let sw = Math.round(cropW * scale);
        let sh = Math.round(cropH * scale);
        // Clamp to canvas bounds.
        if (sx < 0) { sw += sx; sx = 0; }
        if (sy < 0) { sh += sy; sy = 0; }
        if (sx + sw > fullCanvas.width) sw = fullCanvas.width - sx;
        if (sy + sh > fullCanvas.height) sh = fullCanvas.height - sy;
        if (sw < 4 || sh < 4) {
          setAttachError("Marked region was too small to capture — note will still submit");
          return;
        }

        const cropCanvas = document.createElement('canvas');
        cropCanvas.width = sw;
        cropCanvas.height = sh;
        const cropCtx = cropCanvas.getContext('2d');
        cropCtx.drawImage(fullCanvas, sx, sy, sw, sh, 0, 0, sw, sh);

        // Build an annotated full-page canvas so the dev sees where in the
        // page the marked region sits. We draw the marker onto a copy of the
        // full capture (the live MarkerVisual is excluded by html2canvas via
        // data-bugcatcher-ignore, so we redraw it ourselves).
        const annotatedCanvas = document.createElement('canvas');
        annotatedCanvas.width = fullCanvas.width;
        annotatedCanvas.height = fullCanvas.height;
        const annotatedCtx = annotatedCanvas.getContext('2d');
        annotatedCtx.drawImage(fullCanvas, 0, 0);
        annotatedCtx.lineWidth = Math.max(2, Math.round(2 * scale));
        annotatedCtx.strokeStyle = MARKER_COLOR;
        if (geo.type === 'box') {
          const bx = Math.round(geo.xPx * scale);
          const by = Math.round(geo.yPx * scale);
          const bw = Math.round(geo.widthPx * scale);
          const bh = Math.round(geo.heightPx * scale);
          annotatedCtx.fillStyle = MARKER_BG;
          annotatedCtx.fillRect(bx, by, bw, bh);
          annotatedCtx.strokeRect(bx, by, bw, bh);
        } else {
          const px = Math.round(geo.xPx * scale);
          const py = Math.round(geo.yPx * scale);
          const r = Math.max(8, Math.round(10 * scale));
          annotatedCtx.fillStyle = MARKER_BG;
          annotatedCtx.beginPath();
          annotatedCtx.arc(px, py, r * 2, 0, Math.PI * 2);
          annotatedCtx.fill();
          annotatedCtx.fillStyle = MARKER_COLOR;
          annotatedCtx.beginPath();
          annotatedCtx.arc(px, py, r, 0, Math.PI * 2);
          annotatedCtx.fill();
          annotatedCtx.strokeStyle = '#fff';
          annotatedCtx.lineWidth = Math.max(2, Math.round(2 * scale));
          annotatedCtx.stroke();
        }

        const ts = Date.now();
        // Snippet first so it sorts above the full page in the attachment list.
        await new Promise((resolve) => {
          cropCanvas.toBlob((blob) => {
            if (blob) {
              addFiles([new File([blob], `bug-snippet-${geo.type}-${ts}.png`, { type: 'image/png' })]);
            }
            resolve();
          }, 'image/png');
        });
        await new Promise((resolve) => {
          annotatedCanvas.toBlob((blob) => {
            if (blob) {
              addFiles([new File([blob], `bug-fullpage-${ts}.png`, { type: 'image/png' })]);
            }
            resolve();
          }, 'image/png');
        });
      } catch (err) {
        console.error('Screenshot capture failed:', err);
        setAttachError("Couldn't capture screenshot — note will still submit");
      } finally {
        setCapturing(false);
      }
    }, [addFiles, setAttachError]);

    // Mobile path: skip the Box/Pin picker AND the annotation overlay entirely.
    // Drag-to-mark is awkward on touch and the pin overlay was misbehaving on
    // small screens. We just capture the full page on FAB tap and let the
    // reviewer type their note. The submit code already handles a null
    // geometry — it just omits the region coordinates from the task body.
    const captureFullPageOnly = useCallback(async () => {
      setCapturing(true);
      try {
        await new Promise((r) => requestAnimationFrame(() => r()));
        if (typeof window.html2canvas !== 'function') {
          setAttachError("Couldn't capture screenshot — html2canvas not loaded");
          return;
        }
        const fullCanvas = await window.html2canvas(document.body, {
          x: window.scrollX,
          y: window.scrollY,
          width: window.innerWidth,
          height: window.innerHeight,
          windowWidth: window.innerWidth,
          windowHeight: window.innerHeight,
          backgroundColor: null,
          useCORS: true,
          logging: false,
          ignoreElements: (el) => el && el.dataset && el.dataset.bugcatcherIgnore === 'true',
        });
        const ts = Date.now();
        await new Promise((resolve) => {
          fullCanvas.toBlob((blob) => {
            if (blob) {
              addFiles([new File([blob], `bug-fullpage-${ts}.png`, { type: 'image/png' })]);
            }
            resolve();
          }, 'image/png');
        });
      } catch (err) {
        console.error('Screenshot capture failed:', err);
        setAttachError("Couldn't capture screenshot — note will still submit");
      } finally {
        setCapturing(false);
      }
    }, [addFiles, setAttachError]);

    const handleSubmit = () => {
      const trimmedProblem = problem.trim();
      if (!trimmedProblem || submitting) return;

      // Optimistic UX — show "Thanks!" the instant Submit is clicked.
      // The actual POST travels Vercel → dashboard.k2oc.xyz → MongoDB and
      // can take 300–800ms; we fire it in the background and the user is
      // gone by the time it lands.
      setSubmitting(true);
      setThanks(true);
      setTimeout(() => closeAndReset(), 1200);

      (async () => {
        try {
          // Build the problem text so line 1 = task title, rest = description body.
          // Dasher uses the first line of `problem` as the task title (slice 0..200);
          // we keep that line short and user-typed, and push the page URL + region
          // metadata into the body below a blank-line separator so they land in the
          // task description rather than the title.
          const userText = trimmedProblem;
          const firstLine = userText.split('\n')[0].trim();
          const TITLE_LIMIT = 80;
          const titleLine = firstLine.length <= TITLE_LIMIT
            ? firstLine
            : firstLine.slice(0, TITLE_LIMIT - 3).trimEnd() + '...';
          const userOverflow = userText !== titleLine;

          const safeUrl = typeof window !== 'undefined'
            ? `${window.location.origin}${window.location.pathname}`
            : '';

          const bodyLines = [titleLine, ''];
          if (userOverflow) bodyLines.push(userText, '');
          bodyLines.push(`Page: ${safeUrl}`);
          if (geometry) {
            const x = (geometry.x * 100).toFixed(1);
            const y = (geometry.y * 100).toFixed(1);
            if (geometry.type === 'box') {
              const w = (geometry.width * 100).toFixed(1);
              const h = (geometry.height * 100).toFixed(1);
              bodyLines.push(`Region: ${x}%, ${y}% — ${w}% × ${h}%`);
            } else {
              bodyLines.push(`Pin: ${x}%, ${y}%`);
            }
          }
          const body = bodyLines.join('\n');

          const fd = new FormData();
          fd.append('problem', body);
          fd.append('type', 'bug');
          fd.append('project', PROJECT_ID);
          fd.append('source', SOURCE);
          // Opt out of the [Bug]/[Feature] title prefix and the n8n delegation
          // path on the dashboard. The n8n path silently drops attachments
          // because its dedup_hash format diverges from the buffered-attach
          // lookup; using the local-insert path keeps screenshots attached.
          fd.append('metadata', JSON.stringify({ skip_n8n: true, no_prefix: true }));
          for (const f of files) fd.append('file', f.file, f.file.name);
          const response = await fetch(API_ENDPOINT, {
            method: 'POST',
            body: fd,
            credentials: 'include',
          });
          if (!response.ok) {
            const errBody = await response.json().catch(() => ({}));
            console.error('Feedback submit failed:', errBody.error || response.status);
          }
        } catch (err) {
          console.error('Feedback submit failed:', err);
        } finally {
          setSubmitting(false);
        }
      })();
    };

    // Anchor panel near geometry on desktop; fall back to fixed bottom-right.
    // On mobile the panel becomes a bottom sheet — full-width minus 12px
    // gutters, anchored to the bottom of the viewport, so it never overflows
    // a phone portrait viewport (~390px wide).
    const panelStyle = (() => {
      const base = {
        position: 'fixed', zIndex: 200,
        background: C.surface, border: `1px solid ${C.border}`,
        borderRadius: 12, padding: 14,
        boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
        overflowY: 'auto',
        color: C.t_primary,
        fontFamily: '"DM Sans", system-ui, -apple-system, sans-serif',
      };
      if (isMobile) {
        return {
          ...base,
          left: 12, right: 12, bottom: 12,
          maxHeight: 'calc(100vh - 24px)',
        };
      }
      const desktopBase = { ...base, width: 480, maxHeight: 'calc(100vh - 24px)' };
      if (!geometry) return { ...desktopBase, bottom: 76, right: 20 };
      const margin = 12;
      const panelW = 480;
      const panelH = 280;
      const vw = window.innerWidth;
      const vh = window.innerHeight;
      const markRight = (geometry.xPx || 0) + (geometry.widthPx || 0);
      let left = markRight + margin;
      if (left + panelW > vw - margin) left = (geometry.xPx || 0) - panelW - margin;
      if (left < margin) left = margin;
      let top = geometry.yPx || 0;
      if (top + panelH > vh - margin) top = vh - panelH - margin;
      if (top < margin) top = margin;
      return { ...desktopBase, top, left };
    })();

    const fabStyle = {
      position: 'fixed', bottom: 20, right: 20, zIndex: 200,
      width: 48, height: 48, borderRadius: '50%',
      background: C.bg, border: `1px solid ${C.border}`,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      cursor: 'pointer', fontSize: 22, boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
      transition: 'transform 0.15s, border-color 0.15s',
      color: '#fff',
    };

    const pickerStyle = {
      position: 'fixed', bottom: 76, right: 20, zIndex: 200,
      background: C.surface, border: `1px solid ${C.border}`,
      borderRadius: 12, padding: 12,
      boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
      display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'stretch',
      minWidth: 220,
      color: C.t_primary,
      fontFamily: '"DM Sans", system-ui, -apple-system, sans-serif',
    };

    const pickerBtn = {
      padding: '10px 14px', fontSize: 14, fontWeight: 600,
      background: C.bg, color: C.t_primary, border: `1px solid ${C.border}`,
      borderRadius: 8, cursor: 'pointer',
      display: 'flex', alignItems: 'center', gap: 10,
      fontFamily: 'inherit',
    };

    const labelStyle = { display: 'block', fontSize: 12, fontWeight: 600, color: C.t_tertiary, marginBottom: 4 };
    const textareaStyle = {
      width: '100%', padding: '8px 10px', fontSize: 14, boxSizing: 'border-box',
      background: C.bg, color: C.t_primary, border: `1px solid ${C.border}`,
      borderRadius: 8, outline: 'none', resize: 'vertical',
      fontFamily: 'inherit',
    };

    const renderChips = () => files.length > 0 && (
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
        {files.map((f) => (
          <div key={f.id} style={{
            position: 'relative', display: 'flex', alignItems: 'center', gap: 6,
            padding: '4px 6px', background: C.bg, border: `1px solid ${C.border}`,
            borderRadius: 6, fontSize: 11, color: C.t_secondary, maxWidth: 160,
          }}>
            {f.previewUrl ? (
              <img src={f.previewUrl} alt="" style={{ width: 24, height: 24, objectFit: 'cover', borderRadius: 3 }} />
            ) : (
              <span>📄</span>
            )}
            <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
              {f.file.name}
            </span>
            <button
              type="button"
              onClick={() => removeFile(f.id)}
              style={{
                background: 'transparent', border: 'none', color: C.t_tertiary,
                cursor: 'pointer', fontSize: 14, padding: 0, lineHeight: 1,
              }}
              aria-label="Remove attachment"
            >
              ✕
            </button>
          </div>
        ))}
      </div>
    );

    return (
      <>
        {open && flow === 'picker' && !isMobile && (
          <div style={pickerStyle} data-bugcatcher-ignore="true">
            <div style={{ fontSize: 12, fontWeight: 600, color: C.t_tertiary, marginBottom: 2 }}>
              Mark a spot to start
            </div>
            <button type="button" onClick={() => setFlow('box-active')} style={pickerBtn}>
              <span style={{ fontSize: 18 }}>📦</span> Box — drag a region
            </button>
            <button type="button" onClick={() => setFlow('pin-active')} style={pickerBtn}>
              <span style={{ fontSize: 18 }}>📍</span> Pin — click a spot
            </button>
          </div>
        )}

        {open && (flow === 'box-active' || flow === 'pin-active') && (
          <AnnotateOverlay
            tool={flow === 'box-active' ? 'box' : 'pin'}
            onComplete={handleMarkComplete}
            onCancel={() => setFlow('picker')}
          />
        )}

        {open && flow === 'panel' && geometry && <MarkerVisual geometry={geometry} />}

        {open && flow === 'panel' && (
          <div style={panelStyle} data-bugcatcher-ignore="true">
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
              <div style={{ fontSize: 15, fontWeight: 700, color: C.t_primary }}>
                {'🐞 Bug Catcher'}
              </div>
              {isMobile ? (
                <button
                  type="button"
                  onClick={closeAndReset}
                  style={{
                    background: 'transparent', border: 'none', color: C.t_tertiary,
                    cursor: 'pointer', fontSize: 18, padding: 0, lineHeight: 1, fontFamily: 'inherit',
                  }}
                  aria-label="Close"
                >
                  ×
                </button>
              ) : (
                <button
                  type="button"
                  onClick={() => { setGeometry(null); resetAttachments(); setProblem(''); setFlow('picker'); }}
                  style={{
                    background: 'transparent', border: 'none', color: C.t_tertiary,
                    cursor: 'pointer', fontSize: 12, padding: 0, fontFamily: 'inherit',
                  }}
                >
                  ← Re-mark
                </button>
              )}
            </div>

            {thanks ? (
              <div style={{ textAlign: 'center', padding: '18px 0', color: C.brand, fontWeight: 600, fontSize: 16 }}>Thanks!</div>
            ) : (<>
              <div style={{ marginBottom: 10 }}>
                <label style={labelStyle}>Problem *</label>
                <textarea
                  placeholder={'Describe what went wrong... (paste images with ⌘V)'}
                  value={problem}
                  onChange={(e) => setProblem(e.target.value)}
                  onPaste={handlePaste}
                  maxLength={900}
                  rows={3}
                  style={textareaStyle}
                  autoFocus
                />
              </div>

              <div style={{ marginBottom: 10 }}>
                <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap', marginBottom: 6 }}>
                  <button
                    type="button"
                    onClick={() => fileInputRef.current && fileInputRef.current.click()}
                    style={{
                      padding: '5px 10px', fontSize: 12, fontWeight: 600,
                      background: C.bg, color: C.t_secondary, border: `1px solid ${C.border}`,
                      borderRadius: 6, cursor: 'pointer', fontFamily: 'inherit',
                    }}
                  >
                    📎 Attach
                  </button>
                  <span style={{ fontSize: 11, color: C.t_tertiary }}>
                    {capturing ? 'Capturing…' : `${files.length}/${BUG_MAX_FILES} · 5 MB max`}
                  </span>
                  <input
                    ref={fileInputRef}
                    type="file"
                    multiple
                    accept={BUG_ALLOWED_MIMES.join(',')}
                    onChange={(e) => { addFiles(e.target.files); e.target.value = ''; }}
                    style={{ display: 'none' }}
                  />
                </div>
                {renderChips()}
                {attachError && (
                  <div style={{ marginTop: 6, fontSize: 11, color: C.critical }}>{attachError}</div>
                )}
              </div>

              <button
                type="button"
                onClick={handleSubmit}
                disabled={!problem.trim() || submitting || capturing}
                style={{
                  width: '100%', padding: '8px 0', fontSize: 14, fontWeight: 600,
                  background: C.brand, color: '#fff', border: 'none', borderRadius: 8,
                  cursor: (!problem.trim() || submitting || capturing) ? 'not-allowed' : 'pointer',
                  opacity: (!problem.trim() || submitting || capturing) ? 0.5 : 1,
                  fontFamily: 'inherit',
                }}
              >
                {submitting ? 'Submitting...' : 'Submit'}
              </button>
            </>)}
          </div>
        )}

        <button
          type="button"
          onClick={() => {
            if (open) { closeAndReset(); return; }
            setOpen(true);
            // Mobile: skip annotation entirely. Capture the full page in
            // the background and jump straight to the text panel — the
            // reviewer just types their note + submits.
            if (isMobile) {
              setFlow('panel');
              captureFullPageOnly();
            }
          }}
          style={fabStyle}
          aria-label="Bug Catcher"
          data-bugcatcher-ignore="true"
        >
          {open ? '✕' : '🐞'}
        </button>
      </>
    );
  }

  // ─── Mount ──────────────────────────────────────────────────────────────
  function mount() {
    const root = document.getElementById('bug-catcher-root');
    if (!root) return;
    if (!window.React || !window.ReactDOM) {
      console.warn('bug-catcher: React/ReactDOM not loaded yet');
      return;
    }
    ReactDOM.createRoot(root).render(<BugCatcher />);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', mount);
  } else {
    mount();
  }
})();
