'use strict';
// ═══════════════════════════════════════════════
// js/data.js — Analytics page
// ═══════════════════════════════════════════════

let _dataSpan   = 'month';
let _dataOffset = 0; // 0 = current period, 1 = one period back, etc.

function renderDataPage() {
  const el = document.getElementById('data-content');
  if (!el) return;
  el.innerHTML = _buildDataPage();
}

function _setDataSpan(span) {
  _dataSpan   = span;
  _dataOffset = 0;
  renderDataPage();
}

function _prevDataSpan() {
  if (_dataSpan === 'all') return;
  _dataOffset++;
  renderDataPage();
}

function _nextDataSpan() {
  if (_dataOffset <= 0) return;
  _dataOffset--;
  renderDataPage();
}

const _SPAN_MS = { week: 7 * 86400000, month: 30 * 86400000, '3m': 90 * 86400000, year: 365 * 86400000 };

function _dataSpanRange() {
  const today = new Date();
  today.setHours(23, 59, 59, 999);
  const baseEnd = today.getTime();
  if (_dataSpan === 'all') return { start: 0, end: baseEnd };
  const spanMs = _SPAN_MS[_dataSpan];
  const end    = baseEnd - _dataOffset * spanMs;
  const start  = end - spanMs + 86400000; // inclusive: exactly span_days days
  return { start, end };
}

function _dataSpanLabel() {
  if (_dataSpan === 'all') return 'All Time';
  const { start, end } = _dataSpanRange();
  const MO = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const fmt  = ts => { const d = new Date(ts); return `${MO[d.getMonth()]} ${d.getDate()}`; };
  const fmtY = ts => new Date(ts).getFullYear();
  const sY = fmtY(start), eY = fmtY(end);
  if (sY === eY) return `${fmt(start)} – ${fmt(end)}, ${eY}`;
  return `${fmt(start)} ${sY} – ${fmt(end)} ${eY}`;
}

// Format ms as "1h 23m"
function _dFmt(ms) {
  if (!ms || ms < 60000) return '< 1m';
  const h = Math.floor(ms / 3600000);
  const m = Math.floor((ms % 3600000) / 60000);
  if (h === 0) return `${m}m`;
  if (m === 0) return `${h}h`;
  return `${h}h\u202f${m}m`;
}

// ── Main page builder ─────────────────────────────────────────────────────────

function _buildDataPage() {
  const sessions    = getTimerSessions().filter(s => s.endedAt && s.durationMs > 0);
  const entries     = getEntries();
  const weeklyStats = (typeof getWeeklyStats === 'function') ? getWeeklyStats() : [];
  const { start, end } = _dataSpanRange();

  const fSess  = sessions.filter(s => s.startedAt >= start && s.startedAt <= end);
  const fEntr  = entries.filter(e => { const t = new Date(e.date + 'T12:00:00').getTime(); return t >= start && t <= end; });
  const fWeekly = weeklyStats.filter(w => { const t = new Date(w.date + 'T12:00:00').getTime(); return t >= start && t <= end; });

  const picker = _dBuildSpanPicker();
  const hasAny = sessions.length || entries.length || weeklyStats.length;
  if (!hasAny) {
    return picker + `<div class="empty" style="padding:60px 0"><div class="icon">◷</div><p>No data yet — start logging sessions or journal entries.</p></div>`;
  }

  let html = picker;
  html += _dBuildStatsRow(fSess, fEntr, fWeekly, start, end);
  // Heatmap: show sessions + journal/weekly as historical markers
  if (sessions.length || fEntr.length || fWeekly.length) html += _dBuildHeatmapCard(sessions, fEntr, fWeekly, start, end);
  if (fSess.length) {
    html += `<div class="data-2col">${_dBuildTimeTypeDonut(fSess, fEntr, fWeekly)}${_dBuildDayTypeDonut(fSess, start, end)}</div>`;
  } else if (fWeekly.length || fEntr.length) {
    // No timer sessions but has journal/weekly — show derived time split
    html += `<div class="data-2col">${_dBuildTimeTypeDonut([], fEntr, fWeekly)}${_dBuildDayTypeDonut([], start, end, fEntr, fWeekly)}</div>`;
  }
  // Device pie charts from timer sessions
  const _devSess = fSess.filter(s => s.device);
  if (_devSess.length) html += `<div class="data-2col">${_dBuildDevicePie(_devSess, 'tugging')}${_dBuildDevicePie(_devSess, 'retaining')}</div>`;
  if (fSess.length >= 3) {
    html += `<div class="data-2col">${_dBuildSessionLengthHist(fSess)}${_dBuildTimeOfDayHist(fSess)}</div>`;
  }
  if (fEntr.length >= 2) html += _dBuildDeviceChart(fEntr);
  if (fEntr.length >= 3) html += _dBuildCIChart(fEntr);
  // Quick Check records
  html += _dBuildQuickCheckRecords();
  return html;
}

// ── Span picker ───────────────────────────────────────────────────────────────

