(function () {
  const COLORS = ['#3d8bfd', '#5eead4', '#f59e0b', '#34d399', '#a78bfa', '#f472b6', '#94a3b8'];

  const BRICK_DEFS = {
    sessions: { type: 'line', field: 'sessions_per_day', title: 'Sessions / day', color: COLORS[0] },
    devices: { type: 'line', field: 'unique_devices_per_day', title: 'Unique devices / day', color: COLORS[1] },
    duration: { type: 'line', field: 'avg_duration_s_per_day', title: 'Avg duration (s) / day', color: COLORS[2] },
    send: { type: 'line', field: 'send_sessions_per_day', title: 'Send sessions / day', color: COLORS[3] },
    recv: { type: 'line', field: 'recv_sessions_per_day', title: 'Receive sessions / day', color: COLORS[4] },
    bitrate: { type: 'line', field: 'avg_bitrate_kbps_per_day', title: 'Outbound kbps / day', color: COLORS[0] },
    'recv-kbps': { type: 'line', field: 'avg_recv_kbps_per_day', title: 'Inbound kbps / day', color: COLORS[1] },
    crashes: { type: 'line', field: 'crashes_per_day', title: 'Issues / day', color: '#f87171' },
    tickets: { type: 'line', field: 'tickets_per_day', title: 'Tickets / day', color: '#c084fc' },
    'install-pie': { type: 'pie', field: 'install_sources', title: 'Install sources' },
    'ntp-pie': { type: 'pie', field: 'ntp_sources', title: 'NTP sources' },
    transport: { type: 'breakdown', field: 'transport_mix', title: 'Transport mix' },
    versions: { type: 'breakdown', field: 'app_versions', title: 'App versions' },
    success: { type: 'kpi', field: 'success_ratio_pct', title: 'Success ratio', suffix: '%' },
    'ntp-avg': { type: 'kpi', field: 'ntp_correction_avg_s', title: 'NTP correction', suffix: ' s avg correction' },
    fingerprints: { type: 'toplist', field: 'top_crash_fingerprints', title: 'Top issue fingerprints' },
    'crash-versions': { type: 'pie', field: 'crashes_by_app_version', title: 'Issues by app version' },
  };

  let graphScopes = { user: null, slug: null, platform: null };
  let detailState = null;

  function basePath() {
    return document.body.getAttribute('data-base-path') || '';
  }

  function el(id) {
    return document.getElementById(id);
  }

  function escapeHtml(s) {
    return s
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function graphPermalink(scopeKey, suffix, days) {
    const url = new URL(window.location.href);
    url.searchParams.set('graph', scopeKey + '-' + suffix);
    if (days) {
      url.searchParams.set('days', String(days));
    }
    return url.toString();
  }

  function syncGraphPermalink(scopeKey, suffix) {
    const daysSel = el('graphs-days');
    const days = daysSel && daysSel.value ? daysSel.value : '14';
    const link = graphPermalink(scopeKey, suffix, days);
    if (window.history && window.history.replaceState) {
      window.history.replaceState(null, '', link);
    }
    const share = el('graph-detail-share');
    if (share) {
      share.href = link;
    }
  }

  function clearGraphPermalink() {
    const url = new URL(window.location.href);
    url.searchParams.delete('graph');
    if (window.history && window.history.replaceState) {
      window.history.replaceState(null, '', url.toString());
    }
  }

  function openGraphFromPermalink() {
    const params = new URLSearchParams(window.location.search);
    const token = params.get('graph');
    if (!token) return;
    const node = document.getElementById('graph-' + token);
    if (!node) return;
    const card = node.closest('.card.card--lift');
    if (card) {
      openGraphDetail(card);
    }
  }

  function parseCanvasId(id) {
    const m = String(id || '').match(/^graph-(user|slug|platform)-(.+)$/);
    if (!m) return null;
    const scopeKey = m[1] === 'user' ? 'user' : m[1] === 'slug' ? 'slug' : 'platform';
    return { scopeKey, suffix: m[2], def: BRICK_DEFS[m[2]] || null };
  }

  function scopeData(scopeKey) {
    if (scopeKey === 'user') return graphScopes.user;
    if (scopeKey === 'slug') return graphScopes.slug;
    return graphScopes.platform;
  }

  const BRICK_ORDER_KEY = 'graphs_brick_order_v1';

  function prepareCanvas(canvas, fallbackW, fallbackH) {
    const rect = canvas.getBoundingClientRect();
    const w = Math.max(280, Math.round(rect.width || fallbackW || canvas.width || 520));
    const h = Math.max(120, Math.round(rect.height || fallbackH || canvas.height || 160));
    const dpr = Math.min(2.5, window.devicePixelRatio || 1);
    canvas.width = Math.round(w * dpr);
    canvas.height = Math.round(h * dpr);
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';
    const ctx = canvas.getContext('2d');
    if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    return { w, h, dpr };
  }

  function drawLineChart(canvas, series, color, opts) {
    if (!canvas) return null;
    const interactive = opts && opts.interactive;
    const sized = prepareCanvas(canvas, 520, interactive ? 380 : 160);
    const w = sized.w;
    const h = sized.h;
    const ctx = canvas.getContext('2d');
    if (!ctx) return null;
    ctx.clearRect(0, 0, w, h);
    ctx.fillStyle = 'rgba(90,110,140,0.12)';
    ctx.fillRect(0, 0, w, h);
    const points = Array.isArray(series) ? series : [];
    if (!points.length) {
      ctx.fillStyle = '#94a3b8';
      ctx.font = '12px system-ui, sans-serif';
      ctx.fillText('No data', 24, h / 2);
      return { points: [], padL: 36, padT: 12, plotW: w - 48, plotH: h - 40, maxY: 1 };
    }
    let maxY = 0;
    points.forEach((p) => {
      const y = Number(p.y || 0);
      if (y > maxY) maxY = y;
    });
    if (maxY <= 0) maxY = 1;
    const padL = interactive ? 56 : 36;
    const padR = interactive ? 148 : 12;
    const padT = interactive ? 24 : 16;
    const padB = interactive ? 40 : 32;
    const plotW = w - padL - padR;
    const plotH = h - padT - padB;
    const lineColor = color || '#3d8bfd';
    const title = (opts && opts.title) || '';

    ctx.strokeStyle = 'rgba(120,140,170,0.45)';
    ctx.lineWidth = 1;
    ctx.fillStyle = '#94a3b8';
    ctx.font = interactive ? '11px system-ui, sans-serif' : '10px system-ui, sans-serif';
    for (let i = 0; i <= 4; i++) {
      const y = padT + (plotH * i) / 4;
      ctx.beginPath();
      ctx.moveTo(padL, y);
      ctx.lineTo(w - padR, y);
      ctx.stroke();
      if (interactive) {
        const tickVal = Math.round((maxY * (4 - i)) / 4);
        ctx.fillText(String(tickVal), 8, y + 4);
      }
    }

    const plotPoints = points.map((p, idx) => {
      const x = padL + (plotW * idx) / Math.max(1, points.length - 1);
      const y = padT + plotH - (plotH * Number(p.y || 0)) / maxY;
      return { x, y, raw: p, idx };
    });

    ctx.strokeStyle = lineColor;
    ctx.lineWidth = interactive ? 2.5 : 2;
    ctx.beginPath();
    plotPoints.forEach((pt, idx) => {
      if (idx === 0) ctx.moveTo(pt.x, pt.y);
      else ctx.lineTo(pt.x, pt.y);
    });
    ctx.stroke();

    if (interactive) {
      plotPoints.forEach((pt) => {
        ctx.beginPath();
        ctx.fillStyle = lineColor;
        ctx.arc(pt.x, pt.y, 5, 0, Math.PI * 2);
        ctx.fill();
      });
      if (title) {
        const lx = w - padR + 12;
        ctx.fillStyle = lineColor;
        ctx.fillRect(lx, padT, 12, 12);
        ctx.fillStyle = '#e2e8f0';
        ctx.font = '12px system-ui, sans-serif';
        const legend = title.length > 18 ? title.slice(0, 17) + '…' : title;
        ctx.fillText(legend, lx + 18, padT + 10);
        ctx.fillStyle = '#94a3b8';
        ctx.font = '10px system-ui, sans-serif';
        ctx.fillText('Y: value · X: date', lx, padT + 28);
      }
      const xTicks = Math.min(6, points.length);
      for (let t = 0; t < xTicks; t++) {
        const idx = xTicks === 1 ? 0 : Math.round((t * (points.length - 1)) / (xTicks - 1));
        const pt = plotPoints[idx];
        const label = String(points[idx].x || '');
        ctx.fillStyle = '#94a3b8';
        ctx.font = '10px system-ui, sans-serif';
        ctx.fillText(label, Math.max(padL, pt.x - 20), h - 12);
      }
    } else {
      ctx.fillStyle = '#94a3b8';
      ctx.font = '10px system-ui, sans-serif';
      if (points.length) {
        ctx.fillText(String(points[0].x || ''), padL, h - 8);
        const last = points[points.length - 1];
        ctx.fillText(String(last.x || ''), padL + plotW - 48, h - 8);
      }
    }

    const highlightIdx = opts && opts.highlightIdx;
    if (highlightIdx != null && plotPoints[highlightIdx]) {
      const hi = plotPoints[highlightIdx];
      ctx.beginPath();
      ctx.fillStyle = '#f8fafc';
      ctx.strokeStyle = lineColor;
      ctx.lineWidth = 2;
      ctx.arc(hi.x, hi.y, 7, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    }

    return {
      points: plotPoints,
      padL,
      padT,
      padR,
      plotW,
      plotH,
      maxY,
      w,
      h,
      series: points,
      color: lineColor,
      title,
    };
  }

  function detailCanvasSize() {
    const wrap = el('graph-detail-canvas-wrap');
    const maxW = wrap ? wrap.clientWidth - 8 : window.innerWidth - 48;
    return {
      w: Math.max(640, Math.min(1280, maxW)),
      h: Math.max(380, window.innerHeight - 200),
    };
  }

  function redrawDetailChart(highlightIdx) {
    const state = detailState;
    if (!state || state.type !== 'line' || !state.canvas) return;
    const canvas = state.canvas;
    const meta = drawLineChart(canvas, state.series, state.def.color, {
      interactive: true,
      title: state.def.title,
    });
    if (meta && highlightIdx != null && meta.points[highlightIdx]) {
      const ctx = canvas.getContext('2d');
      const pt = meta.points[highlightIdx];
      ctx.strokeStyle = 'rgba(148,163,184,0.55)';
      ctx.lineWidth = 1;
      ctx.setLineDash([4, 4]);
      ctx.beginPath();
      ctx.moveTo(pt.x, meta.padT);
      ctx.lineTo(pt.x, meta.h - meta.padT);
      ctx.stroke();
      ctx.setLineDash([]);
      ctx.beginPath();
      ctx.fillStyle = state.def.color || '#3d8bfd';
      ctx.strokeStyle = '#fff';
      ctx.lineWidth = 2;
      ctx.arc(pt.x, pt.y, 7, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
    }
    detailState.meta = meta;
  }

  function drawPie(canvas, rows, opts) {
    if (!canvas) return null;
    const interactive = opts && opts.interactive;
    const highlightIdx = opts && opts.highlightIdx;
    const sized = prepareCanvas(canvas, interactive ? 640 : 520, interactive ? 380 : 160);
    const w = sized.w;
    const h = sized.h;
    const ctx = canvas.getContext('2d');
    if (!ctx) return null;
    ctx.clearRect(0, 0, w, h);
    if (interactive) {
      ctx.fillStyle = 'rgba(90,110,140,0.12)';
      ctx.fillRect(0, 0, w, h);
    }
    const data = Array.isArray(rows) ? rows.filter((r) => Number(r.value) > 0) : [];
    if (!data.length) {
      ctx.fillStyle = '#94a3b8';
      ctx.font = '12px system-ui, sans-serif';
      ctx.fillText('No data', 24, h / 2);
      return { slices: [], cx: 0, cy: 0, r: 0, data: [], total: 0, w, h };
    }
    const total = data.reduce((s, r) => s + Number(r.value || 0), 0);
    const cx = w * (interactive ? 0.38 : 0.35);
    const cy = h * 0.5;
    const r = Math.min(w, h) * (interactive ? 0.36 : 0.32);
    let angle = -Math.PI / 2;
    const slices = [];
    data.forEach((row, i) => {
      const slice = (Number(row.value) / total) * Math.PI * 2;
      const start = angle;
      const end = angle + slice;
      slices.push({ start, end, idx: i, row, color: COLORS[i % COLORS.length] });
      ctx.beginPath();
      ctx.moveTo(cx, cy);
      ctx.fillStyle = COLORS[i % COLORS.length];
      if (interactive && highlightIdx === i) {
        ctx.shadowColor = 'rgba(0,0,0,0.35)';
        ctx.shadowBlur = 10;
      } else {
        ctx.shadowBlur = 0;
      }
      ctx.arc(cx, cy, r, start, end);
      ctx.closePath();
      ctx.fill();
      ctx.shadowBlur = 0;
      angle = end;
    });
    const legendX = w * (interactive ? 0.58 : 0.62);
    let ly = interactive ? 28 : 16;
    data.forEach((row, i) => {
      const color = COLORS[i % COLORS.length];
      ctx.fillStyle = color;
      ctx.fillRect(legendX, ly, interactive ? 14 : 10, interactive ? 14 : 10);
      ctx.fillStyle = highlightIdx === i && interactive ? '#f8fafc' : '#cbd5e1';
      ctx.font = interactive ? '13px system-ui, sans-serif' : '11px system-ui, sans-serif';
      const pct = total > 0 ? Math.round((100 * Number(row.value)) / total) : 0;
      const label = String(row.label || '') + ' (' + pct + '%)';
      ctx.fillText(label, legendX + (interactive ? 22 : 16), ly + (interactive ? 12 : 9));
      ly += interactive ? 22 : 18;
    });
    return { slices, cx, cy, r, data, total, w, h };
  }

  function redrawDetailPie(highlightIdx) {
    const state = detailState;
    if (!state || state.type !== 'pie' || !state.canvas) return;
    const meta = drawPie(state.canvas, state.series, { interactive: true, highlightIdx: highlightIdx });
    detailState.meta = meta;
  }

  function pieSliceAt(meta, mx, my) {
    if (!meta || !meta.slices.length || meta.total <= 0) return -1;
    const dx = mx - meta.cx;
    const dy = my - meta.cy;
    if (Math.sqrt(dx * dx + dy * dy) > meta.r) return -1;
    let rel = Math.atan2(dy, dx) + Math.PI / 2;
    if (rel < 0) rel += Math.PI * 2;
    if (rel >= Math.PI * 2) rel -= Math.PI * 2;
    let acc = 0;
    for (let i = 0; i < meta.data.length; i++) {
      const slice = (Number(meta.data[i].value) / meta.total) * Math.PI * 2;
      if (rel >= acc && rel < acc + slice) return i;
      acc += slice;
    }
    return -1;
  }

  function renderBreakdownList(target, rows) {
    if (!target) return;
    if (!Array.isArray(rows) || !rows.length) {
      target.textContent = 'No data';
      return;
    }
    const total = rows.reduce((s, r) => s + Number(r.value || 0), 0) || 1;
    target.innerHTML = rows
      .map((r) => {
        const pct = Math.round((100 * Number(r.value || 0)) / total);
        return (
          '<div class="graph-breakdown-row"><span>' +
          escapeHtml(String(r.label || '')) +
          '</span><strong>' +
          Number(r.value || 0) +
          ' <span class="muted">(' +
          pct +
          '%)</span></strong></div>'
        );
      })
      .join('');
  }

  function renderTopList(target, rows) {
    if (!target) return;
    if (!Array.isArray(rows) || !rows.length) {
      target.textContent = 'No data';
      return;
    }
    target.innerHTML = rows
      .map((r) => {
        const label = escapeHtml(String(r.label || ''));
        const val = Number(r.value || 0);
        if (r.href) {
          return (
            '<div class="graph-breakdown-row"><a href="' +
            escapeHtml(r.href) +
            '">' +
            label +
            '</a><strong>' +
            val +
            '</strong></div>'
          );
        }
        return '<div class="graph-breakdown-row"><span>' + label + '</span><strong>' + val + '</strong></div>';
      })
      .join('');
  }

  function renderScope(prefix, scope) {
    if (!scope) return;
    drawLineChart(el('graph-' + prefix + '-sessions'), scope.sessions_per_day, COLORS[0]);
    drawLineChart(el('graph-' + prefix + '-devices'), scope.unique_devices_per_day, COLORS[1]);
    drawLineChart(el('graph-' + prefix + '-duration'), scope.avg_duration_s_per_day, COLORS[2]);
    drawLineChart(el('graph-' + prefix + '-send'), scope.send_sessions_per_day, COLORS[3]);
    drawLineChart(el('graph-' + prefix + '-recv'), scope.recv_sessions_per_day, COLORS[4]);
    drawLineChart(el('graph-' + prefix + '-bitrate'), scope.avg_bitrate_kbps_per_day, COLORS[0]);
    drawLineChart(el('graph-' + prefix + '-recv-kbps'), scope.avg_recv_kbps_per_day, COLORS[1]);
    drawLineChart(el('graph-' + prefix + '-crashes'), scope.crashes_per_day, '#f87171');
    if (scope.tickets_per_day) {
      drawLineChart(el('graph-' + prefix + '-tickets'), scope.tickets_per_day, '#c084fc');
    }
    drawPie(el('graph-' + prefix + '-install-pie'), scope.install_sources);
    drawPie(el('graph-' + prefix + '-ntp-pie'), scope.ntp_sources);
    renderBreakdownList(el('graph-' + prefix + '-transport'), scope.transport_mix);
    renderBreakdownList(el('graph-' + prefix + '-versions'), scope.app_versions);
    const ratioEl = el('graph-' + prefix + '-success');
    if (ratioEl) {
      ratioEl.textContent =
        typeof scope.success_ratio_pct === 'number' ? scope.success_ratio_pct + '%' : '—';
    }
    const ntpAvg = el('graph-' + prefix + '-ntp-avg');
    if (ntpAvg && scope.ntp_correction_avg_s != null) {
      ntpAvg.textContent = scope.ntp_correction_avg_s + ' s avg correction';
    }
    renderTopList(el('graph-' + prefix + '-fingerprints'), scope.top_crash_fingerprints);
    drawPie(el('graph-' + prefix + '-crash-versions'), scope.crashes_by_app_version);
  }

  function setScopeVisibility(viewer) {
    document.querySelectorAll('[data-graph-scope]').forEach((section) => {
      const need = section.getAttribute('data-graph-scope');
      let show = false;
      if (viewer === 'platform_admin') {
        show = need === 'platform_admin';
      } else if (viewer === 'slug_admin') {
        show = need === 'slug_admin' || need === 'user';
      } else {
        show = need === 'user';
      }
      section.hidden = !show;
    });
  }

  function applyColumns(cols) {
    const app = el('graphs-app');
    if (!app) return;
    const n = Math.min(4, Math.max(1, Number(cols) || 2));
    app.style.setProperty('--graphs-columns', String(n));
  }

  function markGraphBricks() {
    document.querySelectorAll('.graphs-grid .card.card--lift').forEach((card) => {
      const canvas = card.querySelector('canvas');
      const breakdown = card.querySelector('.graph-breakdown');
      const kpi = card.querySelector('.graph-kpi');
      if (!canvas && !breakdown && !kpi) return;
      card.classList.add('graph-brick--clickable');
      card.setAttribute('tabindex', '0');
      card.setAttribute('role', 'button');
      const title = card.querySelector('h3');
      if (title) card.setAttribute('aria-label', 'Open full view: ' + title.textContent);
      if (canvas && canvas.id) {
        card.setAttribute('data-brick-id', canvas.id.replace(/^graph-/, ''));
      } else if (breakdown && breakdown.id) {
        card.setAttribute('data-brick-id', breakdown.id.replace(/^graph-/, ''));
      } else if (kpi && kpi.id) {
        card.setAttribute('data-brick-id', kpi.id.replace(/^graph-/, ''));
      }
      if (title && !title.querySelector('.graph-brick-drag-handle')) {
        title.classList.add('graph-brick-head');
        const dragLabel =
          (typeof window.t === 'function' && window.t('graphs.brick_drag')) || 'Drag to reorder';
        const handle = document.createElement('button');
        handle.type = 'button';
        handle.className = 'graph-brick-drag-handle';
        handle.setAttribute('draggable', 'true');
        handle.setAttribute('aria-label', dragLabel);
        handle.title = dragLabel;
        handle.innerHTML = '<span class="graph-brick-drag-grip" aria-hidden="true">⋮⋮</span>';
        title.insertBefore(handle, title.firstChild);
      }
      if (canvas && !card.querySelector('.graph-brick-tip')) {
        const tip = document.createElement('p');
        tip.className = 'graph-brick-tip';
        tip.hidden = true;
        card.style.position = 'relative';
        card.appendChild(tip);
      }
    });
  }

  function fieldHasData(def, scope) {
    if (!def || !scope) return false;
    const field = scope[def.field];
    if (def.type === 'line') {
      return Array.isArray(field) && field.some((p) => Number(p.y || 0) > 0);
    }
    if (def.type === 'pie' || def.type === 'breakdown' || def.type === 'toplist') {
      return Array.isArray(field) && field.some((r) => Number(r.value || 0) > 0);
    }
    if (def.type === 'kpi') {
      return field != null && field !== '' && String(field) !== '—';
    }
    return false;
  }

  function hideEmptyBricks() {
    document.querySelectorAll('.graphs-grid .card.card--lift').forEach((card) => {
      const canvas = card.querySelector('canvas');
      const breakdown = card.querySelector('.graph-breakdown');
      const kpi = card.querySelector('.graph-kpi');
      let empty = true;
      const brickId = card.getAttribute('data-brick-id') || '';
      const m = brickId.match(/^(user|slug|platform)-(.+)$/);
      if (m) {
        const scopeKey = m[1] === 'user' ? 'user' : m[1] === 'slug' ? 'slug' : 'platform';
        const def = BRICK_DEFS[m[2]];
        empty = !fieldHasData(def, scopeData(scopeKey));
      } else if (canvas && canvas.id) {
        const parsed = parseCanvasId(canvas.id);
        empty = !fieldHasData(parsed && parsed.def, scopeData(parsed && parsed.scopeKey));
      } else if (breakdown) {
        empty = breakdown.textContent.trim() === 'No data' || breakdown.innerHTML.trim() === '';
      } else if (kpi) {
        empty = (kpi.textContent || '').trim() === '—';
      }
      card.classList.toggle('graph-brick--empty', empty);
    });
  }

  function sessionMetricsDeficit(scope) {
    if (!scope) return true;
    const sessionFields = [
      'sessions_per_day',
      'unique_devices_per_day',
      'avg_duration_s_per_day',
      'send_sessions_per_day',
      'recv_sessions_per_day',
      'avg_bitrate_kbps_per_day',
    ];
    return !sessionFields.some((key) => {
      const rows = scope[key];
      return Array.isArray(rows) && rows.some((p) => Number(p.y || 0) > 0);
    });
  }

  function readBrickOrder(scopeKey) {
    try {
      const all = JSON.parse(localStorage.getItem(BRICK_ORDER_KEY) || '{}');
      return Array.isArray(all[scopeKey]) ? all[scopeKey] : [];
    } catch {
      return [];
    }
  }

  function saveBrickOrder(scopeKey, ids) {
    try {
      const all = JSON.parse(localStorage.getItem(BRICK_ORDER_KEY) || '{}');
      all[scopeKey] = ids;
      localStorage.setItem(BRICK_ORDER_KEY, JSON.stringify(all));
    } catch { /* ignore */ }
  }

  function applyBrickOrder() {
    document.querySelectorAll('.graphs-scope').forEach((section) => {
      if (section.hidden) return;
      const scopeKey = section.getAttribute('data-graph-scope') || 'user';
      const grid = section.querySelector('.graphs-grid');
      if (!grid) return;
      const order = readBrickOrder(scopeKey);
      if (!order.length) return;
      const cards = Array.from(grid.querySelectorAll('.card.card--lift'));
      const byId = new Map();
      cards.forEach((c) => {
        const id = c.getAttribute('data-brick-id');
        if (id) byId.set(id, c);
      });
      order.forEach((id) => {
        const card = byId.get(id);
        if (card) grid.appendChild(card);
      });
    });
  }

  let brickDragMoved = false;

  function initBrickDragDrop() {
    let dragCard = null;
    document.querySelectorAll('.graphs-grid').forEach((grid) => {
      grid.addEventListener('dragstart', (ev) => {
        const handle = ev.target.closest('.graph-brick-drag-handle');
        if (!handle) {
          ev.preventDefault();
          return;
        }
        const card = handle.closest('.card.card--lift');
        if (!card || !grid.contains(card)) return;
        dragCard = card;
        brickDragMoved = false;
        card.classList.add('graph-brick--dragging');
        if (ev.dataTransfer) {
          ev.dataTransfer.effectAllowed = 'move';
          ev.dataTransfer.setData('text/plain', card.getAttribute('data-brick-id') || '');
        }
      });
      grid.addEventListener('dragend', () => {
        if (dragCard) dragCard.classList.remove('graph-brick--dragging');
        dragCard = null;
        const section = grid.closest('.graphs-scope');
        const scopeKey = section ? section.getAttribute('data-graph-scope') || 'user' : 'user';
        const ids = Array.from(grid.querySelectorAll('.card.card--lift'))
          .map((c) => c.getAttribute('data-brick-id'))
          .filter(Boolean);
        saveBrickOrder(scopeKey, ids);
      });
      grid.addEventListener('dragover', (ev) => {
        ev.preventDefault();
        if (!dragCard) return;
        brickDragMoved = true;
        const over = ev.target.closest('.card.card--lift');
        if (!over || over === dragCard || !grid.contains(over)) return;
        const rect = over.getBoundingClientRect();
        const before = ev.clientY < rect.top + rect.height / 2;
        grid.insertBefore(dragCard, before ? over : over.nextSibling);
      });
    });
  }

  function attachBrickPreviewHover() {
    document.querySelectorAll('.graphs-grid .card.card--lift canvas').forEach((canvas) => {
      const card = canvas.closest('.card');
      const tip = card ? card.querySelector('.graph-brick-tip') : null;
      if (!card || !tip) return;
      const parsed = parseCanvasId(canvas.id);
      if (!parsed || !parsed.def || parsed.def.type !== 'line') return;
      const scope = scopeData(parsed.scopeKey);
      if (!scope || !Array.isArray(scope[parsed.def.field])) return;
      const series = scope[parsed.def.field];
      const meta = drawLineChart(canvas, series, parsed.def.color, { interactive: false });
      if (!meta || !meta.points.length) return;
      canvas.onmousemove = function (ev) {
        const rect = canvas.getBoundingClientRect();
        const scaleX = meta.w / rect.width;
        const x = (ev.clientX - rect.left) * scaleX;
        let best = null;
        let bestD = Infinity;
        meta.points.forEach((pt) => {
          const d = Math.abs(pt.x - x);
          if (d < bestD) {
            bestD = d;
            best = pt;
          }
        });
        if (!best || bestD > 32) {
          tip.hidden = true;
          drawLineChart(canvas, series, parsed.def.color, { interactive: false });
          return;
        }
        const raw = best.raw || {};
        tip.hidden = false;
        tip.textContent = parsed.def.title + ': ' + String(raw.x || '') + ' → ' + Number(raw.y || 0);
        tip.style.left = Math.min(card.clientWidth - 12, ev.clientX - card.getBoundingClientRect().left + 10) + 'px';
        tip.style.top = Math.max(8, ev.clientY - card.getBoundingClientRect().top - 28) + 'px';
        drawLineChart(canvas, series, parsed.def.color, { interactive: false, highlightIdx: best.idx });
      };
      canvas.onmouseleave = function () {
        tip.hidden = true;
        drawLineChart(canvas, series, parsed.def.color, { interactive: false });
      };
    });
  }

  function detachDetailResize() {
    if (detailState && detailState.resizeHandler) {
      window.removeEventListener('resize', detailState.resizeHandler);
    }
  }

  function attachDetailResize(canvas, redrawFn) {
    detachDetailResize();
    const handler = function () {
      if (!detailState || detailState.canvas !== canvas) return;
      const size = detailCanvasSize();
      canvas.width = size.w;
      canvas.height = size.h;
      redrawFn(null);
    };
    if (detailState) detailState.resizeHandler = handler;
    window.addEventListener('resize', handler, { passive: true });
  }

  function closeGraphDetail() {
    detachDetailResize();
    const overlay = el('graph-detail-overlay');
    if (overlay) overlay.hidden = true;
    detailState = null;
    clearGraphPermalink();
    const tip = el('graph-detail-tooltip');
    if (tip) tip.hidden = true;
    const statsEl = el('graph-detail-stats');
    if (statsEl) statsEl.textContent = '';
  }

  function openGraphDetail(card) {
    const canvas = card.querySelector('canvas');
    const breakdown = card.querySelector('.graph-breakdown');
    const kpi = card.querySelector('.graph-kpi');
    let parsed = null;
    if (canvas && canvas.id) parsed = parseCanvasId(canvas.id);
    if (!parsed && breakdown && breakdown.id) parsed = parseCanvasId(breakdown.id);
    if (!parsed && kpi && kpi.id) parsed = parseCanvasId(kpi.id);
    if (!parsed || !parsed.def) return;

    const scope = scopeData(parsed.scopeKey);
    if (!scope) return;
    const def = parsed.def;
    syncGraphPermalink(parsed.scopeKey, parsed.suffix);
    const fieldVal = scope[def.field];
    const titleEl = el('graph-detail-title');
    const canvasEl = el('graph-detail-canvas');
    const bodyEl = el('graph-detail-body');
    const overlay = el('graph-detail-overlay');
    if (!overlay || !titleEl || !bodyEl) return;

    titleEl.textContent = def.title;
    bodyEl.innerHTML = '';
    if (canvasEl) canvasEl.hidden = true;

    if (def.type === 'line' && canvasEl) {
      canvasEl.hidden = false;
      const size = detailCanvasSize();
      canvasEl.width = size.w;
      canvasEl.height = size.h;
      detailState = {
        type: 'line',
        def,
        series: fieldVal,
        canvas: canvasEl,
        meta: null,
      };
      redrawDetailChart(null);
      attachLineHover(canvasEl, def);
      attachDetailResize(canvasEl, redrawDetailChart);
      const statsEl = el('graph-detail-stats');
      if (statsEl) {
        statsEl.textContent =
          (fieldVal && fieldVal.length ? fieldVal.length + ' points · ' : '') +
          'Move pointer over chart for values';
      }
    } else if (def.type === 'pie' && canvasEl) {
      canvasEl.hidden = false;
      const size = detailCanvasSize();
      canvasEl.width = size.w;
      canvasEl.height = size.h;
      detailState = {
        type: 'pie',
        def,
        series: fieldVal,
        canvas: canvasEl,
        meta: null,
      };
      redrawDetailPie(null);
      attachPieHover(canvasEl, def);
      attachDetailResize(canvasEl, redrawDetailPie);
      const statsEl = el('graph-detail-stats');
      if (statsEl) {
        const n = Array.isArray(fieldVal) ? fieldVal.filter((r) => Number(r.value) > 0).length : 0;
        statsEl.textContent =
          (n ? n + ' segments · ' : '') + 'Hover slices for value breakdown';
      }
    } else if (def.type === 'breakdown' || def.type === 'toplist') {
      const wrap = document.createElement('div');
      wrap.className = 'graph-detail-table graph-breakdown';
      if (!Array.isArray(fieldVal) || !fieldVal.length) {
        wrap.textContent = 'No data';
      } else if (def.type === 'toplist') {
        renderTopList(wrap, fieldVal);
      } else {
        renderBreakdownList(wrap, fieldVal);
      }
      bodyEl.appendChild(wrap);
      detailState = { type: def.type };
    } else if (def.type === 'kpi') {
      const p = document.createElement('p');
      p.className = 'graph-kpi graph-detail-kpi';
      let text = '—';
      if (typeof fieldVal === 'number') {
        text = String(fieldVal) + (def.suffix || '');
      } else if (fieldVal != null) {
        text = String(fieldVal);
      }
      p.textContent = text;
      bodyEl.appendChild(p);
      detailState = { type: 'kpi' };
    }

    overlay.hidden = false;
  }

  function valueUnit(def) {
    if (!def || !def.title) return '';
    if (def.title.indexOf('kbps') >= 0) return ' kbps';
    if (def.title.indexOf('duration') >= 0) return ' s';
    return '';
  }

  function attachLineHover(canvas, def) {
    const tip = el('graph-detail-tooltip');
    const statsEl = el('graph-detail-stats');
    const wrap = canvas && canvas.closest('.graph-detail-canvas-wrap');
    if (!wrap) return;

    canvas.onmousemove = function (ev) {
      const meta = detailState && detailState.meta;
      if (!meta || !meta.points.length) return;
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const x = (ev.clientX - rect.left) * scaleX;
      let best = null;
      let bestD = Infinity;
      let bestIdx = -1;
      meta.points.forEach((pt, idx) => {
        const d = Math.abs(pt.x - x);
        if (d < bestD) {
          bestD = d;
          best = pt;
          bestIdx = idx;
        }
      });
      if (!best || bestD > 32) {
        if (tip) tip.hidden = true;
        redrawDetailChart(null);
        return;
      }
      redrawDetailChart(bestIdx);
      const raw = best.raw || {};
      const unit = valueUnit(def);
      const label = String(raw.x || '') + ' → ' + Number(raw.y || 0) + unit;
      if (tip) {
        tip.hidden = false;
        tip.textContent = label;
        const wrapRect = wrap.getBoundingClientRect();
        tip.style.left = ev.clientX - wrapRect.left + 14 + 'px';
        tip.style.top = ev.clientY - wrapRect.top - 10 + 'px';
      }
      if (statsEl) {
        statsEl.textContent = def.title + ' · ' + label;
      }
    };
    canvas.onmouseleave = function () {
      if (tip) tip.hidden = true;
      redrawDetailChart(null);
      if (statsEl && detailState && detailState.series) {
        statsEl.textContent =
          detailState.series.length + ' points · Move pointer over chart for values';
      }
    };
  }

  function attachPieHover(canvas, def) {
    const tip = el('graph-detail-tooltip');
    const statsEl = el('graph-detail-stats');
    const wrap = canvas && canvas.closest('.graph-detail-canvas-wrap');
    if (!wrap) return;

    canvas.onmousemove = function (ev) {
      const meta = detailState && detailState.meta;
      if (!meta || !meta.slices.length) return;
      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;
      const mx = (ev.clientX - rect.left) * scaleX;
      const my = (ev.clientY - rect.top) * scaleY;
      const idx = pieSliceAt(meta, mx, my);
      if (idx < 0) {
        if (tip) tip.hidden = true;
        redrawDetailPie(null);
        if (statsEl) {
          statsEl.textContent =
            meta.data.length + ' segments · Hover slices for value breakdown';
        }
        return;
      }
      redrawDetailPie(idx);
      const row = meta.data[idx] || {};
      const pct = meta.total > 0 ? Math.round((100 * Number(row.value)) / meta.total) : 0;
      const label =
        String(row.label || '') + ': ' + Number(row.value || 0) + ' (' + pct + '%)';
      if (tip) {
        tip.hidden = false;
        tip.textContent = label;
        const wrapRect = wrap.getBoundingClientRect();
        tip.style.left = ev.clientX - wrapRect.left + 14 + 'px';
        tip.style.top = ev.clientY - wrapRect.top - 10 + 'px';
      }
      if (statsEl) {
        statsEl.textContent = def.title + ' · ' + label;
      }
    };
    canvas.onmouseleave = function () {
      if (tip) tip.hidden = true;
      redrawDetailPie(null);
      if (statsEl && detailState && detailState.series) {
        const n = detailState.series.filter((r) => Number(r.value) > 0).length;
        statsEl.textContent = n + ' segments · Hover slices for value breakdown';
      }
    };
  }

  function bootGraphs() {
    const app = el('graphs-app');
    if (!app) return;
    const status = el('graphs-status');
    const daysSel = el('graphs-days');
    const colsSel = el('graphs-columns');

    const overlay = el('graph-detail-overlay');
    if (overlay) {
      overlay.addEventListener('click', (ev) => {
        if (ev.target === overlay) closeGraphDetail();
      });
      const closeBtn = el('graph-detail-close');
      if (closeBtn) closeBtn.addEventListener('click', closeGraphDetail);
      const copyBtn = el('graph-detail-copy-link');
      if (copyBtn) {
        copyBtn.addEventListener('click', function () {
          const share = el('graph-detail-share');
          const link = (share && share.href && share.href !== '#') ? share.href : window.location.href;
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(link).then(function () {
              copyBtn.textContent = 'Copied';
              setTimeout(function () { copyBtn.textContent = 'Copy link'; }, 1500);
            }).catch(function () { window.prompt('Copy graph link:', link); });
          } else {
            window.prompt('Copy graph link:', link);
          }
        });
      }
    }
    document.addEventListener('keydown', (ev) => {
      if (ev.key === 'Escape') closeGraphDetail();
    });

    if (colsSel) {
      applyColumns(colsSel.value);
      colsSel.addEventListener('change', () => applyColumns(colsSel.value));
    } else {
      applyColumns(2);
    }

    initBrickDragDrop();

    app.addEventListener('click', (ev) => {
      if (ev.target.closest('.graph-brick-drag-handle') || brickDragMoved) {
        brickDragMoved = false;
        return;
      }
      const card = ev.target.closest('.graph-brick--clickable');
      if (card && app.contains(card)) openGraphDetail(card);
    });
    app.addEventListener('keydown', (ev) => {
      const card = ev.target.closest('.graph-brick--clickable');
      if (card && (ev.key === 'Enter' || ev.key === ' ')) {
        ev.preventDefault();
        openGraphDetail(card);
      }
    });

    function load() {
      const params = new URLSearchParams(window.location.search);
      const daysParam = params.get('days');
      if (daysParam && daysSel) {
        daysSel.value = daysParam;
      }
      const days = Number(daysSel && daysSel.value ? daysSel.value : 14);
      status.textContent = 'Loading…';
      const xhr = new XMLHttpRequest();
      xhr.open('GET', basePath() + '/api/graphs.php?days=' + encodeURIComponent(String(days)), true);
      xhr.onload = function () {
        let payload = null;
        try {
          payload = JSON.parse(xhr.responseText);
        } catch {
          status.textContent = 'Invalid response';
          return;
        }
        if (!payload || !payload.ok) {
          status.textContent = (payload && payload.error) || 'Failed';
          return;
        }
        const data = payload.data || {};
        const viewer = data.viewer || 'user';
        graphScopes.user = data.user || null;
        graphScopes.slug = data.slug_admin || null;
        graphScopes.platform = data.platform_admin || null;
        setScopeVisibility(viewer);
        renderScope('user', graphScopes.user);
        if (graphScopes.slug) renderScope('slug', graphScopes.slug);
        if (graphScopes.platform) renderScope('platform', graphScopes.platform);
        markGraphBricks();
        hideEmptyBricks();
        applyBrickOrder();
        attachBrickPreviewHover();
        const cols = colsSel && colsSel.value ? colsSel.value : '2';
        let statusText = 'Loaded · viewer=' + viewer + ' · window=' + days + 'd · columns=' + cols;
        const visibleScope =
          viewer === 'platform_admin' ? graphScopes.platform : viewer === 'slug_admin' ? graphScopes.slug : graphScopes.user;
        const deficitNotice = el('graphs-session-notice');
        const deficit = sessionMetricsDeficit(visibleScope);
        if (deficitNotice) {
          deficitNotice.hidden = !deficit;
        }
        status.textContent = statusText;
        openGraphFromPermalink();
      };
      xhr.onerror = function () {
        status.textContent = 'Network error';
      };
      xhr.send();
    }

    if (daysSel) {
      daysSel.addEventListener('change', load);
    }
    load();
  }

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