// ============================================================================
// DEV-ONLY in-app feedback / annotation toolbar.
// ----------------------------------------------------------------------------
// Mounts a floating bubble → toolbar that lets the owner point at UI elements,
// draw, snapshot "what I see", record a voice note, type a note, and send a
// bundle to the DEV Supabase (public.feedback + private feedback-media bucket).
//
// HARD GATE: everything below runs ONLY when HABIT_CONFIG.ENV === 'development'.
// On production this file early-returns before doing ANYTHING — no React root,
// no listeners, no html2canvas CDN load. See app/config.js for how ENV is set.
// ============================================================================
(function () {
  var CFG = window.HABIT_CONFIG || {};
  if (CFG.ENV !== 'development') return; // ← production kill-switch. Do nothing.
  if (!window.React || !window.ReactDOM || !window.supabase) return;
  if (document.getElementById('__fb_root')) return; // guard double-mount

  var React = window.React;
  var h = React.createElement;

  // Prefer the app's already-authenticated supabase client (exposed as
  // window.__APP_SB by live.jsx) so sends carry the user's live session.
  // Fall back to our own client (which recovers the session from storage) if
  // that global isn't present.
  var _ownSb = null;
  function fbClient() {
    if (window.__APP_SB) return window.__APP_SB;
    if (!_ownSb) {
      _ownSb = window.supabase.createClient(CFG.SUPABASE_URL, CFG.SUPABASE_ANON_KEY, {
        auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: false },
      });
    }
    return _ownSb;
  }

  var GREEN = (CFG.HABIT && CFG.HABIT.accent) || '#3a6b3e';
  var Z = 2147483000; // above everything the app uses
  var BUBBLE = 52;    // bubble diameter (px)
  var LS_BUBBLE = '__fb_bubble_pos';
  var LS_PANEL = '__fb_panel_pos';

  function lsGet(k) { try { return JSON.parse(localStorage.getItem(k)); } catch (_) { return null; } }
  function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (_) {} }

  // Stable client anon id for pre-login (logged-out) feedback attribution.
  function anonId() {
    var v = lsGet('__fb_anon_id');
    if (!v) { v = 'anon-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); lsSet('__fb_anon_id', v); }
    return v;
  }

  // ── IndexedDB — persist unsent bundles (png + audio blobs) so a failed send
  // (or a reload) never loses a recording. Text/meta ride along in the record. ──
  function idbOpen() {
    return new Promise(function (res, rej) {
      var r = indexedDB.open('__fb_pending', 1);
      r.onupgradeneeded = function () { if (!r.result.objectStoreNames.contains('bundles')) r.result.createObjectStore('bundles', { keyPath: 'id' }); };
      r.onsuccess = function () { res(r.result); };
      r.onerror = function () { rej(r.error); };
    });
  }
  function idbPut(rec) { return idbOpen().then(function (db) { return new Promise(function (res, rej) { var tx = db.transaction('bundles', 'readwrite'); tx.objectStore('bundles').put(rec); tx.oncomplete = function () { res(); }; tx.onerror = function () { rej(tx.error); }; }); }); }
  function idbGetAll() { return idbOpen().then(function (db) { return new Promise(function (res, rej) { var tx = db.transaction('bundles', 'readonly'); var rq = tx.objectStore('bundles').getAll(); rq.onsuccess = function () { res(rq.result || []); }; rq.onerror = function () { rej(rq.error); }; }); }); }
  function idbDel(id) { return idbOpen().then(function (db) { return new Promise(function (res, rej) { var tx = db.transaction('bundles', 'readwrite'); tx.objectStore('bundles').delete(id); tx.oncomplete = function () { res(); }; tx.onerror = function () { rej(tx.error); }; }); }); }

  // ── recent tap coordinates (viewport-relative), captured app-wide ──────────
  var recentTaps = [];
  document.addEventListener('pointerdown', function (e) {
    recentTaps.push({ x: Math.round(e.clientX), y: Math.round(e.clientY), t: Date.now() });
    if (recentTaps.length > 6) recentTaps.shift();
  }, true);

  // ── html2canvas lazy loader (dev only — never fetched on prod) ─────────────
  var _h2cPromise = null;
  function loadHtml2Canvas() {
    if (window.html2canvas) return Promise.resolve(window.html2canvas);
    if (_h2cPromise) return _h2cPromise;
    _h2cPromise = new Promise(function (resolve, reject) {
      var s = document.createElement('script');
      s.src = 'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js';
      s.crossOrigin = 'anonymous';
      s.onload = function () { resolve(window.html2canvas); };
      s.onerror = function () { reject(new Error('html2canvas failed to load')); };
      document.head.appendChild(s);
    });
    return _h2cPromise;
  }

  // Build a robust descriptor for a picked element.
  function describeElement(el) {
    if (!el || el === document.body || el === document.documentElement) return null;
    var rect = el.getBoundingClientRect();
    var cls = (typeof el.className === 'string' ? el.className : '').trim();
    var text = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 80);
    // nth-child DOM path (cap depth so it stays short & robust)
    var path = [];
    var node = el, depth = 0;
    while (node && node.nodeType === 1 && node !== document.body && depth < 8) {
      var idx = 1, sib = node;
      while ((sib = sib.previousElementSibling)) idx++;
      path.unshift(node.tagName.toLowerCase() + ':nth-child(' + idx + ')');
      node = node.parentElement; depth++;
    }
    return {
      testid: el.getAttribute && el.getAttribute('data-testid') || null,
      tag: el.tagName ? el.tagName.toLowerCase() : null,
      classes: cls ? cls.split(/\s+/).slice(0, 12) : [],
      text: text,
      path: path.join(' > '),
      rect: { x: Math.round(rect.left), y: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height) },
    };
  }

  function inferRoute() {
    try {
      if (window.__APP_ROUTE) return String(window.__APP_ROUTE);
    } catch (_) {}
    var head = document.querySelector('.shell h1, .shell h2, .shell header');
    var ht = head ? (head.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 40) : '';
    return (location.hash || '') + (ht ? ' | ' + ht : '') || location.pathname;
  }

  // Track the FULL browser viewport. Annotation overlays + screenshot capture are
  // sized to the whole window (app column PLUS the side margins), so strokes land
  // at the right coords and the shot shows exactly what's on screen.
  function useViewport() {
    var get = function () { return { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; };
    var ref = React.useState(get);
    var rect = ref[0], setRect = ref[1];
    React.useEffect(function () {
      var upd = function () { setRect(get()); };
      window.addEventListener('resize', upd);
      window.addEventListener('orientationchange', upd);
      return function () {
        window.removeEventListener('resize', upd);
        window.removeEventListener('orientationchange', upd);
      };
    }, []);
    return rect;
  }

  function fmtTime(ms) {
    var s = Math.floor(ms / 1000);
    return Math.floor(s / 60) + ':' + ('0' + (s % 60)).slice(-2);
  }

  // ── Draw overlay canvas ────────────────────────────────────────────────────
  function DrawCanvas(props) {
    // props: rect, tool ('pen'|'arrow'), color, canvasRef
    var rect = props.rect;
    var toolRef = React.useRef(props.tool);
    var colorRef = React.useRef(props.color);
    var onDrawRef = React.useRef(props.onDraw);
    toolRef.current = props.tool; colorRef.current = props.color; onDrawRef.current = props.onDraw;
    var cvRef = props.canvasRef;
    var dpr = window.devicePixelRatio || 1;

    React.useEffect(function () {
      var cv = cvRef.current; if (!cv) return;
      cv.width = Math.max(1, Math.round(rect.width * dpr));
      cv.height = Math.max(1, Math.round(rect.height * dpr));
      var ctx = cv.getContext('2d');
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.lineCap = 'round'; ctx.lineJoin = 'round';
    }, [rect.width, rect.height]);

    React.useEffect(function () {
      var cv = cvRef.current; if (!cv) return;
      var ctx = cv.getContext('2d');
      var drawing = false, start = null, snapshot = null;

      function pos(e) {
        var b = cv.getBoundingClientRect();
        return { x: e.clientX - b.left, y: e.clientY - b.top };
      }
      function down(e) {
        e.preventDefault();
        drawing = true; start = pos(e);
        if (onDrawRef.current) onDrawRef.current();
        ctx.strokeStyle = colorRef.current; ctx.fillStyle = colorRef.current; ctx.lineWidth = 3.5;
        if (toolRef.current === 'arrow') {
          snapshot = ctx.getImageData(0, 0, cv.width, cv.height);
        } else {
          ctx.beginPath(); ctx.moveTo(start.x, start.y);
        }
        cv.setPointerCapture && cv.setPointerCapture(e.pointerId);
      }
      function move(e) {
        if (!drawing) return;
        e.preventDefault();
        var p = pos(e);
        if (toolRef.current === 'arrow') {
          if (snapshot) ctx.putImageData(snapshot, 0, 0);
          drawArrow(ctx, start.x, start.y, p.x, p.y, colorRef.current);
        } else {
          ctx.lineTo(p.x, p.y); ctx.stroke();
        }
      }
      function up(e) {
        if (!drawing) return;
        drawing = false;
        if (toolRef.current === 'arrow') {
          var p = pos(e);
          if (snapshot) ctx.putImageData(snapshot, 0, 0);
          drawArrow(ctx, start.x, start.y, p.x, p.y, colorRef.current);
          snapshot = null;
        }
      }
      cv.addEventListener('pointerdown', down);
      cv.addEventListener('pointermove', move);
      cv.addEventListener('pointerup', up);
      cv.addEventListener('pointercancel', up);
      return function () {
        cv.removeEventListener('pointerdown', down);
        cv.removeEventListener('pointermove', move);
        cv.removeEventListener('pointerup', up);
        cv.removeEventListener('pointercancel', up);
      };
    }, []);

    return h('canvas', {
      ref: cvRef,
      style: {
        position: 'fixed', left: rect.left, top: rect.top,
        width: rect.width, height: rect.height,
        zIndex: props.active ? Z + 2 : Z + 1, touchAction: 'none',
        pointerEvents: props.active ? 'auto' : 'none',
        cursor: props.active ? 'crosshair' : 'default',
      },
    });
  }

  function drawArrow(ctx, x1, y1, x2, y2, color) {
    var head = 14, ang = Math.atan2(y2 - y1, x2 - x1);
    ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = 3.5;
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - head * Math.cos(ang - Math.PI / 6), y2 - head * Math.sin(ang - Math.PI / 6));
    ctx.lineTo(x2 - head * Math.cos(ang + Math.PI / 6), y2 - head * Math.sin(ang + Math.PI / 6));
    ctx.closePath(); ctx.fill();
  }

  // ── Main toolbar ────────────────────────────────────────────────────────────
  function FeedbackToolbar() {
    var open = React.useState(false); var isOpen = open[0], setOpen = open[1];
    var mode = React.useState(null); var curMode = mode[0], setMode = mode[1]; // null|'pick'|'draw'
    var tool = React.useState('pen'); var curTool = tool[0], setTool = tool[1];
    var color = React.useState('#e23c3c'); var curColor = color[0], setColor = color[1];
    var el = React.useState(null); var picked = el[0], setPicked = el[1];
    var noteS = React.useState(''); var note = noteS[0], setNote = noteS[1];
    var recS = React.useState('idle'); var recState = recS[0], setRecState = recS[1]; // idle|recording|recorded|denied
    var recMsS = React.useState(0); var recMs = recMsS[0], setRecMs = recMsS[1];
    var sendS = React.useState('idle'); var sendState = sendS[0], setSendState = sendS[1]; // idle|sending|sent|error
    var errS = React.useState(null); var errMsg = errS[0], setErrMsg = errS[1];
    var cntS = React.useState(null); var sentCount = cntS[0], setSentCount = cntS[1]; // # of my unprocessed items
    var restoredS = React.useState(false); var restored = restoredS[0], setRestored = restoredS[1]; // unsent bundle recovered
    var pendingRef = React.useRef(null); // the last failed/restored bundle (blobs + text)

    // On load, recover any unsent bundle (failed send from a previous session) so
    // a reload never discards a recording. Surfaces a Retry/Discard banner.
    React.useEffect(function () {
      idbGetAll().then(function (list) {
        if (!list || !list.length) return;
        list.sort(function (a, b) { return (a.savedAt || 0) - (b.savedAt || 0); });
        var b = list[list.length - 1];
        pendingRef.current = b;
        if (b.note) setNote(b.note);
        setRestored(true); setSendState('error');
        setErrMsg('Unsent feedback recovered from this device — tap Retry to send it.');
        setOpen(true);
      }).catch(function () {});
    }, []);

    // Load my count of unprocessed feedback (status='new') whenever the panel opens.
    function loadCount() {
      try {
        // Plain GET (no HEAD) — avoids the ERR_ABORTED noise HEAD count requests
        // produce; dev feedback volume is tiny so fetching ids is cheap.
        fbClient().from('feedback').select('id').eq('status', 'new')
          .then(function (r) { if (r && Array.isArray(r.data)) setSentCount(r.data.length); });
      } catch (_) {}
    }
    React.useEffect(function () { if (isOpen) loadCount(); }, [isOpen]);

    var rect = useViewport(); // full-window rect for overlays + capture
    var canvasRef = React.useRef(null);
    var panelRef = React.useRef(null);
    var audioBlobRef = React.useRef(null);
    var mediaRecRef = React.useRef(null);
    var recTimerRef = React.useRef(null);
    var recStartRef = React.useRef(0);

    var hasDrawing = React.useRef(false);

    // ── draggable positions (persisted) ──────────────────────────────────────
    function clampBubble(p) {
      return {
        x: Math.min(Math.max(p.x, 0), Math.max(0, window.innerWidth - BUBBLE)),
        y: Math.min(Math.max(p.y, 0), Math.max(0, window.innerHeight - BUBBLE)),
      };
    }
    function defaultBubblePos() {
      return { x: window.innerWidth - BUBBLE - 12, y: Math.round(window.innerHeight * 0.66) };
    }
    var bpS = React.useState(function () { return clampBubble(lsGet(LS_BUBBLE) || defaultBubblePos()); });
    var bubblePos = bpS[0], setBubblePos = bpS[1];
    // panelPos null ⇒ default (bottom-centered, near where the bubble starts).
    var ppS = React.useState(function () {
      var s = lsGet(LS_PANEL);
      if (!s) return null;
      // reset if clearly off-screen
      if (s.x > window.innerWidth - 40 || s.y > window.innerHeight - 40 || s.x < -400 || s.y < 0) return null;
      return s;
    });
    var panelPos = ppS[0], setPanelPos = ppS[1];

    // keep positions on-screen across resize / orientation change
    React.useEffect(function () {
      function onResize() {
        setBubblePos(function (p) { return clampBubble(p); });
        setPanelPos(function (p) {
          if (!p) return p;
          return { x: Math.min(Math.max(p.x, 40 - 360), window.innerWidth - 40), y: Math.min(Math.max(p.y, 0), window.innerHeight - 40) };
        });
      }
      window.addEventListener('resize', onResize);
      window.addEventListener('orientationchange', onResize);
      return function () { window.removeEventListener('resize', onResize); window.removeEventListener('orientationchange', onResize); };
    }, []);

    // Bubble: drag anywhere; a real drag (>6px) suppresses the tap-to-toggle and
    // snaps to the nearest window edge; a plain tap opens/closes the panel.
    function onBubbleDown(e) {
      e.preventDefault();
      var start = { px: e.clientX, py: e.clientY, bx: bubblePos.x, by: bubblePos.y, moved: false };
      try { e.target.setPointerCapture && e.target.setPointerCapture(e.pointerId); } catch (_) {}
      function mv(ev) {
        var dx = ev.clientX - start.px, dy = ev.clientY - start.py;
        if (!start.moved && Math.sqrt(dx * dx + dy * dy) > 6) start.moved = true;
        if (start.moved) setBubblePos(clampBubble({ x: start.bx + dx, y: start.by + dy }));
      }
      function upf() {
        window.removeEventListener('pointermove', mv);
        window.removeEventListener('pointerup', upf);
        if (start.moved) {
          setBubblePos(function (cur) {
            var snapped = clampBubble({ x: (cur.x + BUBBLE / 2 < window.innerWidth / 2) ? 12 : window.innerWidth - BUBBLE - 12, y: cur.y });
            lsSet(LS_BUBBLE, snapped);
            return snapped;
          });
        } else {
          setOpen(function (o) { return !o; });
        }
      }
      window.addEventListener('pointermove', mv);
      window.addEventListener('pointerup', upf);
    }

    // Panel: dragged by its header. Stays partly on-screen; position persisted.
    function onPanelHeaderDown(e) {
      if (e.target.closest && e.target.closest('button')) return; // let ✕ work
      e.preventDefault();
      var pr = panelRef.current ? panelRef.current.getBoundingClientRect() : { left: 0, top: 0, width: 360, height: 300 };
      var start = { px: e.clientX, py: e.clientY, x: pr.left, y: pr.top, w: pr.width };
      setPanelPos({ x: start.x, y: start.y }); // switch from default anchoring to absolute
      function mv(ev) {
        var dx = ev.clientX - start.px, dy = ev.clientY - start.py;
        var nx = Math.min(Math.max(start.x + dx, 40 - start.w), window.innerWidth - 40);
        var ny = Math.min(Math.max(start.y + dy, 0), window.innerHeight - 40);
        setPanelPos({ x: nx, y: ny });
      }
      function upf() {
        window.removeEventListener('pointermove', mv);
        window.removeEventListener('pointerup', upf);
        setPanelPos(function (cur) { if (cur) lsSet(LS_PANEL, cur); return cur; });
      }
      window.addEventListener('pointermove', mv);
      window.addEventListener('pointerup', upf);
    }

    var pickBackupRef = React.useRef(null); // selection snapshot for Cancel

    // ── element pick handling ──────────────────────────────────────────────────
    // While select mode is ACTIVE, a full-viewport catcher swallows every tap so
    // the app's own handlers never fire (no navigation). Each tap re-picks the
    // element under the finger (highlight tracks it); the user stays in select
    // mode until they explicitly Lock in / Clear / Cancel.
    React.useEffect(function () {
      if (curMode !== 'pick') return;
      function pickAt(x, y) {
        var root = document.getElementById('__fb_root');
        var prev = root ? root.style.display : '';
        if (root) root.style.display = 'none';         // hide our whole overlay…
        var target = document.elementFromPoint(x, y);  // …so we read the APP element
        if (root) root.style.display = prev;           // …then restore (never clicked)
        var desc = describeElement(target);
        if (desc) { desc._pickAt = { x: Math.round(x), y: Math.round(y) }; setPicked(desc); }
      }
      function onDown(e) { e.preventDefault(); e.stopPropagation(); pickAt(e.clientX, e.clientY); }
      function swallow(e) { e.preventDefault(); e.stopPropagation(); } // eat click/pointerup too
      var catcher = document.getElementById('__fb_pick_catcher');
      if (catcher) {
        catcher.addEventListener('pointerdown', onDown);
        catcher.addEventListener('pointerup', swallow);
        catcher.addEventListener('click', swallow);
      }
      return function () {
        if (catcher) {
          catcher.removeEventListener('pointerdown', onDown);
          catcher.removeEventListener('pointerup', swallow);
          catcher.removeEventListener('click', swallow);
        }
      };
    }, [curMode]);

    function enterPick() { pickBackupRef.current = picked; setOpen(false); setMode('pick'); }
    function lockInPick() { setMode(null); }                       // keep current pick
    function clearPick() { setPicked(null); setMode(null); }       // drop selection
    function cancelPick() { setPicked(pickBackupRef.current); setMode(null); } // restore

    // ── voice recording ──
    function startRec() {
      setErrMsg(null);
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.MediaRecorder) {
        setRecState('denied'); return;
      }
      navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
        var chunks = [];
        var mime = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : '';
        var mr = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
        mr.ondataavailable = function (ev) { if (ev.data && ev.data.size) chunks.push(ev.data); };
        mr.onstop = function () {
          audioBlobRef.current = new Blob(chunks, { type: mr.mimeType || 'audio/webm' });
          stream.getTracks().forEach(function (t) { t.stop(); });
          setRecState('recorded');
          if (recTimerRef.current) clearInterval(recTimerRef.current);
        };
        mediaRecRef.current = mr;
        recStartRef.current = Date.now();
        setRecMs(0);
        recTimerRef.current = setInterval(function () { setRecMs(Date.now() - recStartRef.current); }, 200);
        mr.start();
        setRecState('recording');
      }).catch(function () { setRecState('denied'); });
    }
    function stopRec() { var mr = mediaRecRef.current; if (mr && mr.state !== 'inactive') mr.stop(); }
    function delRec() { audioBlobRef.current = null; setRecState('idle'); setRecMs(0); }

    // ── capture composite screenshot (html2canvas + annotation) ──
    // Rasterize the WHOLE visible viewport (the app column PLUS the beige side
    // margins / page background) — i.e. exactly what's on screen — not just the
    // app column. Our own overlay UI (#__fb_root) is excluded, then the
    // annotation canvas is composited back on top.
    function captureBlob() {
      var dpr = window.devicePixelRatio || 1;
      var pageBg = (function () { try { return getComputedStyle(document.body).backgroundColor || '#ece5d1'; } catch (_) { return '#ece5d1'; } })();
      return loadHtml2Canvas().then(function (html2canvas) {
        return html2canvas(document.documentElement, {
          scale: dpr, useCORS: true, backgroundColor: pageBg, logging: false,
          width: window.innerWidth, height: window.innerHeight,
          windowWidth: window.innerWidth, windowHeight: window.innerHeight,
          x: window.scrollX || 0, y: window.scrollY || 0,
          scrollX: 0, scrollY: 0,
          ignoreElements: function (el) { return el && el.id === '__fb_root'; },
        });
      }).then(function (base) {
        return compose(base, dpr);
      }).catch(function () {
        // Fallback: annotation canvas alone (or a blank sized canvas).
        return compose(null, dpr);
      });
    }
    function compose(baseCanvas, dpr) {
      var w = Math.round(rect.width * dpr), hgt = Math.round(rect.height * dpr);
      var out = document.createElement('canvas');
      out.width = baseCanvas ? baseCanvas.width : w;
      out.height = baseCanvas ? baseCanvas.height : hgt;
      var ctx = out.getContext('2d');
      if (baseCanvas) ctx.drawImage(baseCanvas, 0, 0);
      else { ctx.fillStyle = '#fbf7ee'; ctx.fillRect(0, 0, out.width, out.height); }
      // annotation canvas (device-px) drawn to match
      var cv = canvasRef.current;
      if (cv && cv.width) ctx.drawImage(cv, 0, 0, cv.width, cv.height, 0, 0, out.width, out.height);
      // draw the picked-element highlight into the shot too
      if (picked && picked.rect) {
        var sx = out.width / rect.width, sy = out.height / rect.height;
        ctx.strokeStyle = '#e23c3c'; ctx.lineWidth = 2 * dpr;
        ctx.strokeRect((picked.rect.x - rect.left) * sx, (picked.rect.y - rect.top) * sy,
          picked.rect.w * sx, picked.rect.h * sy);
      }
      return new Promise(function (res) { out.toBlob(function (b) { res(b); }, 'image/png'); });
    }

    // ── send bundle ──
    // Uploads the media + inserts the row for a fully-formed bundle. Works both
    // authenticated (user_id set, <uid>/ prefix) and ANONYMOUS/pre-login
    // (user_id null, anon/ prefix). Rejects on any failure so callers can persist.
    function uploadAndInsert(b) {
      var sb = fbClient();
      var prefix = b.uid ? b.uid : 'anon';
      var shotPath = prefix + '/' + b.ts + '.png';
      return sb.storage.from('feedback-media').upload(shotPath, b.pngBlob, { contentType: 'image/png', upsert: false })
        .then(function (res) {
          if (res.error) throw res.error;
          if (b.audioBlob) {
            var audioPath = prefix + '/' + b.ts + '.webm';
            return sb.storage.from('feedback-media').upload(audioPath, b.audioBlob, { contentType: b.audioBlob.type || 'audio/webm', upsert: false })
              .then(function (ar) { if (ar.error) throw ar.error; return { shotPath: shotPath, audioPath: audioPath }; });
          }
          return { shotPath: shotPath, audioPath: null };
        })
        .then(function (paths) {
          // return=minimal (no .select()) — a chained .select() would force
          // return=representation, which needs a SELECT policy; the anon role has
          // none, so the read-back would fail. We don't need the row id client-side.
          return sb.from('feedback').insert({
            user_id: b.uid || null,
            route: b.route,
            note: b.note || null,
            element: b.element || null,
            meta: b.meta,
            screenshot_path: paths.shotPath,
            audio_path: paths.audioPath,
          });
        })
        .then(function (res) { if (res.error) throw res.error; return res; });
    }

    function buildMeta(uid) {
      return {
        viewport: { w: window.innerWidth, h: window.innerHeight },
        dpr: window.devicePixelRatio || 1,
        userAgent: navigator.userAgent,
        route: inferRoute(),
        hash: location.hash || null,
        headerText: (function () { var e = document.querySelector('.shell h1, .shell h2, .shell header'); return e ? (e.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 80) : null; })(),
        taps: recentTaps.slice(),
        captureRect: rect,
        capturedAt: new Date().toISOString(),
        anonId: uid ? null : anonId(),   // who filed it when logged out
        preAuth: !uid,                     // filed from a logged-out screen
      };
    }

    function send() {
      setSendState('sending'); setErrMsg(null);
      var ts = new Date().toISOString().replace(/[:.]/g, '-');
      var sb = fbClient();
      // Determine uid from the in-memory session (no network) — null when logged out.
      sb.auth.getSession().then(function (r) {
        var sess = r && r.data && r.data.session;
        var uid = sess && sess.user ? sess.user.id : null;
        return captureBlob().then(function (pngBlob) {
          var bundle = {
            id: 'fb-' + ts, ts: ts, uid: uid,
            note: note, route: inferRoute(), element: picked || null,
            meta: buildMeta(uid), pngBlob: pngBlob, audioBlob: audioBlobRef.current || null,
          };
          return uploadAndInsert(bundle).then(function () {
            pendingRef.current = null; setRestored(false);
            setSendState('sent'); loadCount();
          }).catch(function (e) {
            // Persist so the recording/screenshot survive a failed send + reload.
            bundle.savedAt = Date.now();
            bundle.error = (e && e.message) || 'Send failed';
            return idbPut(bundle).then(function () {
              pendingRef.current = bundle; setRestored(false);
              setSendState('error'); setErrMsg('Send failed — saved on this device. Tap Retry when you’re back online.');
            }).catch(function () {
              pendingRef.current = bundle;
              setSendState('error'); setErrMsg((e && e.message) || 'Send failed');
            });
          });
        });
      }).catch(function (e) {
        setSendState('error'); setErrMsg((e && e.message) || 'Send failed');
      });
    }

    // Re-send a persisted bundle (from a failed send or restored on load).
    function retrySend() {
      var b = pendingRef.current; if (!b) return;
      setSendState('sending'); setErrMsg(null);
      uploadAndInsert(b).then(function () { return idbDel(b.id).catch(function () {}); }).then(function () {
        pendingRef.current = null; setRestored(false); setSendState('sent'); loadCount();
      }).catch(function (e) {
        setSendState('error'); setErrMsg('Retry failed — still saved on this device. ' + ((e && e.message) || ''));
      });
    }

    function discardPending() {
      var b = pendingRef.current;
      if (b) { idbDel(b.id).catch(function () {}); }
      pendingRef.current = null; setRestored(false); setSendState('idle'); setErrMsg(null);
    }

    function resetAll() {
      setPicked(null); setNote(''); setMode(null); setSendState('idle'); setErrMsg(null);
      pendingRef.current = null; setRestored(false);
      delRec();
      var cv = canvasRef.current;
      if (cv) { var ctx = cv.getContext('2d'); ctx.clearRect(0, 0, cv.width, cv.height); }
      hasDrawing.current = false;
    }

    // clear drawing helper
    function clearDrawing() {
      var cv = canvasRef.current;
      if (cv) { var ctx = cv.getContext('2d'); ctx.clearRect(0, 0, cv.width, cv.height); }
      hasDrawing.current = false;
    }

    // ── styles ── (bubble floats free at VIEWPORT level via draggable bubblePos)
    var bubbleStyle = {
      position: 'fixed', left: bubblePos.x, top: bubblePos.y,
      width: BUBBLE, height: BUBBLE, borderRadius: BUBBLE / 2, zIndex: Z + 5,
      background: GREEN, color: '#fff', border: 'none',
      boxShadow: '0 6px 18px rgba(31,42,36,0.35)', fontSize: 22,
      cursor: 'grab', touchAction: 'none',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    };

    var children = [];

    // pick-mode catcher — FULL-VIEWPORT tap target that blocks the app entirely
    // (every tap is consumed for picking, never reaches the app's handlers).
    if (curMode === 'pick') {
      children.push(h('div', {
        key: 'catcher', id: '__fb_pick_catcher',
        style: {
          position: 'fixed', left: 0, top: 0, width: '100vw', height: '100vh',
          zIndex: Z + 2, background: 'rgba(58,107,62,0.06)', cursor: 'crosshair', touchAction: 'none',
        },
      }));
      // instruction + explicit actions (above the catcher so they're tappable)
      children.push(h('div', {
        key: 'pickbar',
        style: {
          position: 'fixed', left: '50%', top: 10, transform: 'translateX(-50%)', zIndex: Z + 3,
          display: 'flex', gap: 6, alignItems: 'center', background: 'rgba(255,255,255,0.97)',
          padding: '8px 10px', borderRadius: 12, boxShadow: '0 4px 14px rgba(0,0,0,0.2)',
          fontFamily: 'Inter, system-ui, sans-serif', maxWidth: 'calc(100vw - 16px)', flexWrap: 'wrap', justifyContent: 'center',
        },
      }, [
        h('span', { key: 'lbl', style: { fontSize: 12, color: '#1f2a24', fontWeight: 600, marginRight: 4 } },
          picked ? 'Tap to re-pick, or Lock in' : 'Tap an element to select it'),
        h('button', { key: 'lock', 'data-testid': 'fb-lockin', onClick: lockInPick, disabled: !picked,
          style: { padding: '6px 12px', borderRadius: 8, border: 'none', background: picked ? GREEN : '#b9c6bb', color: '#fff', fontSize: 12, fontWeight: 700, cursor: picked ? 'pointer' : 'default' } }, 'Lock in'),
        h('button', { key: 'clear', 'data-testid': 'fb-clearpick', onClick: clearPick,
          style: { padding: '6px 10px', borderRadius: 8, border: '1px solid #e3c4c4', background: '#fff', color: '#e23c3c', fontSize: 12, cursor: 'pointer' } }, 'Clear'),
        h('button', { key: 'cancel', onClick: cancelPick,
          style: { padding: '6px 10px', borderRadius: 8, border: '1px solid #ddd', background: '#fff', color: '#556', fontSize: 12, cursor: 'pointer' } }, 'Cancel'),
      ]));
    }

    // draw canvas — ALWAYS mounted so the drawing persists after "Done" (and so
    // it composites into the screenshot). It only captures input in draw mode;
    // otherwise it's a click-through overlay showing the annotation on the app.
    children.push(h(DrawCanvas, { key: 'draw', rect: rect, tool: curTool, color: curColor, canvasRef: canvasRef, active: curMode === 'draw', onDraw: function () { hasDrawing.current = true; } }));
    if (curMode === 'draw') {
      // draw toolbar
      var swatches = ['#e23c3c', '#f5a623', '#2f6b52', '#2b6cb0', '#1f2a24'];
      children.push(h('div', {
        key: 'drawbar',
        style: {
          position: 'fixed', left: rect.left + 8, top: rect.top + 8, zIndex: Z + 3,
          display: 'flex', gap: 6, alignItems: 'center', background: 'rgba(255,255,255,0.96)',
          padding: '6px 8px', borderRadius: 12, boxShadow: '0 4px 12px rgba(0,0,0,0.18)', flexWrap: 'wrap', maxWidth: rect.width - 16,
        },
      }, [
        h('button', { key: 'pen', onClick: function () { setTool('pen'); }, style: toolBtn(curTool === 'pen') }, '✏️'),
        h('button', { key: 'arr', onClick: function () { setTool('arrow'); }, style: toolBtn(curTool === 'arrow') }, '↗'),
      ].concat(swatches.map(function (c) {
        return h('button', {
          key: c, onClick: function () { setColor(c); },
          style: { width: 22, height: 22, borderRadius: 11, background: c, border: curColor === c ? '2px solid #1f2a24' : '2px solid #fff', cursor: 'pointer' },
        });
      })).concat([
        h('button', { key: 'clr', onClick: clearDrawing, style: { marginLeft: 4, padding: '4px 8px', borderRadius: 8, border: '1px solid #ddd', background: '#fff', fontSize: 12, cursor: 'pointer' } }, 'Clear'),
        h('button', { key: 'done', onClick: function () { hasDrawing.current = true; setMode(null); }, style: { padding: '4px 10px', borderRadius: 8, border: 'none', background: GREEN, color: '#fff', fontSize: 12, cursor: 'pointer' } }, 'Done'),
      ])));
    }

    // picked-element highlight — red rect tracking the current pick. Shown while
    // selecting (so re-picks are visible) and after lock-in; hidden during draw.
    if (picked && picked.rect && curMode !== 'draw') {
      children.push(h('div', {
        key: 'hl', 'data-testid': 'fb-highlight',
        style: {
          position: 'fixed', left: picked.rect.x, top: picked.rect.y, width: picked.rect.w, height: picked.rect.h,
          border: '2px solid #e23c3c', borderRadius: 4, zIndex: Z + 3, pointerEvents: 'none',
        },
      }));
    }

    // bubble (only when panel closed and no mode) + unprocessed-count badge
    if (!isOpen && !curMode) {
      var bubbleKids = ['🐞'];
      if (sentCount) {
        bubbleKids.push(h('span', {
          key: 'badge',
          style: {
            position: 'absolute', top: -4, right: -4, minWidth: 18, height: 18, padding: '0 4px',
            borderRadius: 9, background: '#e23c3c', color: '#fff', fontSize: 11, fontWeight: 700,
            display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #fff',
          },
        }, String(sentCount)));
      }
      children.push(h('button', { key: 'bubble', 'data-testid': 'fb-bubble', onPointerDown: onBubbleDown, style: bubbleStyle, title: 'Drag to move · tap to open' }, bubbleKids));
    }

    // main panel — floats at VIEWPORT level; draggable by its header. Default
    // (panelPos null) = bottom-centered so the Send button is always reachable.
    if (isOpen) {
      // Compact panel: ~65% of the old 400px width (min 260 so controls stay
      // tappable on a phone), tighter paddings/fonts for ~80% of the old height.
      var panelW = Math.min(window.innerWidth - 16, 260);
      var pStyle = {
        position: 'fixed', width: panelW, zIndex: Z + 4,
        background: '#fff', borderRadius: 14, boxShadow: '0 12px 40px rgba(31,42,36,0.35)',
        maxHeight: 'calc(100vh - 24px)', overflowY: 'auto', padding: 8,
        fontFamily: 'Inter, system-ui, sans-serif', color: '#1f2a24',
      };
      if (panelPos) {
        pStyle.left = panelPos.x; pStyle.top = panelPos.y;
      } else {
        pStyle.left = Math.round((window.innerWidth - panelW) / 2);
        pStyle.bottom = 'calc(env(safe-area-inset-bottom) + 12px)';
      }
      var panel = h('div', { key: 'panel', 'data-testid': 'fb-panel', ref: panelRef, style: pStyle }, buildPanel());
      children.push(panel);
    }

    function buildPanel() {
      if (sendState === 'sent') {
        return h('div', { style: { textAlign: 'center', padding: '10px 4px' } }, [
          h('div', { key: 'ck', style: { fontSize: 34 } }, '✅'),
          h('div', { key: 'msg', style: { fontWeight: 700, margin: '6px 0' } }, 'Sent — thanks!'),
          (typeof sentCount === 'number' ? h('div', { key: 'cnt', style: { fontSize: 12, color: '#889', marginBottom: 4 } }, sentCount + ' waiting to be processed') : null),
          h('div', { key: 'btns', style: { display: 'flex', gap: 8, marginTop: 8 } }, [
            h('button', { key: 'again', onClick: resetAll, style: primaryBtn() }, 'Send another'),
            h('button', { key: 'close', onClick: function () { resetAll(); setOpen(false); }, style: ghostBtn() }, 'Close'),
          ]),
        ]);
      }
      var rows = [];
      rows.push(h('div', { key: 'hdr', 'data-testid': 'fb-drag', onPointerDown: onPanelHeaderDown,
        style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4, cursor: 'move', touchAction: 'none', userSelect: 'none' } }, [
        h('div', { key: 't', style: { fontWeight: 800, fontSize: 12.5 } }, '⠿ 🐞 Dev feedback'),
        h('div', { key: 'r', style: { display: 'flex', alignItems: 'center', gap: 6 } }, [
          (typeof sentCount === 'number' ? h('span', { key: 'cnt', 'data-testid': 'fb-count', style: { fontSize: 10.5, color: '#889' } }, sentCount + ' sent') : null),
          h('button', { key: 'x', onClick: function () { setOpen(false); }, style: { border: 'none', background: 'transparent', fontSize: 18, lineHeight: 1, cursor: 'pointer', color: '#889', padding: '0 2px' } }, '✕'),
        ]),
      ]));
      rows.push(h('div', { key: 'route', style: { fontSize: 10.5, color: '#889', marginBottom: 4 } }, 'On: ' + inferRoute()));

      // action buttons row
      rows.push(h('div', { key: 'acts', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginBottom: 4 } }, [
        h('button', { key: 'pick', 'data-testid': 'fb-pick', onClick: enterPick, style: actionBtn(!!picked) }, picked ? '✓ 1 selected' : 'Select el.'),
        h('button', { key: 'draw', 'data-testid': 'fb-draw', onClick: function () { setOpen(false); setMode('draw'); }, style: actionBtn(hasDrawing.current) }, hasDrawing.current ? '✓ Drawing' : 'Draw'),
      ]));

      if (picked) {
        rows.push(h('div', { key: 'pinfo', style: { fontSize: 10.5, background: '#f6f7f6', borderRadius: 8, padding: '5px 7px', marginBottom: 4, wordBreak: 'break-word' } }, [
          h('b', { key: 'b' }, picked.testid ? '[' + picked.testid + '] ' : ''),
          (picked.tag || '') + (picked.text ? ' — “' + picked.text + '”' : ''),
          h('button', { key: 'clr', onClick: function () { setPicked(null); }, style: { marginLeft: 6, border: 'none', background: 'transparent', color: '#e23c3c', cursor: 'pointer', fontSize: 10.5 } }, 'clear'),
        ]));
      }

      // voice note
      var voiceRow;
      if (recState === 'recording') {
        voiceRow = h('div', { style: voiceBox() }, [
          h('span', { key: 'dot', style: { color: '#e23c3c', fontWeight: 700 } }, '● ' + fmtTime(recMs)),
          h('button', { key: 'stop', onClick: stopRec, style: smallBtn() }, 'Stop'),
        ]);
      } else if (recState === 'recorded') {
        voiceRow = h('div', { style: voiceBox() }, [
          h('span', { key: 'ok', style: { color: '#2f6b52', fontWeight: 600 } }, '🎙 Voice note ' + fmtTime(recMs)),
          h('button', { key: 're', onClick: startRec, style: smallBtn() }, 'Re-record'),
          h('button', { key: 'del', onClick: delRec, style: smallBtn('#e23c3c') }, 'Delete'),
        ]);
      } else if (recState === 'denied') {
        voiceRow = h('div', { style: voiceBox() }, [
          h('span', { key: 'd', style: { color: '#a15', fontSize: 12 } }, 'Mic unavailable — continuing without audio'),
          h('button', { key: 'retry', onClick: startRec, style: smallBtn() }, 'Retry'),
        ]);
      } else {
        voiceRow = h('div', { style: voiceBox() }, [
          h('span', { key: 'l', style: { fontSize: 12, color: '#556' } }, 'Voice note'),
          h('button', { key: 'rec', 'data-testid': 'fb-record', onClick: startRec, style: smallBtn() }, '● Record'),
        ]);
      }
      rows.push(h('div', { key: 'voice', style: { marginBottom: 4 } }, voiceRow));

      // typed note
      rows.push(h('textarea', {
        key: 'note', 'data-testid': 'fb-note', value: note, onChange: function (e) { setNote(e.target.value); },
        placeholder: 'Type a note…', rows: 2,
        style: { width: '100%', border: '1px solid #dfe6df', borderRadius: 9, padding: 7, minHeight: 40, fontFamily: 'inherit', fontSize: 13, resize: 'vertical', marginBottom: 4, boxSizing: 'border-box' },
      }));

      // Failed-vs-saved status line. A persisted (recoverable) bundle shows amber
      // "saved on device"; a transient error with nothing saved shows red.
      var savedLocally = !!pendingRef.current;
      if (errMsg) rows.push(h('div', {
        key: 'err', 'data-testid': 'fb-status',
        style: { color: savedLocally ? '#8a6d00' : '#c0392b', background: savedLocally ? '#fdf6e3' : 'transparent', border: savedLocally ? '1px solid #efe0b0' : 'none', borderRadius: 8, padding: savedLocally ? '6px 8px' : 0, fontSize: 11.5, marginBottom: 6 },
      }, (savedLocally ? '💾 ' : '') + errMsg));

      if (savedLocally) {
        // Recoverable bundle present → Retry / Discard (keeps the recording safe).
        rows.push(h('button', {
          key: 'retry', 'data-testid': 'fb-retry', disabled: sendState === 'sending',
          onClick: retrySend, style: primaryBtn(sendState === 'sending'),
        }, sendState === 'sending' ? 'Sending…' : 'Retry send'));
        rows.push(h('button', {
          key: 'discard', 'data-testid': 'fb-discard', onClick: discardPending,
          style: Object.assign(ghostBtn(), { marginTop: 6 }),
        }, 'Discard saved feedback'));
      } else {
        rows.push(h('button', {
          key: 'send', 'data-testid': 'fb-send', disabled: sendState === 'sending',
          onClick: send, style: primaryBtn(sendState === 'sending'),
        }, sendState === 'sending' ? 'Sending…' : 'Send feedback'));
      }

      return rows;
    }

    return h('div', null, children);
  }

  // ── style helpers ──
  function toolBtn(active) {
    return { width: 30, height: 26, borderRadius: 8, border: active ? '2px solid ' + GREEN : '1px solid #ddd', background: active ? '#eef4ee' : '#fff', cursor: 'pointer', fontSize: 14 };
  }
  function actionBtn(done) {
    return { minHeight: 40, padding: '6px', borderRadius: 9, border: done ? '1px solid ' + GREEN : '1px solid #dfe6df', background: done ? '#eef4ee' : '#fff', color: '#1f2a24', fontWeight: 600, fontSize: 12, cursor: 'pointer' };
  }
  function primaryBtn(busy) {
    return { width: '100%', minHeight: 42, padding: '9px', borderRadius: 9, border: 'none', background: busy ? '#8aa88f' : GREEN, color: '#fff', fontWeight: 700, fontSize: 13, cursor: busy ? 'default' : 'pointer' };
  }
  function ghostBtn() {
    return { width: '100%', minHeight: 42, padding: '9px', borderRadius: 9, border: '1px solid #dfe6df', background: '#fff', color: '#1f2a24', fontWeight: 600, fontSize: 13, cursor: 'pointer' };
  }
  function smallBtn(color) {
    return { minHeight: 40, padding: '6px 12px', borderRadius: 8, border: '1px solid #dfe6df', background: '#fff', color: color || '#1f2a24', fontSize: 12, cursor: 'pointer' };
  }
  function voiceBox() {
    return { display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'space-between', background: '#f6f7f6', borderRadius: 9, padding: '6px 8px' };
  }

  // ── mount (dev only) ──
  var rootDiv = document.createElement('div');
  rootDiv.id = '__fb_root';
  document.body.appendChild(rootDiv);
  window.ReactDOM.createRoot(rootDiv).render(h(FeedbackToolbar));
})();