function _dBuildSpanPicker() {
  const spans = [
    { key: 'week',  label: 'Week'     },
    { key: 'month', label: 'Month'    },
    { key: '3m',    label: '3 Months' },
    { key: 'year',  label: 'Year'     },
    { key: 'all',   label: 'All Time' },
  ];
  const hasNav  = _dataSpan !== 'all';
  const canNext = hasNav && _dataOffset > 0;
  const navHtml = hasNav ? `
    <div class="data-span-nav">
      <button class="data-span-nav-btn" data-action="prevDataSpan" title="Previous period">&#8249;</button>
      <span class="data-span-period">${_dataSpanLabel()}</span>
      <button class="data-span-nav-btn${canNext ? '' : ' disabled'}" data-action="nextDataSpan" ${canNext ? '' : 'disabled'} title="Next period">&#8250;</button>
    </div>` : '';
  return `<div class="data-span-bar">
    <div class="data-span-types">${
      spans.map(s => `<button class="data-span-btn${_dataSpan === s.key ? ' active' : ''}" data-action="setDataSpan" data-a0="${s.key}">${s.label}</button>`).join('')
    }</div>${navHtml}
  </div>`;
}

// ── Stats row ─────────────────────────────────────────────────────────────────

function _dBuildStatsRow(fSess, fEntr, fWeekly, start, end) {
  const tug     = fSess.filter(s => s.type === 'tugging');
  const ret     = fSess.filter(s => s.type === 'retaining');
  let tugMs   = tug.reduce((a, s) => a + s.durationMs, 0);
  let retMs   = ret.reduce((a, s) => a + s.durationMs, 0);

  // Supplement with weekly stats when no session data
  const usingHistorical = !fSess.length && (fWeekly.length || fEntr.length);
  if (usingHistorical) {
    if (fWeekly.length) {
      tugMs = fWeekly.reduce((a, w) => a + (w.totalHrsTugged   || 0) * 3600000, 0);
      retMs = fWeekly.reduce((a, w) => a + (w.totalHrsRetained || 0) * 3600000, 0);
    } else {
      // Fall back to journal entry avg * ~7 days
      tugMs = fEntr.reduce((a, e) => a + (e.avgHrsTugged   || 0) * 7 * 3600000, 0);
      retMs = fEntr.reduce((a, e) => a + (e.avgHrsRetained || 0) * 7 * 3600000, 0);
    }
  }

  const totalMs    = tugMs + retMs;
  const activeDays = new Set(fSess.map(s => new Date(s.startedAt).toISOString().slice(0, 10))).size;
  const spanDays   = _dataSpan === 'all' && fSess.length
    ? Math.ceil((end - Math.min(...fSess.map(s => s.startedAt))) / 86400000) + 1
    : Math.round((end - start) / 86400000) + 1;
  const avgSession = fSess.length ? Math.round(totalMs / fSess.length) : 0;
  const longestMs  = fSess.length ? Math.max(...fSess.map(s => s.durationMs)) : 0;
  const latestCI   = fEntr.length
    ? [...fEntr].sort((a, b) => b.date.localeCompare(a.date))[0]?.ci
    : null;
  const historicalNote = usingHistorical ? ' (est.)' : '';

  const cards = [
    { val: totalMs    ? _dFmt(totalMs) + historicalNote  : '—', label: 'Total time',  sub: fSess.length ? `${fSess.length} sessions` : usingHistorical ? 'from journal/weekly' : 'no sessions' },
    { val: tugMs      ? _dFmt(tugMs)                     : '—', label: 'Tugging',     sub: tug.length   ? `${tug.length} sessions` : usingHistorical ? 'estimated' : 'none', color: '#22c55e' },
    { val: retMs      ? _dFmt(retMs)                     : '—', label: 'Retaining',   sub: ret.length   ? `${ret.length} sessions` : usingHistorical ? 'estimated' : 'none', color: '#8b5cf6' },
    { val: activeDays ? `${activeDays}`                  : '—', label: 'Active days', sub: spanDays > 1 ? `of ${spanDays}` : '' },
    { val: avgSession ? _dFmt(avgSession)                : '—', label: 'Avg session', sub: longestMs    ? `max ${_dFmt(longestMs)}` : '' },
  ];
  if (latestCI !== null && latestCI !== undefined) {
    cards.push({ val: `CI-${latestCI}`, label: 'Latest CI', sub: `${fEntr.length} entries` });
  }
  return `<div class="data-stats-row">${cards.map(c =>
    `<div class="data-stat-card">
      <div class="data-stat-val"${c.color ? ` style="color:${c.color}"` : ''}>${c.val}</div>
      <div class="data-stat-label">${c.label}</div>
      ${c.sub ? `<div class="data-stat-sub">${c.sub}</div>` : ''}
    </div>`
  ).join('')}</div>`;
}

// ── Heatmap card ──────────────────────────────────────────────────────────────

