// flags.jsx — Flag-from-anywhere widget, backed by Supabase.
// File a flag → POST to public.cla_flags. Mark fixed / dismissed inline.
// Claude Code can read + update via the same REST endpoint.

(function(){
  const SB_URL = 'https://blpvjuykqxdnuyiqivwu.supabase.co';
  const SB_ANON = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJscHZqdXlrcXhkbnV5aXFpdnd1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE2NDYxODAsImV4cCI6MjA4NzIyMjE4MH0.6WT8NB4CjVWsxjBCRqIzevsT7HgL04cKNMbcz7JqQnw';
  const TABLE = 'cla_flags';

  function api(path, opts = {}){
    return fetch(`${SB_URL}/rest/v1/${path}`, {
      ...opts,
      headers: {
        apikey: SB_ANON,
        Authorization: `Bearer ${SB_ANON}`,
        'Content-Type': 'application/json',
        Prefer: opts.prefer || 'return=representation',
        ...(opts.headers || {}),
      },
    });
  }
  // `status` is constrained to open/fixed/dismissed, so a claim can't be its own
  // status. Instead a claimed flag stays status=open with a `wip:<worker>` token
  // in resolution_note (+ resolved_at = claim time). So:
  //   open        = status open, NOT claimed
  //   in_progress = status open AND claimed (resolution_note set)
  function filterQuery(statusFilter){
    if (statusFilter === 'in_progress') return '&status=eq.open&resolution_note=not.is.null';
    if (statusFilter === 'open')        return '&status=eq.open&resolution_note=is.null';
    if (statusFilter && statusFilter !== 'all') return `&status=eq.${statusFilter}`;
    return '';
  }
  async function fetchFlags(statusFilter){
    const r = await api(`${TABLE}?order=created_at.desc${filterQuery(statusFilter)}`);
    return r.ok ? r.json() : [];
  }
  // Who filed it + what they were just doing (2026-08-04, Tim: track where a
  // flag comes from + the flagger's ~25 recent actions, so Hidde's flags carry
  // their own repro context). `flagged_by` = signed-in email (localStorage
  // session mirror — same read the beacon uses); `location` jsonb = app/url/
  // team/session ids + the beacon's action trail (window.claUxTrail, auth.jsx).
  // Every field is best-effort: a signed-out or beacon-less page still files
  // a plain {page, note} flag exactly like before.
  function flagAuthor(){
    try {
      const s = JSON.parse(localStorage.getItem('cla-auth') || 'null');
      const u = s && (s.user || (s.currentSession && s.currentSession.user) || (s.session && s.session.user));
      return (u && u.email) || null;
    } catch (_) { return null; }
  }
  function flagContext(){
    const ctx = { app: 'planner' };
    try { ctx.url = (location.pathname || '') + (location.search || '') + (location.hash || ''); } catch (_) {}
    try { ctx.team_id = localStorage.getItem('cla-current-team') || null; } catch (_) {}
    try { ctx.flow_id = (localStorage.getItem('cla-flow') || '').split('|')[0] || null; } catch (_) {}
    try { ctx.session_key = window.claUxSessionKey || null; } catch (_) {}
    try { ctx.version = (typeof V6_VERSION !== 'undefined') ? V6_VERSION : null; } catch (_) {}
    try { ctx.viewport = window.innerWidth + 'x' + window.innerHeight; } catch (_) {}
    try { ctx.trail = (typeof window.claUxTrail === 'function') ? window.claUxTrail() : []; } catch (_) { ctx.trail = []; }
    return ctx;
  }
  async function insertFlag(page, note){
    const body = { page, note, flagged_by: flagAuthor(), location: flagContext() };
    const r = await api(TABLE, { method:'POST', body: JSON.stringify(body) });
    if (!r.ok) throw new Error(await r.text());
    const rows = await r.json();
    return rows[0];
  }
  async function updateFlag(id, patch){
    const r = await api(`${TABLE}?id=eq.${id}`, { method:'PATCH', body: JSON.stringify(patch) });
    if (!r.ok) throw new Error(await r.text());
    const rows = await r.json();
    return rows[0];
  }
  async function deleteFlag(id){
    const r = await api(`${TABLE}?id=eq.${id}`, { method:'DELETE', prefer:'return=minimal' });
    if (!r.ok) throw new Error(await r.text());
  }

  function shortStamp(iso){
    const d = new Date(iso);
    const mm = String(d.getMonth()+1).padStart(2,'0');
    const dd = String(d.getDate()).padStart(2,'0');
    const hh = String(d.getHours()).padStart(2,'0');
    const mi = String(d.getMinutes()).padStart(2,'0');
    return `${d.getFullYear()}-${mm}-${dd} ${hh}:${mi}`;
  }
  // Age of an in_progress claim. >10 min = stale (lock auto-expires), so other
  // terminals may reclaim it — mirrors the HoopsEnglish reviewing_by 10-min lock.
  function claimAge(iso){
    if (!iso) return '';
    const m = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
    if (m < 1) return 'just now';
    if (m < 60) return m + 'm ago';
    return Math.floor(m/60) + 'h ago';
  }
  function pagePath(){
    const p = window.location.pathname || '/';
    return p === '/' ? '/' : p.replace(/\/$/, '');
  }
  function toMarkdown(flags, filter){
    const label = filter === 'all' ? '' : ` (${filter})`;
    if (!flags.length) return `## CLAP — no${label} flags\n`;
    const header = `## CLAP — ${filter} flags (${flags.length}) · ${shortStamp(new Date().toISOString())}\n\n`;
    return header + flags.map(f => {
      const claimed = f.status === 'open' && f.resolution_note;
      const checkbox = claimed ? '[-]' : f.status === 'open' ? '[ ]' : f.status === 'fixed' ? '[x]' : '[~]';
      const status = claimed
        ? ` _(in progress · ${(f.resolution_note||'').replace(/^wip:/,'')})_`
        : f.status !== 'open' ? ` _(${f.status}${f.resolved_at ? ' ' + shortStamp(f.resolved_at) : ''})_` : '';
      const by = f.flagged_by ? ` · by ${String(f.flagged_by).replace(/@.*$/, '')}` : '';
      return `- ${checkbox} [${f.page}] ${f.note}  · filed ${shortStamp(f.created_at)}${by}${status}`;
    }).join('\n') + '\n';
  }

  const ink = '#16110e';
  const paper = '#f4ede2';
  const muted = 'rgba(22,17,14,.55)';
  const accent = '#c0492c';

  function FlagWidget(){
    const [open, setOpen] = React.useState(false);
    const [filter, setFilter] = React.useState('open');
    const [flags, setFlags] = React.useState([]);
    const [draft, setDraft] = React.useState('');
    const [busy, setBusy] = React.useState(false);
    const [toast, setToast] = React.useState('');

    function flash(msg){ setToast(msg); setTimeout(() => setToast(''), 2200); }

    async function reload(f = filter){
      setBusy(true);
      try { setFlags(await fetchFlags(f)); }
      catch (e) { flash('Load failed'); }
      setBusy(false);
    }
    React.useEffect(() => { if (open) reload(filter); }, [open, filter]);

    // CLA flag ebf8d458 (Tim) — let other UI (e.g. the full-screen Settings dialog,
    // where the settings + print preview are both visible) open the flag composer so
    // he can file a flag about exactly what he's looking at.
    React.useEffect(() => {
      const openFlags = () => setOpen(true);
      window.addEventListener('cla:open-flags', openFlags);
      return () => window.removeEventListener('cla:open-flags', openFlags);
    }, []);
    // CLA flag a43e3c64 (Tim) — broadcast open/closed so host UI (the full-screen
    // Settings dialog) can keep itself open while the coach writes a flag.
    React.useEffect(() => {
      try { window.dispatchEvent(new CustomEvent('cla:flags-open', { detail: { open } })); } catch (_) {}
    }, [open]);

    // unobtrusive open-count badge — fetch on first render even when closed
    const [openCount, setOpenCount] = React.useState(null);
    React.useEffect(() => {
      let live = true;
      fetchFlags('open').then(rows => { if (live) setOpenCount(rows.length); }).catch(() => {});
      return () => { live = false; };
    }, [flags]); // refresh badge whenever flags change

    async function addFlag(){
      const note = draft.trim();
      if (!note) return;
      setBusy(true);
      try {
        const row = await insertFlag(pagePath(), note);
        setDraft('');
        if (filter === 'open' || filter === 'all') setFlags([row, ...flags]);
        flash('Flag saved');
      } catch (e) { flash('Save failed'); }
      setBusy(false);
    }
    async function setStatus(id, newStatus){
      // Releasing → open clears the claim. Fixing/dismissing also clears the
      // in_progress claim token from resolution_note (terminals set a real note
      // via the API when they resolve; the widget's button has none).
      const patch = newStatus === 'open'
        ? { status: 'open', resolved_at: null, resolution_note: null }
        : { status: newStatus, resolved_at: new Date().toISOString(), resolution_note: null };
      // optimistic remove from the current view, then reconcile (derived filters
      // — open/in_progress — can't be reproduced by a local status compare).
      setFlags(flags.filter(f => f.id !== id || filter === 'all'));
      try { await updateFlag(id, patch); flash(newStatus === 'open' ? 'Released' : newStatus === 'fixed' ? 'Marked fixed' : 'Dismissed'); reload(); }
      catch (e) { flash('Update failed'); reload(); }
    }
    async function removeFlag(id){
      if (!window.confirm('Permanently delete this flag?')) return;
      setFlags(flags.filter(f => f.id !== id));
      try { await deleteFlag(id); flash('Deleted'); }
      catch (e) { flash('Delete failed'); reload(); }
    }
    async function copyMarkdown(){
      try {
        await navigator.clipboard.writeText(toMarkdown(flags, filter));
        flash('Copied to clipboard');
      } catch { flash('Copy failed'); }
    }

    const btn = {
      position:'fixed', bottom: 20, right: 20, zIndex: 99999,
      width: 44, height: 44, borderRadius: 22, background: ink, color: paper,
      border: 0, cursor:'pointer', boxShadow:'0 8px 24px -8px rgba(0,0,0,.45)',
      fontSize: 18, display:'flex', alignItems:'center', justifyContent:'center',
    };
    const badge = openCount ? {
      position:'absolute', top: -4, right: -4, minWidth: 18, height: 18,
      borderRadius: 9, background: accent, color: paper,
      fontSize: 10, fontWeight: 700, fontFamily:'"JetBrains Mono", monospace',
      display:'flex', alignItems:'center', justifyContent:'center',
      padding:'0 5px', boxShadow:'0 2px 6px rgba(0,0,0,.3)',
    } : null;

    if (!open) {
      return (
        <button onClick={()=>setOpen(true)} style={btn}
          title={openCount ? `${openCount} open flag${openCount===1?'':'s'}` : 'Flag this page'}>
          <span style={{ position:'relative' }}>
            🚩
            {badge && <span style={badge}>{openCount}</span>}
          </span>
        </button>
      );
    }

    const filters = [
      { v:'open',        label:'Open' },
      { v:'in_progress', label:'In progress' },
      { v:'fixed',       label:'Fixed' },
      { v:'dismissed',   label:'Dismissed' },
      { v:'all',         label:'All' },
    ];

    return (
      <div className="flag-widget" style={{
        position:'fixed', bottom: 20, right: 20, zIndex: 99999,
        width: 380, maxHeight:'82vh', display:'flex', flexDirection:'column',
        background:'#fbf7ed', color: ink, borderRadius: 14,
        boxShadow:'0 30px 70px -20px rgba(0,0,0,.45), inset 0 0 0 .5px rgba(22,17,14,.14)',
        fontFamily:'-apple-system, BlinkMacSystemFont, "SF Pro Display", system-ui, sans-serif',
        fontSize: 13,
      }}>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
          padding:'12px 14px', borderBottom:'.5px solid rgba(22,17,14,.10)' }}>
          <div style={{ display:'flex', alignItems:'baseline', gap: 8 }}>
            <span style={{ fontSize: 14 }}>🚩</span>
            <span style={{ fontFamily:'"Iowan Old Style", Georgia, serif', fontStyle:'italic', fontSize: 16 }}>Flags</span>
            <span style={{ fontFamily:'"JetBrains Mono", monospace', fontSize: 10.5, color: muted, letterSpacing:'.08em' }}>
              {pagePath()}
            </span>
          </div>
          <button onClick={()=>setOpen(false)} title="Close" style={{
            background:'transparent', border: 0, cursor:'pointer', color: muted,
            fontSize: 18, padding: 4,
          }}>×</button>
        </div>

        <div style={{ padding:'12px 14px', borderBottom:'.5px solid rgba(22,17,14,.10)' }}>
          <textarea
            value={draft}
            onChange={(e)=>setDraft(e.target.value)}
            onKeyDown={(e)=>{ if ((e.metaKey||e.ctrlKey) && e.key==='Enter') addFlag(); }}
            placeholder="Flag this — what's wrong / missing / odd? (⌘↵ to save)"
            rows={3}
            style={{
              width:'100%', resize:'vertical', minHeight: 60,
              background:'#fff', color: ink, border:'.5px solid rgba(22,17,14,.18)',
              borderRadius: 8, padding:'10px 12px', fontFamily:'inherit', fontSize: 13,
              boxSizing:'border-box',
            }} />
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginTop: 8 }}>
            <div style={{ display:'flex', gap: 4, background:'rgba(22,17,14,.06)', borderRadius: 6, padding: 2 }}>
              {filters.map(f => (
                <button key={f.v} onClick={()=>setFilter(f.v)} style={{
                  background: filter===f.v ? '#fff' : 'transparent',
                  color: filter===f.v ? ink : muted,
                  border: 0, borderRadius: 5,
                  padding:'4px 8px', fontFamily:'inherit', fontSize: 11.5,
                  fontWeight: filter===f.v ? 600 : 500, cursor:'pointer',
                  boxShadow: filter===f.v ? 'inset 0 0 0 .5px rgba(22,17,14,.10)' : 'none',
                }}>{f.label}</button>
              ))}
            </div>
            <button onClick={addFlag} disabled={!draft.trim() || busy} style={{
              background: (draft.trim() && !busy) ? ink : 'rgba(22,17,14,.25)',
              color: paper, border: 0, borderRadius: 7,
              padding:'7px 14px', fontFamily:'inherit', fontSize: 12, fontWeight: 600,
              cursor: (draft.trim() && !busy) ? 'pointer' : 'not-allowed',
            }}>Add flag</button>
          </div>
        </div>

        <div style={{ flex: 1, overflowY:'auto', padding:'4px 6px' }}>
          {busy && flags.length === 0 && (
            <div style={{ padding:'24px 16px', textAlign:'center', color: muted, fontSize: 12.5 }}>Loading…</div>
          )}
          {!busy && flags.length === 0 && (
            <div style={{ padding:'24px 16px', textAlign:'center', color: muted, fontSize: 12.5, lineHeight: 1.5 }}>
              No {filter === 'all' ? '' : filter + ' '}flags.
            </div>
          )}
          {flags.map((f) => {
            // claimed/in-progress = still open, but a terminal holds a wip: token
            const isClaimed = f.status === 'open' && !!f.resolution_note;
            const statusColor = f.status === 'fixed' ? '#2f8a3e' : f.status === 'dismissed' ? muted : isClaimed ? '#0a84ff' : accent;
            const claimedBy = isClaimed ? (f.resolution_note || 'claimed').replace(/^wip:/, '') : '';
            const claimStale = isClaimed && f.resolved_at && (Date.now() - new Date(f.resolved_at).getTime()) > 10*60*1000;
            return (
              <div key={f.id} style={{
                display:'flex', gap: 8, padding:'10px 8px',
                borderBottom:'.5px solid rgba(22,17,14,.06)',
                opacity: f.status === 'dismissed' ? 0.6 : 1,
              }}>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, lineHeight: 1.4, color: ink, wordBreak:'break-word',
                    textDecoration: f.status === 'fixed' ? 'line-through' : 'none' }}>
                    {f.note}
                  </div>
                  <div style={{ fontSize: 10.5, color: muted, fontFamily:'"JetBrains Mono", monospace',
                    marginTop: 4, display:'flex', gap: 8, alignItems:'center', flexWrap:'wrap', letterSpacing:'.04em' }}>
                    <span style={{ background:'rgba(22,17,14,.06)', padding:'1px 6px', borderRadius: 4 }}>{f.page}</span>
                    {f.flagged_by && (
                      <span title={f.flagged_by}>👤 {String(f.flagged_by).replace(/@.*$/, '')}</span>
                    )}
                    <span>{shortStamp(f.created_at)}</span>
                    {isClaimed ? (
                      <span style={{ color: claimStale ? accent : statusColor, fontWeight: 600 }}>
                        🔧 {claimedBy}{f.resolved_at ? ' · ' + claimAge(f.resolved_at) : ''}{claimStale ? ' · STALE' : ''}
                      </span>
                    ) : f.status !== 'open' ? (
                      <span style={{ color: statusColor, fontWeight: 600, textTransform:'uppercase' }}>
                        {f.status}{f.resolved_at ? ' · ' + shortStamp(f.resolved_at) : ''}
                      </span>
                    ) : null}
                  </div>
                </div>
                <div style={{ display:'flex', flexDirection:'column', gap: 4, alignSelf:'flex-start' }}>
                  {f.status === 'open' && isClaimed ? (
                    <>
                      <button onClick={()=>setStatus(f.id, 'fixed')} title="Mark fixed" style={iconBtn('#2f8a3e')}>✓</button>
                      <button onClick={()=>setStatus(f.id, 'open')} title="Release claim → back to Open" style={iconBtn(accent, 12)}>↺</button>
                    </>
                  ) : f.status === 'open' ? (
                    <>
                      <button onClick={()=>setStatus(f.id, 'fixed')} title="Mark fixed" style={iconBtn('#2f8a3e')}>✓</button>
                      <button onClick={()=>setStatus(f.id, 'dismissed')} title="Dismiss" style={iconBtn(muted)}>×</button>
                    </>
                  ) : (
                    <>
                      <button onClick={()=>setStatus(f.id, 'open')} title="Reopen" style={iconBtn(accent, 12)}>↺</button>
                      <button onClick={()=>removeFlag(f.id)} title="Delete permanently" style={iconBtn(muted, 11)}>🗑</button>
                    </>
                  )}
                </div>
              </div>
            );
          })}
        </div>

        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
          padding:'10px 14px', borderTop:'.5px solid rgba(22,17,14,.10)', background:'#f4ede2',
          borderBottomLeftRadius: 14, borderBottomRightRadius: 14 }}>
          <span style={{ fontSize: 11, color: muted, fontFamily:'"JetBrains Mono", monospace' }}>
            {flags.length} {filter === 'all' ? 'total' : filter}
          </span>
          <div style={{ display:'flex', alignItems:'center', gap: 10 }}>
            {toast && <span style={{ fontSize: 11, color: accent, fontFamily:'"JetBrains Mono", monospace' }}>{toast}</span>}
            <button onClick={copyMarkdown} disabled={!flags.length} style={{
              background: flags.length ? accent : 'rgba(22,17,14,.18)',
              color: paper, border: 0, borderRadius: 7,
              padding:'7px 12px', fontFamily:'inherit', fontSize: 12, fontWeight: 600,
              cursor: flags.length ? 'pointer' : 'not-allowed',
            }}>Copy as Markdown</button>
          </div>
        </div>
      </div>
    );
  }

  function iconBtn(color, fontSize = 14){
    return {
      background:'transparent', border: '.5px solid rgba(22,17,14,.14)', borderRadius: 5,
      cursor:'pointer', color, fontSize, lineHeight: 1,
      width: 24, height: 24, padding: 0,
      display:'flex', alignItems:'center', justifyContent:'center',
    };
  }

  const style = document.createElement('style');
  style.textContent = '@media print { .flag-widget, .flag-widget-mount { display: none !important; } }' +
    '@media (min-width:761px) {' +
      'body:has([data-screen-label="Bottom practice tabs"]) .flag-widget-mount > button { bottom:56px !important; }' +
      'body:has([data-screen-label="Bottom practice tabs"]) .flag-widget { bottom:56px !important; }' +
    '}' +
    '@media (max-width:760px) {' +
      '.flag-widget-mount > button { bottom:68px !important; right:12px !important; }' +
      '.flag-widget { bottom:68px !important; right:12px !important; width:min(380px,calc(100vw - 24px)) !important; }' +
    '}';
  document.head.appendChild(style);

  const mount = document.createElement('div');
  mount.className = 'flag-widget-mount';
  document.body.appendChild(mount);
  ReactDOM.createRoot(mount).render(<FlagWidget />);
})();