// Build day map from journal entries (avgHrs/day) and weekly stats (totalHrs/week ÷ 7)
function _dBuildHistoricalDayMap(fEntr, fWeekly, start, end) {
  const hist = {};
  // Weekly stats: distribute total hours evenly across each day of the week
  for (const w of fWeekly) {
    const monT = new Date(w.date + 'T12:00:00').getTime();
    for (let d = 0; d < 7; d++) {
      const dayT = monT + d * 86400000;
      if (dayT < start || dayT > end) continue;
      const key = new Date(dayT).toISOString().slice(0, 10);
      const tugMs = ((w.totalHrsTugged   || 0) / 7) * 3600000;
      const retMs = ((w.totalHrsRetained || 0) / 7) * 3600000;
      if (!hist[key]) hist[key] = { tugMs: 0, retMs: 0, historical: true };
      hist[key].tugMs += tugMs;
      hist[key].retMs += retMs;
    }
  }
  // Journal entries: avgHrsTugged/avgHrsRetained represent per-day averages on that entry's date
  for (const e of fEntr) {
    const dayT = new Date(e.date + 'T12:00:00').getTime();
    if (dayT < start || dayT > end) continue;
    if (!hist[e.date]) hist[e.date] = { tugMs: 0, retMs: 0, historical: true };
    // Only add if not already covered by weekly stats (prefer weekly stats)
    if (!hist[e.date].tugMs) hist[e.date].tugMs += (e.avgHrsTugged   || 0) * 3600000;
    if (!hist[e.date].retMs) hist[e.date].retMs += (e.avgHrsRetained || 0) * 3600000;
  }
  return hist;
}

function _dBuildHeatmapCard(allSessions, fEntr, fWeekly, start, end) {
  const dayMap = {};
  for (const s of allSessions.filter(s => s.startedAt >= start && s.startedAt <= end)) {
    const day = new Date(s.startedAt).toISOString().slice(0, 10);
    if (!dayMap[day]) dayMap[day] = { tugMs: 0, retMs: 0 };
    if (s.type === 'tugging') dayMap[day].tugMs += s.durationMs;
    else dayMap[day].retMs += s.durationMs;
  }
  const histMap = _dBuildHistoricalDayMap(fEntr, fWeekly, start, end);
  // Merge: session data takes priority over historical data for a given day
  for (const [day, h] of Object.entries(histMap)) {
    if (!dayMap[day]) dayMap[day] = { ...h };
  }
  if (_dataSpan === 'week') return _dBuildWeekBars(dayMap, start, end);
  return _dBuildCalendarHeatmap(dayMap, start, end);
}

function _dBuildWeekBars(dayMap, start, end) {
  const days = [];
  for (let t = start; t <= end; t += 86400000) {
    const d   = new Date(t);
    const key = d.toISOString().slice(0, 10);
    days.push({ key, d, ...(dayMap[key] || { tugMs: 0, retMs: 0 }) });
  }
  const maxMs = Math.max(...days.map(d => d.tugMs + d.retMs), 3600000);
  const W = 500, H = 120, PAD = { l: 36, r: 8, t: 8, b: 28 };
  const cW  = (W - PAD.l - PAD.r) / days.length;
  const cH  = H - PAD.t - PAD.b;
  const barW = cW * 0.7, barX = (cW - cW * 0.7) / 2;
  const DAY  = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

  const gridLines = [0, 0.5, 1].map(f => {
    const y = PAD.t + cH * (1 - f);
    return `<line x1="${PAD.l}" y1="${y}" x2="${W - PAD.r}" y2="${y}" stroke="var(--border2)" stroke-width="0.5"/>
      <text x="${PAD.l - 3}" y="${y + 3}" font-size="7" fill="var(--muted2)" text-anchor="end">${f ? _dFmt(maxMs * f) : ''}</text>`;
  }).join('');

  const bars = days.map((d, i) => {
    const x     = PAD.l + i * cW + barX;
    const baseY = PAD.t + cH;
    const tugH  = (d.tugMs / maxMs) * cH;
    const retH  = (d.retMs / maxMs) * cH;
    const title = (d.tugMs || d.retMs)
      ? `${d.key}: ${_dFmt(d.tugMs + d.retMs)}${d.tugMs ? ' (tug\u00a0' + _dFmt(d.tugMs) + ')' : ''}${d.retMs ? ' (ret\u00a0' + _dFmt(d.retMs) + ')' : ''}`
      : `${d.key}: no sessions`;
    return `<g><title>${title}</title>
      <rect x="${x}" y="${baseY - retH}" width="${barW}" height="${retH}" fill="#8b5cf6" opacity="0.85" rx="2"/>
      <rect x="${x}" y="${baseY - retH - tugH}" width="${barW}" height="${tugH}" fill="#22c55e" opacity="0.85" rx="2"/>
      <text x="${PAD.l + i * cW + cW / 2}" y="${H - 14}" font-size="9" fill="var(--muted)" text-anchor="middle">${DAY[d.d.getDay()]}</text>
      <text x="${PAD.l + i * cW + cW / 2}" y="${H - 4}" font-size="8" fill="var(--muted2)" text-anchor="middle">${d.d.getDate()}</text>
    </g>`;
  }).join('');

  return `<div class="data-card">
    <div class="data-card-title">Daily Sessions</div>
    <div class="data-legend-row">
      <span class="data-ldot" style="background:#22c55e"></span>Tugging
      <span class="data-ldot" style="background:#8b5cf6;margin-left:10px"></span>Retaining
    </div>
    <svg viewBox="0 0 ${W} ${H}" style="width:100%;display:block">${gridLines}${bars}</svg>
  </div>`;
}

function _dBuildCalendarHeatmap(dayMap, start, end) {
  const startDay = new Date(start); startDay.setHours(0, 0, 0, 0);
  const endDay   = new Date(end);   endDay.setHours(23, 59, 59, 999);

  // Roll back to Monday of the start week
  const firstMon = new Date(startDay);
  firstMon.setDate(startDay.getDate() - ((startDay.getDay() + 6) % 7));

  const cells = [];
  for (let d = new Date(firstMon); d <= endDay; d = new Date(d.getTime() + 86400000)) {
    const key = d.toISOString().slice(0, 10);
    cells.push({ key, inRange: d >= startDay, ...(dayMap[key] || { tugMs: 0, retMs: 0 }) });
  }

  const weeks    = Math.ceil(cells.length / 7);
  const maxMs    = Math.max(...cells.map(c => c.tugMs + c.retMs), 1);
  const cellSize = (_dataSpan === 'year' || _dataSpan === 'all') ? 11 : _dataSpan === '3m' ? 14 : 18;
  const gap  = 2, step = cellSize + gap;
  const lPad = 24, tPad = 18;
  const svgW = lPad + weeks * step;
  const svgH = tPad + 7 * step;

  const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  let monthLabels = '', prevMo = -1;
  cells.forEach((c, i) => {
    if (!c.inRange) return;
    const mo = new Date(c.key + 'T12:00:00').getMonth();
    if (mo !== prevMo) {
      prevMo = mo;
      monthLabels += `<text x="${lPad + Math.floor(i / 7) * step}" y="${tPad - 5}" font-size="9" fill="var(--muted2)">${MONTHS[mo]}</text>`;
    }
  });

  const DAY_ABBR = ['Mo','Tu','We','Th','Fr','Sa','Su'];
  const dayLabels = [0, 2, 4, 6].map(row =>
    `<text x="${lPad - 3}" y="${tPad + row * step + cellSize - 1}" font-size="8" fill="var(--muted2)" text-anchor="end">${DAY_ABBR[row]}</text>`
  ).join('');

  const cellColor = (tugMs, retMs) => {
    const total = tugMs + retMs;
    if (!total) return null;
    const intensity = (0.25 + 0.75 * Math.min(total / maxMs, 1)).toFixed(2);
    if (!retMs) return `rgba(34,197,94,${intensity})`;
    if (!tugMs) return `rgba(139,92,246,${intensity})`;
    return tugMs >= retMs
      ? `rgba(20,184,166,${intensity})`
      : `rgba(99,102,241,${intensity})`;
  };

  const rects = cells.map((c, i) => {
    const col  = Math.floor(i / 7), row = i % 7;
    const fill = c.inRange ? (cellColor(c.tugMs, c.retMs) || 'var(--surface3,#252e3d)') : 'none';
    const total = c.tugMs + c.retMs;
    const title = total
      ? `${c.key}: ${_dFmt(total)}${c.tugMs ? ' · tug\u00a0' + _dFmt(c.tugMs) : ''}${c.retMs ? ' · ret\u00a0' + _dFmt(c.retMs) : ''}`
      : c.inRange ? `${c.key}: no sessions` : '';
    return `<rect x="${lPad + col * step}" y="${tPad + row * step}" width="${cellSize}" height="${cellSize}" rx="2" fill="${fill}" stroke="${c.inRange ? 'var(--border2)' : 'none'}" stroke-width="0.5">${title ? `<title>${title}</title>` : ''}</rect>`;
  }).join('');

  return `<div class="data-card">
    <div class="data-card-title">Session Activity</div>
    <div class="data-legend-row" style="margin-bottom:8px">
      <span class="data-ldot" style="background:#22c55e"></span>Tugging
      <span class="data-ldot" style="background:#8b5cf6;margin-left:8px"></span>Retaining
      <span class="data-ldot" style="background:#14b8a6;margin-left:8px"></span>Both
      <span style="margin-left:auto;font-size:10px;color:var(--muted2)">Darker = more time</span>
    </div>
    <div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
      <svg viewBox="0 0 ${svgW} ${svgH}" width="${svgW}" height="${svgH}" style="display:block">
        ${monthLabels}${dayLabels}${rects}
      </svg>
    </div>
  </div>`;
}

// ── Donut helpers ─────────────────────────────────────────────────────────────

function _dDonutPaths(slices, cx, cy, r, inner) {
  const total = slices.reduce((a, s) => a + (s.value || 0), 0);
  if (!total) return '';
  let angle = -Math.PI / 2;
  return slices.map(s => {
    if (!s.value) return '';
    const sweep = (s.value / total) * 2 * Math.PI;
    const x1 = cx + r * Math.cos(angle),           y1 = cy + r * Math.sin(angle);
    angle += sweep;
    const x2  = cx + r     * Math.cos(angle),      y2  = cy + r     * Math.sin(angle);
    const xi1 = cx + inner * Math.cos(angle),       yi1 = cy + inner * Math.sin(angle);
    const xi2 = cx + inner * Math.cos(angle-sweep), yi2 = cy + inner * Math.sin(angle-sweep);
    const lg  = sweep > Math.PI ? 1 : 0;
    const fmt = n => n.toFixed(1);
    return `<path d="M${fmt(x1)},${fmt(y1)} A${r},${r} 0 ${lg},1 ${fmt(x2)},${fmt(y2)} L${fmt(xi1)},${fmt(yi1)} A${inner},${inner} 0 ${lg},0 ${fmt(xi2)},${fmt(yi2)} Z" fill="${s.color}"><title>${s.label}</title></path>`;
  }).join('');
}

// ── Donut: Tugging vs Retaining time ─────────────────────────────────────────

function _dBuildTimeTypeDonut(fSess, fEntr, fWeekly) {
  let tugMs = fSess.filter(s => s.type === 'tugging').reduce((a, s) => a + s.durationMs, 0);
  let retMs = fSess.filter(s => s.type === 'retaining').reduce((a, s) => a + s.durationMs, 0);
  const historical = !fSess.length;
  if (historical) {
    if (fWeekly && fWeekly.length) {
      tugMs = fWeekly.reduce((a, w) => a + (w.totalHrsTugged   || 0) * 3600000, 0);
      retMs = fWeekly.reduce((a, w) => a + (w.totalHrsRetained || 0) * 3600000, 0);
    } else if (fEntr && fEntr.length) {
      tugMs = fEntr.reduce((a, e) => a + (e.avgHrsTugged   || 0) * 7 * 3600000, 0);
      retMs = fEntr.reduce((a, e) => a + (e.avgHrsRetained || 0) * 7 * 3600000, 0);
    }
  }
  const total = tugMs + retMs;
  const tugPct = total ? Math.round(tugMs / total * 100) : 0;
  const retPct = total ? 100 - tugPct : 0;
  const slices = [
    { value: tugMs, color: '#22c55e', label: `Tugging (${tugPct}% · ${_dFmt(tugMs)})` },
    { value: retMs, color: '#8b5cf6', label: `Retaining (${retPct}% · ${_dFmt(retMs)})` },
  ];
  const paths = _dDonutPaths(slices, 65, 65, 52, 33);
  const legend = [
    { color: '#22c55e', label: 'Tugging',   pct: tugPct, ms: tugMs },
    { color: '#8b5cf6', label: 'Retaining', pct: retPct, ms: retMs },
  ].map(r => `<div style="display:flex;align-items:center;gap:6px;margin:5px 0">
    <span class="data-ldot" style="background:${r.color}"></span>
    <span style="font-size:12px;color:var(--text)">${r.label}</span>
    <span style="margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--muted)">${r.pct}% · ${_dFmt(r.ms)}</span>
  </div>`).join('');
  return `<div class="data-card">
    <div class="data-card-title">Time Split</div>
    <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
      <svg viewBox="0 0 130 130" width="110" height="110" style="flex-shrink:0">
        ${total ? paths : `<circle cx="65" cy="65" r="52" fill="var(--surface3,#252e3d)"/>`}
        <text x="65" y="61" text-anchor="middle" font-size="17" font-weight="600" fill="var(--text)">${tugPct}%</text>
        <text x="65" y="75" text-anchor="middle" font-size="9" fill="var(--muted2)">tugging</text>
      </svg>
      <div style="flex:1;min-width:110px">${legend}</div>
    </div>
  </div>`;
}

// ── Donut: Days by activity type ──────────────────────────────────────────────

function _dBuildDayTypeDonut(fSess, start, end, fEntr, fWeekly) {
  const dayMap = {};
  for (const s of fSess) {
    const day = new Date(s.startedAt).toISOString().slice(0, 10);
    if (!dayMap[day]) dayMap[day] = { tug: false, ret: false };
    if (s.type === 'tugging') dayMap[day].tug = true; else dayMap[day].ret = true;
  }
  // If no session data, use weekly stats days
  if (!fSess.length && fWeekly && fWeekly.length) {
    for (const w of fWeekly) {
      if (w.daysTugged)   for (let d = 0; d < w.daysTugged;   d++) { const k = `${w.date}-tug${d}`;   dayMap[k] = { tug: true,  ret: false }; }
      if (w.daysRetained) for (let d = 0; d < w.daysRetained; d++) { const k = `${w.date}-ret${d}`;   dayMap[k] = { tug: false, ret: true  }; }
    }
  } else if (!fSess.length && fEntr && fEntr.length) {
    for (const e of fEntr) {
      if (e.avgHrsTugged   > 0) dayMap[e.date + '-t'] = { tug: true,  ret: false };
      if (e.avgHrsRetained > 0) dayMap[e.date + '-r'] = { tug: false, ret: true  };
    }
  }
  let tugOnly = 0, retOnly = 0, both = 0;
  for (const v of Object.values(dayMap)) {
    if (v.tug && v.ret) both++;
    else if (v.tug) tugOnly++;
    else retOnly++;
  }
  const spanDays   = Math.round((end - start) / 86400000) + 1;
  const active     = tugOnly + retOnly + both;
  const inactive   = Math.max(spanDays - active, 0);
  const activePct  = spanDays ? Math.round(active / spanDays * 100) : 0;
  const slices = [
    { value: tugOnly,  color: '#22c55e',               label: `Tug only (${tugOnly}d)` },
    { value: both,     color: '#14b8a6',               label: `Both (${both}d)` },
    { value: retOnly,  color: '#8b5cf6',               label: `Ret only (${retOnly}d)` },
    { value: inactive, color: 'rgba(255,255,255,.07)', label: `No session (${inactive}d)` },
  ];
  const paths = _dDonutPaths(slices, 65, 65, 52, 33);
  const legend = slices.filter(s => s.value > 0).map(s =>
    `<div style="display:flex;align-items:center;gap:6px;margin:5px 0">
      <span class="data-ldot" style="background:${s.color}"></span>
      <span style="font-size:12px;color:var(--text)">${s.label.split(' (')[0]}</span>
      <span style="margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--muted)">${s.value}d</span>
    </div>`
  ).join('');
  return `<div class="data-card">
    <div class="data-card-title">Activity by Day</div>
    <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
      <svg viewBox="0 0 130 130" width="110" height="110" style="flex-shrink:0">
        ${paths || `<circle cx="65" cy="65" r="52" fill="var(--surface3,#252e3d)"/>`}
        <text x="65" y="61" text-anchor="middle" font-size="17" font-weight="600" fill="var(--text)">${activePct}%</text>
        <text x="65" y="75" text-anchor="middle" font-size="9" fill="var(--muted2)">active</text>
      </svg>
      <div style="flex:1;min-width:110px">${legend}</div>
    </div>
  </div>`;
}

// ── Session length histogram ──────────────────────────────────────────────────

function _dBuildSessionLengthHist(fSess) {
  const buckets = [
    { label: '<15m',   min: 0,        max: 900000   },
    { label: '15–30m', min: 900000,   max: 1800000  },
    { label: '30–60m', min: 1800000,  max: 3600000  },
    { label: '1–2h',   min: 3600000,  max: 7200000  },
    { label: '2–4h',   min: 7200000,  max: 14400000 },
    { label: '4h+',    min: 14400000, max: Infinity  },
  ];
  const counts = buckets.map(b => ({ ...b, count: fSess.filter(s => s.durationMs >= b.min && s.durationMs < b.max).length }));
  const maxCount = Math.max(...counts.map(c => c.count), 1);
  const W = 280, H = 110, PAD = { l: 20, r: 6, t: 6, b: 26 };
  const bW  = (W - PAD.l - PAD.r) / counts.length;
  const cH  = H - PAD.t - PAD.b;
  const barW = bW * 0.65, barX = (bW - bW * 0.65) / 2;

  const grid = [0, 0.5, 1].map(f =>
    `<line x1="${PAD.l}" y1="${PAD.t + cH * (1-f)}" x2="${W - PAD.r}" y2="${PAD.t + cH * (1-f)}" stroke="var(--border2)" stroke-width="0.5"/>`
  ).join('');

  const bars = counts.map((b, i) => {
    const x  = PAD.l + i * bW + barX;
    const bH = (b.count / maxCount) * cH;
    const y  = PAD.t + cH - bH;
    return `<g><title>${b.label}: ${b.count} sessions</title>
      <rect x="${x}" y="${y}" width="${barW}" height="${bH || 0}" fill="#3b82f6" opacity="0.85" rx="2"/>
      <text x="${PAD.l + i * bW + bW/2}" y="${H - 10}" font-size="7.5" fill="var(--muted)" text-anchor="middle">${b.label}</text>
      ${b.count ? `<text x="${PAD.l + i * bW + bW/2}" y="${y - 2}" font-size="7" fill="var(--muted2)" text-anchor="middle">${b.count}</text>` : ''}
    </g>`;
  }).join('');

  return `<div class="data-card">
    <div class="data-card-title">Session Length</div>
    <svg viewBox="0 0 ${W} ${H}" style="width:100%;display:block">${grid}${bars}</svg>
  </div>`;
}

// ── Time of day histogram ─────────────────────────────────────────────────────

function _dBuildTimeOfDayHist(fSess) {
  const labels = ['12a','3a','6a','9a','12p','3p','6p','9p'];
  const counts = new Array(8).fill(0);
  for (const s of fSess) counts[Math.floor(new Date(s.startedAt).getHours() / 3)]++;
  const maxCount = Math.max(...counts, 1);
  const W = 280, H = 110, PAD = { l: 20, r: 6, t: 6, b: 26 };
  const bW  = (W - PAD.l - PAD.r) / 8;
  const cH  = H - PAD.t - PAD.b;
  const barW = bW * 0.65, barX = (bW - bW * 0.65) / 2;

  const grid = [0, 0.5, 1].map(f =>
    `<line x1="${PAD.l}" y1="${PAD.t + cH * (1-f)}" x2="${W - PAD.r}" y2="${PAD.t + cH * (1-f)}" stroke="var(--border2)" stroke-width="0.5"/>`
  ).join('');

  const bars = counts.map((count, i) => {
    const x  = PAD.l + i * bW + barX;
    const bH = (count / maxCount) * cH;
    const y  = PAD.t + cH - bH;
    return `<g><title>${labels[i]}: ${count} sessions</title>
      <rect x="${x}" y="${y}" width="${barW}" height="${bH || 0}" fill="#f59e0b" opacity="0.85" rx="2"/>
      <text x="${PAD.l + i * bW + bW/2}" y="${H - 10}" font-size="7.5" fill="var(--muted)" text-anchor="middle">${labels[i]}</text>
      ${count ? `<text x="${PAD.l + i * bW + bW/2}" y="${y - 2}" font-size="7" fill="var(--muted2)" text-anchor="middle">${count}</text>` : ''}
    </g>`;
  }).join('');

  return `<div class="data-card">
    <div class="data-card-title">Time of Day</div>
    <svg viewBox="0 0 ${W} ${H}" style="width:100%;display:block">${grid}${bars}</svg>
  </div>`;
}

// ── Device time pie chart (from timer sessions) ──────────────────────────────

function _dBuildDevicePie(devSess, type) {
  const label = type === 'tugging' ? 'Tugging' : 'Retaining';
  const filtered = devSess.filter(s => s.type === type);
  if (!filtered.length) return `<div class="data-card"><div class="data-card-title">${label} by Device</div><p style="font-size:12px;color:var(--muted2);margin:12px 0">No ${label.toLowerCase()} sessions with a device selected.</p></div>`;

  // Aggregate hours per device
  const devMs = {};
  for (const s of filtered) {
    devMs[s.device] = (devMs[s.device] || 0) + s.durationMs;
  }
  const sorted = Object.entries(devMs).sort((a, b) => b[1] - a[1]);
  const totalMs = sorted.reduce((a, [, ms]) => a + ms, 0) || 1;

  // SVG donut
  const CX = 70, CY = 70, R = 55, R2 = 32;
  const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
  let angle = 0;
  const slices = sorted.map(([name, ms], i) => {
    const frac = ms / totalMs;
    const startAngle = angle;
    angle += frac * 2 * Math.PI;
    const endAngle = angle;
    const large = frac > 0.5 ? 1 : 0;
    const x1 = CX + R * Math.cos(startAngle - Math.PI / 2);
    const y1 = CY + R * Math.sin(startAngle - Math.PI / 2);
    const x2 = CX + R * Math.cos(endAngle - Math.PI / 2);
    const y2 = CY + R * Math.sin(endAngle - Math.PI / 2);
    const ix1 = CX + R2 * Math.cos(endAngle - Math.PI / 2);
    const iy1 = CY + R2 * Math.sin(endAngle - Math.PI / 2);
    const ix2 = CX + R2 * Math.cos(startAngle - Math.PI / 2);
    const iy2 = CY + R2 * Math.sin(startAngle - Math.PI / 2);
    const col = colors[i % colors.length];
    const hrs = (ms / 3600000).toFixed(1);
    const pct = (frac * 100).toFixed(0);
    if (frac < 0.003) return ''; // skip tiny slices
    return `<path d="M${x1},${y1} A${R},${R} 0 ${large} 1 ${x2},${y2} L${ix1},${iy1} A${R2},${R2} 0 ${large} 0 ${ix2},${iy2} Z" fill="${col}" opacity="0.85"><title>${esc(name)}: ${hrs}h (${pct}%)</title></path>`;
  }).join('');

  // Center text
  const totalHrs = (totalMs / 3600000).toFixed(1);
  const centerText = `<text x="${CX}" y="${CY - 4}" text-anchor="middle" font-size="13" font-weight="700" fill="var(--text)">${totalHrs}h</text>
    <text x="${CX}" y="${CY + 10}" text-anchor="middle" font-size="8" fill="var(--muted2)">total</text>`;

  // Legend
  const legend = sorted.map(([name, ms], i) => {
    const hrs = (ms / 3600000).toFixed(1);
    const pct = (ms / totalMs * 100).toFixed(0);
    const col = colors[i % colors.length];
    return `<div style="display:flex;align-items:center;gap:6px;font-size:11px;margin:3px 0">
      <span style="width:10px;height:10px;border-radius:50%;background:${col};flex-shrink:0;opacity:.85"></span>
      <span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)">${esc(name)}</span>
      <span style="font-family:var(--mono);color:var(--muted2);flex-shrink:0">${hrs}h · ${pct}%</span>
    </div>`;
  }).join('');

  return `<div class="data-card">
    <div class="data-card-title">${label} by Device</div>
    <div style="display:flex;gap:12px;align-items:center">
      <svg viewBox="0 0 140 140" style="width:120px;height:120px;flex-shrink:0">${slices}${centerText}</svg>
      <div style="flex:1;min-width:0">${legend}</div>
    </div>
  </div>`;
}

// ── Device usage ──────────────────────────────────────────────────────────────

function _dBuildDeviceChart(fEntr) {
  const counts = {};
  for (const e of fEntr) {
    for (const d of (e.tuggingDevices   || [])) if (d && d !== 'Other') counts[d] = (counts[d] || 0) + 1;
    for (const d of (e.retainingDevices || [])) if (d && d !== 'Other') counts[d] = (counts[d] || 0) + 1;
  }
  const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
  if (!sorted.length) return '';
  const maxCount = sorted[0][1];
  const rows = sorted.map(([name, count]) =>
    `<div style="margin:6px 0">
      <div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:3px">
        <span style="color:var(--text)">${esc(name)}</span>
        <span style="color:var(--muted);font-family:var(--mono)">${count}×</span>
      </div>
      <div style="height:7px;background:var(--surface3,#252e3d);border-radius:4px">
        <div style="height:7px;background:#3b82f6;border-radius:4px;width:${(count / maxCount * 100).toFixed(1)}%"></div>
      </div>
    </div>`
  ).join('');
  return `<div class="data-card">
    <div class="data-card-title">Devices Used</div>
    ${rows}
  </div>`;
}

// ── CI progression ────────────────────────────────────────────────────────────

function _dBuildCIChart(fEntr) {
  const pts = [...fEntr]
    .filter(e => e.ci != null)
    .sort((a, b) => a.date.localeCompare(b.date));
  if (pts.length < 2) return '';

  const W = 500, H = 110, PAD = { l: 28, r: 44, t: 10, b: 24 };
  const cW = W - PAD.l - PAD.r, cH = H - PAD.t - PAD.b;
  const dates  = pts.map(e => new Date(e.date + 'T12:00:00').getTime());
  const minT   = dates[0], maxT = dates[dates.length - 1], tRange = maxT - minT || 1;
  const xOf = t  => PAD.l + ((t - minT) / tRange) * cW;
  const yOf = ci => PAD.t + cH - (ci / 10) * cH;

  const grid = [0, 2, 4, 6, 8, 10].map(ci => {
    const y = yOf(ci);
    return `<line x1="${PAD.l}" y1="${y}" x2="${W - PAD.r}" y2="${y}" stroke="var(--border2)" stroke-width="0.5"/>
      <text x="${PAD.l - 3}" y="${y + 3}" font-size="7" fill="var(--muted2)" text-anchor="end">${ci}</text>`;
  }).join('');

  const area     = `<polygon points="${PAD.l},${PAD.t + cH} ${pts.map((e,i) => `${xOf(dates[i])},${yOf(e.ci)}`).join(' ')} ${xOf(dates[dates.length-1])},${PAD.t + cH}" fill="#3b82f6" opacity="0.08"/>`;
  const polyline = `<polyline points="${pts.map((e,i) => `${xOf(dates[i])},${yOf(e.ci)}`).join(' ')}" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>`;
  const dots     = pts.map((e,i) => `<circle cx="${xOf(dates[i])}" cy="${yOf(e.ci)}" r="2.5" fill="#3b82f6"><title>${e.date}: CI-${e.ci}</title></circle>`).join('');

  // Up to 5 x-axis labels
  const step = Math.max(1, Math.floor(pts.length / 4));
  const xLabels = pts
    .filter((_, i) => i === 0 || i === pts.length - 1 || (i % step === 0))
    .map(e => `<text x="${xOf(new Date(e.date+'T12:00:00').getTime())}" y="${H - 4}" font-size="7.5" fill="var(--muted2)" text-anchor="middle">${e.date.slice(5)}</text>`)
    .join('');

  const last = pts[pts.length - 1];
  return `<div class="data-card">
    <div class="data-card-title">CI Progression</div>
    <svg viewBox="0 0 ${W} ${H}" style="width:100%;display:block">
      ${grid}${area}${polyline}${dots}${xLabels}
      <text x="${xOf(dates[dates.length-1]) + 5}" y="${yOf(last.ci) + 4}" font-size="11" font-weight="600" fill="#3b82f6">CI-${last.ci}</text>
    </svg>
  </div>`;
}

// ── Quick Check records ──────────────────────────────────────────────────────

function _dBuildQuickCheckRecords() {
  const qcs = typeof getQuickChecks === 'function' ? getQuickChecks() : [];
  const withHistory = qcs.filter(q => (q.history || []).length > 0 || q.spree > 0);
  if (!withHistory.length) return '';
  const contacts = typeof getContacts === 'function' ? getContacts() : [];
  const rows = withHistory.map(qc => {
    const c = contacts.find(x => x.pubKeyB64 === qc.buddyPubKey);
    const name = c?.journal?.displayName || c?.nickname || qc.buddyPubKey?.slice(0, 8) || '?';
    const avatar = c?.journal?.avatar || '📋';
    const totalRounds = (qc.history || []).length + (qc.mySubmittedAt ? 1 : 0);
    const successes = (qc.history || []).filter(h => h.success).length + (qc.revealed ? 1 : 0);
    const rate = totalRounds > 0 ? Math.round(successes / totalRounds * 100) : 0;
    const cadLabel = typeof _QC_CADENCES !== 'undefined' ? (_QC_CADENCES[qc.cadence]?.label || qc.cadence) : qc.cadence;
    return `<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border2)">
      <span style="font-size:18px">${esc(avatar)}</span>
      <div style="flex:1;min-width:0">
        <div style="font-size:12px;font-weight:600;color:var(--text)">${esc(name)} · ${esc(qc.description)}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${cadLabel} · ${totalRounds} round${totalRounds !== 1 ? 's' : ''} · ${rate}% success</div>
      </div>
      <div style="text-align:right">
        <div style="font-size:14px;font-weight:700;color:var(--text)">🔥 ${qc.spree || 0}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">best: ${qc.bestSpree || 0}</div>
      </div>
    </div>`;
  }).join('');
  return `<div class="data-card">
    <div class="data-card-title">Quick Check Records</div>
    ${rows}
  </div>`;
}
