(function () {
  function t(key, params) {
    return window.CrashI18n ? window.CrashI18n.t(key, params) : key;
  }

  const STORAGE = {
    theme: 'crash_console_theme',
    perPage: 'crash_console_per_page',
    lastVisit: 'crash_console_last_visit_ms',
    viewed: 'crash_console_viewed_ids',
    sort: 'crash_console_sort',
    skipStamp: 'crash_skip_visit_stamp',
    colWidths: 'crash_console_col_widths',
    colOrder: 'crash_console_col_order',
  };

  const DEFAULT_COL_WIDTHS = {
    expand: 40,
    device_model: 128,
    app_version: 80,
    os_name: 88,
    os_version: 120,
    received_at_ms: 152,
    rating: 92,
    tags: 128,
  };

  function tagNew() {
    return { label: t('tag.new'), bg: '#3d8bfd' };
  }

  const LIST_COLUMNS = [
    { key: 'device_model', labelKey: 'col.device' },
    { key: 'app_version', labelKey: 'col.app' },
    { key: 'os_name', labelKey: 'col.os' },
    { key: 'os_version', labelKey: 'col.os_ver' },
    { key: 'received_at_ms', labelKey: 'col.received', fmt: 'time' },
    { key: 'rating', labelKey: 'col.rating', fmt: 'stars' },
  ];

  const SORT_API_MAP = {
    priority: 'priority',
    rating: 'fingerprint_cnt',
    os_name: 'device_model',
    os_version: 'app_version',
  };

  const GROUP_COLUMNS = [
    { key: 'cnt', labelKey: 'col.count' },
    { key: 'rating', labelKey: 'col.rating', fmt: 'stars' },
    { key: 'fingerprint', labelKey: 'col.fingerprint', fmt: 'fp' },
    { key: 'crash_type', labelKey: 'col.type' },
    { key: 'last_generated_ms', labelKey: 'col.last_generated', fmt: 'time' },
    { key: 'last_received_ms', labelKey: 'col.last_received', fmt: 'time' },
  ];

  function colLabel(col) {
    if (!col) return '';
    return t(col.labelKey || 'col.' + col.key);
  }

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

  function lsGet(key, fallback) {
    try {
      const v = localStorage.getItem(key);
      return v === null ? fallback : v;
    } catch {
      return fallback;
    }
  }

  function lsSet(key, value) {
    try {
      localStorage.setItem(key, value);
    } catch { /* ignore */ }
  }

  function getViewedIds() {
    try {
      const raw = lsGet(STORAGE.viewed, '[]');
      const arr = JSON.parse(raw);
      return new Set(Array.isArray(arr) ? arr.map(Number).filter(Boolean) : []);
    } catch {
      return new Set();
    }
  }

  function addViewedId(id) {
    const set = getViewedIds();
    set.add(Number(id));
    lsSet(STORAGE.viewed, JSON.stringify([...set]));
  }

  function lastVisitMs() {
    return Number(lsGet(STORAGE.lastVisit, '0')) || 0;
  }

  function stampLastVisit() {
    lsSet(STORAGE.lastVisit, String(Date.now()));
  }

  function applyTheme(theme) {
    const t = theme === 'light' ? 'light' : 'dark';
    document.documentElement.setAttribute('data-theme', t);
    lsSet(STORAGE.theme, t);
    const sel = document.getElementById('theme-select');
    if (sel) sel.value = t;
  }

  function initTheme() {
    applyTheme(lsGet(STORAGE.theme, 'dark'));
    const sel = document.getElementById('theme-select');
    if (sel) {
      sel.addEventListener('change', () => applyTheme(sel.value));
    }
  }

  function initNav() {
    /* nav_shell.js handles toggle + drag on #nav-pane / #nav-handle */
  }

  function markReportViewed(id) {
    if (!id) return;
    addViewedId(id);
    const xhr = new XMLHttpRequest();
    xhr.open('POST', basePath() + '/api/report_viewed.php', true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.send(JSON.stringify({ id: Number(id) }));
  }

  function bindDetailTree(root) {
    root.querySelectorAll('[data-tree-toggle]').forEach((row) => {
      const panelId = row.getAttribute('data-controls');
      const panel = panelId ? document.getElementById(panelId) : null;
      const btn = row.querySelector('.report-tree-toggle');
      if (!panel) return;
      const toggle = (e) => {
        if (e && e.target.closest('.report-tree-toggle') && e.type === 'click') {
          e.stopPropagation();
        }
        const open = panel.hidden;
        panel.hidden = !open;
        if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
        row.classList.toggle('report-row--open', open);
      };
      if (btn) {
        btn.addEventListener('click', (e) => {
          e.stopPropagation();
          toggle(e);
        });
      }
      row.addEventListener('click', () => toggle());
      row.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          toggle();
        }
      });
    });
  }

  function initReportDetail() {
    const id = document.body.getAttribute('data-report-id');
    if (!id) return;
    markReportViewed(id);
    const tree = document.getElementById('detail-tree');
    if (tree) bindDetailTree(tree);
  }

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

  function formatTime(ms) {
    if (!ms) return '—';
    const d = new Date(Number(ms));
    return d.toISOString().replace('T', ' ').slice(0, 19);
  }

  function formatCell(col, row) {
    const v = row[col.key];
    if (col.fmt === 'time') return formatTime(v);
    if (col.fmt === 'stars') return formatStars(Number(row.rating ?? 0));
    if (col.fmt === 'fp') {
      const s = String(v || '');
      return '<code class="fp">' + escapeHtml(s.slice(0, 12)) + (s.length > 12 ? '…' : '') + '</code>';
    }
    return escapeHtml(v ?? '') || '<span class="muted">—</span>';
  }

  function formatStars(n) {
    const score = Math.max(0, Math.min(5, Number(n) || 0));
    let html =
      '<span class="star-rating" role="img" aria-label="' + score + ' out of 5 priority">';
    for (let i = 1; i <= 5; i++) {
      html += '<span class="star' + (i <= score ? ' star--on' : '') + '" aria-hidden="true"></span>';
    }
    return html + '</span>';
  }

  function crashKindTag(crashType) {
    const kind = String(crashType || '').toLowerCase();
    const map = {
      native: { labelKey: 'tag.ndk', bg: '#b45309' },
      java: { labelKey: 'tag.java', bg: '#6d28d9' },
      android: { labelKey: 'tag.android', bg: '#047857' },
    };
    const hit = map[kind];
    if (hit) return { label: t(hit.labelKey), bg: hit.bg };
    return { label: kind || '?', bg: '#5c6b82' };
  }

  function renderTagPills(tags, filterable) {
    if (!tags.length) return '<span class="muted">—</span>';
    return tags
      .map((t) => {
        const label = escapeHtml(t.label || t.id || '');
        const bg = escapeHtml(t.bg || '#5c6b82');
        const id = t.id ? String(t.id).trim() : '';
        if (filterable && id) {
          return (
            '<button type="button" class="report-tag report-tag--filter" data-tag-id="' +
            escapeHtml(id) +
            '" style="--tag-bg:' +
            bg +
            '" title="Filter by tag">' +
            label +
            '</button>'
          );
        }
        return '<span class="report-tag" style="--tag-bg:' + bg + '">' + label + '</span>';
      })
      .join('');
  }

  function buildTags(row, grouped) {
    const tags = [];
    const kind = crashKindTag(row.crash_type);
    tags.push(kind);
    if (!grouped && row.id) {
      const viewed = row.viewed || getViewedIds().has(Number(row.id));
      const sinceVisit = Number(row.received_at_ms) > lastVisitMs();
      if (!viewed && (lastVisitMs() === 0 || sinceVisit)) {
        tags.push(tagNew());
      }
    }
    if (Array.isArray(row.tags)) {
      row.tags.forEach((t) => {
        if (!t || !t.id) return;
        const id = String(t.id).toLowerCase();
        if (id === 'new' || id === 'java' || id === 'ndk' || id === 'android') return;
        tags.push({
          id,
          label: t.label || t.id,
          bg: t.bg || '#5c6b82',
        });
      });
    }
    return renderTagPills(tags, !grouped);
  }

  function apiSortKey(key) {
    return SORT_API_MAP[key] || key;
  }

  function loadSavedSort(isGrouped) {
    try {
      const s = JSON.parse(lsGet(STORAGE.sort, ''));
      if (!s || !s.sort) {
        return { sort: isGrouped ? 'cnt' : 'priority', dir: 'desc' };
      }
      if (!!s.grouped !== !!isGrouped) {
        return { sort: isGrouped ? 'cnt' : 'priority', dir: 'desc' };
      }
      return { sort: s.sort, dir: s.dir || 'desc' };
    } catch {
      return { sort: isGrouped ? 'cnt' : 'priority', dir: 'desc' };
    }
  }

  function getColWidths() {
    try {
      const w = JSON.parse(lsGet(STORAGE.colWidths, '{}'));
      return { ...DEFAULT_COL_WIDTHS, ...(w && typeof w === 'object' ? w : {}) };
    } catch {
      return { ...DEFAULT_COL_WIDTHS };
    }
  }

  function saveColWidths(widths) {
    lsSet(STORAGE.colWidths, JSON.stringify(widths));
  }

  function getOrderedColumnKeys() {
    const defaults = LIST_COLUMNS.map((c) => c.key);
    try {
      const saved = JSON.parse(lsGet(STORAGE.colOrder, '[]'));
      if (Array.isArray(saved) && saved.length) {
        const out = saved.filter((k) => defaults.includes(k));
        defaults.forEach((k) => {
          if (!out.includes(k)) out.push(k);
        });
        return out;
      }
    } catch { /* ignore */ }
    return defaults;
  }

  function saveColumnOrder(keys) {
    lsSet(STORAGE.colOrder, JSON.stringify(keys));
  }

  function getDisplayColumns(isGrouped) {
    if (isGrouped) return GROUP_COLUMNS;
    return getOrderedColumnKeys()
      .map((k) => LIST_COLUMNS.find((c) => c.key === k))
      .filter(Boolean);
  }

  function applyColgroup(isGrouped) {
    const cg = document.getElementById('reports-colgroup');
    if (!cg || isGrouped) {
      if (cg) cg.innerHTML = '';
      return;
    }
    const widths = getColWidths();
    const order = getOrderedColumnKeys();
    let html =
      '<col class="col-expand" style="width:' + (widths.expand || 40) + 'px" />';
    order.forEach((k) => {
      html +=
        '<col data-col="' +
        escapeHtml(k) +
        '" style="width:' +
        (widths[k] || 100) +
        'px" />';
    });
    html +=
      '<col class="col-tags" style="width:' + (widths.tags || 128) + 'px" />';
    cg.innerHTML = html;
  }

  function columnLabelByKey(key) {
    const c = LIST_COLUMNS.find((col) => col.key === key);
    if (c) return colLabel(c);
    const g = GROUP_COLUMNS.find((col) => col.key === key);
    if (g) return colLabel(g);
    if (key === 'tags') return t('col.tags');
    return key;
  }

  function clearColumnDropMarkers(thead) {
    thead.querySelectorAll('.th-drop-target').forEach((el) => {
      el.classList.remove('th-drop-target');
    });
    thead.querySelectorAll('.th-dragging').forEach((el) => {
      el.classList.remove('th-dragging');
    });
  }

  function bindReportsTableLayout(table, thead, ctx) {
    if (!table || !thead || table.dataset.layoutBound === '1') return;
    table.dataset.layoutBound = '1';

    thead.addEventListener('dragstart', (e) => {
      if (ctx.getGrouped()) return;
      if (e.target.closest('.col-resizer')) {
        e.preventDefault();
        return;
      }
      const th = e.target.closest('th.th-draggable[data-col-key]');
      if (!th) return;
      const sourceKey = th.getAttribute('data-col-key');
      if (!sourceKey) return;
      table.dataset.dragColKey = sourceKey;
      th.classList.add('th-dragging');
      const label = columnLabelByKey(sourceKey);
      const ghost = document.createElement('div');
      ghost.className = 'col-drag-ghost';
      ghost.textContent = label;
      ghost.setAttribute('aria-hidden', 'true');
      document.body.appendChild(ghost);
      table._dragGhost = ghost;
      if (e.dataTransfer) {
        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/plain', sourceKey);
        try {
          e.dataTransfer.setDragImage(ghost, 16, 14);
        } catch { /* ignore */ }
      }
    });

    thead.addEventListener('dragend', () => {
      clearColumnDropMarkers(thead);
      delete table.dataset.dragColKey;
      if (table._dragGhost) {
        table._dragGhost.remove();
        table._dragGhost = null;
      }
    });

    thead.addEventListener('dragenter', (e) => {
      if (ctx.getGrouped() || !table.dataset.dragColKey) return;
      e.preventDefault();
    });

    thead.addEventListener('dragover', (e) => {
      if (ctx.getGrouped() || !table.dataset.dragColKey) return;
      e.preventDefault();
      if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      const active = thead.querySelector('.th-drop-target');
      if (active && active !== th) active.classList.remove('th-drop-target');
      th.classList.add('th-drop-target');
    });

    thead.addEventListener('dragleave', (e) => {
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      const rel = e.relatedTarget;
      if (rel && th.contains(rel)) return;
      th.classList.remove('th-drop-target');
    });

    thead.addEventListener('drop', (e) => {
      if (ctx.getGrouped()) return;
      const th = e.target.closest('th[data-col-key]');
      if (!th) return;
      e.preventDefault();
      e.stopPropagation();
      const sourceKey =
        table.dataset.dragColKey ||
        (e.dataTransfer && e.dataTransfer.getData('text/plain')) ||
        '';
      const targetKey = th.getAttribute('data-col-key');
      clearColumnDropMarkers(thead);
      delete table.dataset.dragColKey;
      if (table._dragGhost) {
        table._dragGhost.remove();
        table._dragGhost = null;
      }
      if (!sourceKey || !targetKey || sourceKey === targetKey) return;
      const order = getOrderedColumnKeys();
      const from = order.indexOf(sourceKey);
      const to = order.indexOf(targetKey);
      if (from < 0 || to < 0) return;
      order.splice(from, 1);
      order.splice(to, 0, sourceKey);
      saveColumnOrder(order);
      ctx.refreshLayout();
    });

    table.addEventListener('mousedown', (e) => {
      if (ctx.getGrouped()) return;
      const handle = e.target.closest('.col-resizer');
      if (!handle) return;
      e.preventDefault();
      e.stopPropagation();
      const colKey = handle.getAttribute('data-resize-col');
      const th = handle.closest('th');
      if (!colKey || !th) return;
      const widths = getColWidths();
      const startX = e.clientX;
      const startW = th.getBoundingClientRect().width;

      const onMove = (ev) => {
        const w = Math.max(48, Math.round(startW + (ev.clientX - startX)));
        widths[colKey] = w;
        th.style.width = w + 'px';
        th.style.minWidth = w + 'px';
        th.style.maxWidth = w + 'px';
        const cg = document.getElementById('reports-colgroup');
        const col = cg && cg.querySelector('col[data-col="' + colKey + '"]');
        if (col) col.style.width = w + 'px';
        else if (colKey === 'tags') {
          const tagCol = cg && cg.querySelector('col.col-tags');
          if (tagCol) tagCol.style.width = w + 'px';
        }
      };
      const onUp = () => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('mouseup', onUp);
        saveColWidths(widths);
        applyColgroup(false);
      };
      document.addEventListener('mousemove', onMove);
      document.addEventListener('mouseup', onUp);
    });
  }

  function parseListContext() {
    const p = new URLSearchParams(window.location.search);
    const filterTags = [];
    p.getAll('tag').forEach((t) => {
      const id = String(t).trim().toLowerCase();
      if (id && !filterTags.includes(id)) filterTags.push(id);
    });
    return {
      similarTo: Number(p.get('similar_to')) || 0,
      filterDevice: p.get('filter_device') || '',
      filterAbi: p.get('filter_abi') || '',
      filterOsSdk: Number(p.get('filter_os_sdk')) || 0,
      filterOsRelease: p.get('filter_os_release') || '',
      filterAppVersion: p.get('filter_app_version') || '',
      filterBuild: p.get('filter_build') || '',
      filterTags,
      tagMode: p.get('tag_mode') === 'or' ? 'or' : 'and',
      searchQuery: p.get('q') || '',
    };
  }

  function listContextActive(ctx) {
    return (
      ctx.similarTo > 0 ||
      !!ctx.filterDevice ||
      !!ctx.filterAbi ||
      ctx.filterOsSdk > 0 ||
      !!ctx.filterOsRelease ||
      !!ctx.filterAppVersion ||
      !!ctx.filterBuild ||
      (ctx.filterTags && ctx.filterTags.length > 0) ||
      !!ctx.searchQuery
    );
  }

  function reportsUrlWithTagToggle(tagId, tagMode) {
    const p = new URLSearchParams(window.location.search);
    p.set('view', 'reports');
    const tags = p.getAll('tag');
    const idx = tags.indexOf(tagId);
    if (idx >= 0) {
      tags.splice(idx, 1);
    } else {
      tags.push(tagId);
    }
    p.delete('tag');
    tags.forEach((t) => p.append('tag', t));
    if (tags.length) {
      p.set('tag_mode', tagMode === 'or' ? 'or' : 'and');
    } else {
      p.delete('tag_mode');
    }
    p.delete('group');
    p.delete('page');
    return basePath() + '/?' + p.toString();
  }

  function updateFilterBanner(ctx, bannerEl) {
    if (!bannerEl) return;
    const parts = [];
    if (ctx.similarTo > 0) {
      parts.push('Similar reports for #' + ctx.similarTo);
    }
    if (ctx.filterDevice) {
      parts.push('Device: “' + ctx.filterDevice + '”');
    }
    if (ctx.filterAbi) {
      parts.push('ABI: ' + ctx.filterAbi);
    }
    if (ctx.filterOsSdk > 0) {
      parts.push('SDK ' + ctx.filterOsSdk);
    }
    if (ctx.filterOsRelease) {
      parts.push('Android ' + ctx.filterOsRelease);
    }
    if (ctx.filterAppVersion) {
      parts.push('App ' + ctx.filterAppVersion);
    }
    if (ctx.filterBuild) {
      parts.push('Build ' + ctx.filterBuild);
    }
    if (ctx.filterTags && ctx.filterTags.length) {
      const modeLabel = ctx.tagMode === 'or' ? 'OR' : 'AND';
      parts.push('Tags (' + modeLabel + '): ' + ctx.filterTags.join(', '));
    }
    if (ctx.searchQuery) {
      parts.push('Search: “' + ctx.searchQuery + '”');
    }
    if (!parts.length) {
      bannerEl.hidden = true;
      bannerEl.textContent = '';
      return;
    }
    bannerEl.hidden = false;
    bannerEl.innerHTML =
      parts.join(' · ') +
      ' — <a href="' +
      escapeHtml(basePath() + '/?view=reports') +
      '">Clear filters</a>';
  }

  function navigateTo(href) {
    try {
      sessionStorage.setItem(STORAGE.skipStamp, '1');
    } catch { /* ignore */ }
    window.location.href = href;
  }

  function bindTreeInteractions(root) {
    root.querySelectorAll('.report-tree-toggle').forEach((btn) => {
      btn.addEventListener('click', (e) => {
        e.stopPropagation();
        const id = btn.getAttribute('aria-controls');
        const briefRow = id ? document.getElementById(id) : null;
        const row = btn.closest('.report-row');
        if (!briefRow || !row) return;
        const open = briefRow.hidden;
        briefRow.hidden = !open;
        btn.setAttribute('aria-expanded', open ? 'true' : 'false');
        row.classList.toggle('report-row--open', open);
      });
    });

    root.querySelectorAll('[data-href]').forEach((el) => {
      const href = el.getAttribute('data-href');
      if (!href) return;
      const go = () => navigateTo(href);
      const onActivate = (e) => {
        if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return;
        go();
      };
      el.addEventListener('click', onActivate);
      el.addEventListener('keydown', (e) => {
        if (e.target.closest('.report-tree-toggle, .tag-edit-btn, .report-tag--filter')) return;
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          go();
        }
      });
    });
  }

  function bindReportTagFilters(root, onTag) {
    if (!root || typeof onTag !== 'function') return;
    root.querySelectorAll('.report-tag--filter').forEach((btn) => {
      btn.addEventListener('click', (e) => {
        const tagId = btn.getAttribute('data-tag-id') || '';
        if (tagId) onTag(tagId, e);
      });
    });
  }

  function initReportsApp() {
    const app = document.getElementById('reports-app');
    if (!app) return;

    const thead = document.getElementById('reports-thead');
    const tbody = document.getElementById('reports-tbody');
    const pagination = document.getElementById('reports-pagination');
    const statusEl = document.getElementById('reports-status');
    const perPageSel = document.getElementById('per-page-select');

    const table = document.getElementById('reports-tree');
    const filterBanner = document.getElementById('reports-filter-banner');
    const listContext = parseListContext();
    let grouped =
      listContext.filterTags && listContext.filterTags.length > 0
        ? false
        : listContextActive(listContext)
          ? false
          : app.getAttribute('data-grouped') === '1';
    let page = 1;
    let perPage = Number(lsGet(STORAGE.perPage, '50')) || 50;
    let listTotal = 0;
    const saved = loadSavedSort(grouped);
    let sort = saved.sort;
    let dir = saved.dir;
    let loading = false;
    let lastItems = [];

    function saveSortState() {
      lsSet(
        STORAGE.sort,
        JSON.stringify({ sort, dir, grouped: !!grouped })
      );
    }

    function sortActive(colKey) {
      if (sort === 'priority' && colKey === 'rating') return true;
      if (sort === colKey) return true;
      return apiSortKey(sort) === apiSortKey(colKey);
    }

    const layoutCtx = {
      getGrouped: () => grouped,
      refreshLayout: () => {
        applyColgroup(grouped);
        renderHead();
        if (lastItems.length) renderRows(lastItems);
      },
    };

    function refreshTableChrome() {
      layoutCtx.refreshLayout();
    }

    bindReportsTableLayout(table, thead, layoutCtx);
    updateFilterBanner(listContext, filterBanner);
    initTagFilterBar(listContext);

    if (listContextActive(listContext)) {
      app.setAttribute('data-grouped', '0');
      app.querySelectorAll('.reports-mode-btn').forEach((b) => {
        b.classList.toggle('active', b.getAttribute('data-grouped') === '0');
        b.disabled = true;
        b.title = 'Disabled while a filter is active';
      });
    }

    if (perPageSel) {
      perPageSel.value = String(perPage);
      perPageSel.addEventListener('change', () => {
        perPage = Number(perPageSel.value) || 50;
        lsSet(STORAGE.perPage, String(perPage));
        page = 1;
        load();
      });
    }

    app.querySelectorAll('.reports-mode-btn').forEach((btn) => {
      btn.addEventListener('click', () => {
        grouped = btn.getAttribute('data-grouped') === '1';
        app.setAttribute('data-grouped', grouped ? '1' : '0');
        app.querySelectorAll('.reports-mode-btn').forEach((b) => {
          b.classList.toggle('active', b === btn);
        });
        const next = loadSavedSort(grouped);
        sort = next.sort;
        dir = next.dir;
        page = 1;
        load();
      });
    });

    const maybeStampVisit = () => {
      try {
        if (sessionStorage.getItem(STORAGE.skipStamp)) {
          sessionStorage.removeItem(STORAGE.skipStamp);
          return;
        }
      } catch { /* ignore */ }
      stampLastVisit();
    };
    window.addEventListener('pagehide', maybeStampVisit);

    function columns() {
      return getDisplayColumns(grouped);
    }

    function colspan() {
      return columns().length + 2;
    }

    function renderHead() {
      const cols = columns();
      const widths = getColWidths();
      let html =
        '<tr><th class="report-tree-head" aria-label="Expand"><span class="col-resizer" data-resize-col="expand"></span></th>';
      cols.forEach((c) => {
        const active = sortActive(c.key);
        const arrow =
          active ? (dir === 'asc' ? '▲' : '▼') : '<span class="sort-idle">↕</span>';
        const w = widths[c.key] ? ' style="width:' + widths[c.key] + 'px"' : '';
        let thClass = 'sortable' + (active ? ' sortable--active' : '');
        let thDragAttrs = '';
        if (!grouped) {
          thClass += ' th-draggable';
          thDragAttrs =
            ' draggable="true" data-col-key="' +
            escapeHtml(c.key) +
            '" title="' +
            escapeHtml(t('col.drag_reorder')) +
            '"';
        }
        html +=
          '<th class="' +
          thClass +
          '"' +
          thDragAttrs +
          ' data-sort="' +
          escapeHtml(c.key) +
          '"' +
          w +
          ' scope="col"><span class="th-inner">' +
          (grouped
            ? ''
            : '<span class="th-drag" aria-hidden="true" title="' +
              escapeHtml(t('col.drag')) +
              '">⋮⋮</span>') +
          '<span class="th-label">' +
          escapeHtml(colLabel(c)) +
          '</span> <span class="sort-ind" aria-hidden="true">' +
          arrow +
          '</span></span>' +
          (grouped ? '' : '<span class="col-resizer" data-resize-col="' + escapeHtml(c.key) + '"></span>') +
          '</th>';
      });
      html +=
        '<th class="col-tags" scope="col" style="width:' +
        (widths.tags || 128) +
        'px">Tags<span class="col-resizer" data-resize-col="tags"></span></th></tr>';
      thead.innerHTML = html;
      thead.querySelectorAll('.sortable').forEach((th) => {
        th.addEventListener('click', (e) => {
          if (e.target.closest('.col-resizer')) return;
          const key = th.getAttribute('data-sort');
          if (!key) return;
          if (sort === key) {
            dir = dir === 'asc' ? 'desc' : 'asc';
          } else {
            sort = key;
            dir = 'desc';
          }
          saveSortState();
          page = 1;
          load();
        });
      });
    }

    function pageRangeEnd(targetPage, totalCount) {
      return Math.min(targetPage * perPage, totalCount);
    }

    function renderPagination(total) {
      listTotal = total;
      const pages = Math.max(1, Math.ceil(total / perPage));
      if (page > pages) page = pages;
      const prev = page > 1 ? page - 1 : null;
      const next = page < pages ? page + 1 : null;
      let prevLabel = '← Previous';
      let nextLabel = 'Next →';
      if (prev) {
        const t = page - 1;
        const before = (t - 1) * perPage;
        const through = pageRangeEnd(t, total);
        prevLabel = '← Previous (' + before + '/' + through + ')';
      }
      if (next) {
        const start = page * perPage + 1;
        const end = pageRangeEnd(page + 1, total);
        nextLabel = 'Next (' + start + '/' + end + ') →';
      }
      let html = '<div class="pagination-inner">';
      html +=
        '<button type="button" class="btn page-btn" data-page="' +
        (prev || '') +
        '" ' +
        (prev ? '' : 'disabled') +
        '>' +
        escapeHtml(prevLabel) +
        '</button>';
      html +=
        '<span class="page-info">Page ' +
        page +
        ' / ' +
        pages +
        ' · ' +
        total +
        ' reports</span>';
      html +=
        '<button type="button" class="btn page-btn" data-page="' +
        (next || '') +
        '" ' +
        (next ? '' : 'disabled') +
        '>' +
        escapeHtml(nextLabel) +
        '</button>';
      html += '</div>';
      pagination.innerHTML = html;
      pagination.querySelectorAll('.page-btn').forEach((btn) => {
        btn.addEventListener('click', () => {
          const p = Number(btn.getAttribute('data-page'));
          if (!p) return;
          page = p;
          load();
        });
      });
    }

    function renderRows(items) {
      const cols = columns();
      if (!items.length) {
        tbody.innerHTML =
          '<tr><td colspan="' +
          colspan() +
          '" class="muted">' +
          escapeHtml(t('reports.empty')) +
          '</td></tr>';
        return;
      }
      let html = '';
      items.forEach((row, i) => {
        const rowKey = grouped ? 'g' + i + '-' + page : 'r' + row.id;
        const briefId = 'brief-' + rowKey;
        const openUrl = !grouped && row.id
          ? basePath() + '/?view=report&id=' + row.id
          : '';
        const navClass = openUrl ? ' report-row--nav' : '';
        const hrefAttr = openUrl ? ' data-href="' + escapeHtml(openUrl) + '"' : '';
        const tabAttr = openUrl ? ' tabindex="0" role="link"' : '';

        html += '<tr class="report-row' + navClass + '"' + hrefAttr + tabAttr + '>';
        html +=
          '<td class="report-tree-cell"><button type="button" class="report-tree-toggle" aria-expanded="false" aria-controls="' +
          briefId +
          '" title="' +
          escapeHtml(t('row.show_summary')) +
          '"><span class="report-tree-arrow" aria-hidden="true"></span></button></td>';
        cols.forEach((c) => {
          if (c.key === 'cnt') {
            html += '<td><span class="badge">' + escapeHtml(row.cnt) + '</span></td>';
          } else {
            html += '<td>' + formatCell(c, row) + '</td>';
          }
        });
        const tagEditBtn =
          !grouped && row.id && canTagEdit()
            ? '<button type="button" class="tag-edit-btn" data-report-id="' +
              row.id +
              '" title="Edit tags" aria-label="Edit tags">✎</button>'
            : '';
        html +=
          '<td class="col-tags"><div class="report-tags">' +
          buildTags(row, grouped) +
          tagEditBtn +
          '</div></td>';
        html += '</tr>';

        const briefLines = row.brief || [];
        let panelOpen = '<div class="report-brief-panel">';
        if (openUrl) {
          panelOpen =
            '<div class="report-brief-panel report-brief-panel--nav report-row--nav" data-href="' +
            escapeHtml(openUrl) +
            '" tabindex="0" role="link">';
        }
        html +=
          '<tr class="report-brief-row" id="' +
          briefId +
          '" hidden><td colspan="' +
          colspan() +
          '" class="report-brief-cell">' +
          panelOpen +
          '<div class="report-brief">';
        briefLines.forEach((line) => {
          html += '<p>' + escapeHtml(line) + '</p>';
        });
        if (openUrl) {
          html += '<p class="report-brief-hint muted">Click to open full report</p>';
        }
        html += '</div></div></td></tr>';
      });
      tbody.innerHTML = html;
      bindTreeInteractions(tbody);
      bindReportTagFilters(tbody, handleReportTagClick);
    }

    function applyReportTagFilter(tagId) {
      listContext.filterTags = [tagId];
      listContext.tagMode = listContext.tagMode || 'and';
      grouped = false;
      app.setAttribute('data-grouped', '0');
      page = 1;
      updateFilterBanner(listContext, filterBanner);
      const p = new URLSearchParams(window.location.search);
      p.set('view', 'reports');
      p.delete('tag');
      p.append('tag', tagId);
      p.set('tag_mode', listContext.tagMode);
      p.delete('group');
      p.delete('page');
      history.replaceState(null, '', basePath() + '/?' + p.toString());
      const modeWrap = document.getElementById('tag-mode-wrap');
      if (modeWrap) modeWrap.hidden = false;
      const modeSel = document.getElementById('tag-mode-select');
      if (modeSel) modeSel.value = listContext.tagMode;
      load();
    }

    function handleReportTagClick(tagId, ev) {
      ev.stopPropagation();
      ev.preventDefault();
      if (grouped) {
        applyReportTagFilter(tagId);
        return;
      }
      const xhr = new XMLHttpRequest();
      xhr.open(
        'GET',
        basePath() +
          '/api/reports.php?page=1&per_page=2&tag=' +
          encodeURIComponent(tagId) +
          '&tag_mode=and&sort=received_at_ms&dir=desc&since_ms=0',
        true
      );
      xhr.onload = function () {
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          return;
        }
        if (!data.ok) return;
        const total = Number(data.total || 0);
        if (total === 1 && data.items && data.items[0] && data.items[0].id) {
          navigateTo(basePath() + '/?view=report&id=' + data.items[0].id);
          return;
        }
        applyReportTagFilter(tagId);
      };
      xhr.send();
    }

    function load() {
      if (loading) return;
      loading = true;
      statusEl.textContent = t('reports.loading');
      let qs =
        'page=' +
        page +
        '&per_page=' +
        perPage +
        '&sort=' +
        encodeURIComponent(apiSortKey(sort)) +
        '&dir=' +
        encodeURIComponent(dir) +
        '&since_ms=' +
        encodeURIComponent(String(lastVisitMs()));
      if (listContext.searchQuery) {
        qs += '&q=' + encodeURIComponent(listContext.searchQuery);
      } else if (listContext.similarTo > 0) {
        qs += '&similar_to=' + encodeURIComponent(String(listContext.similarTo));
      } else {
        if (grouped) qs += '&group=1';
        if (listContext.filterDevice) {
          qs += '&filter_device=' + encodeURIComponent(listContext.filterDevice);
        }
        if (listContext.filterAbi) {
          qs += '&filter_abi=' + encodeURIComponent(listContext.filterAbi);
        }
        if (listContext.filterOsSdk > 0) {
          qs += '&filter_os_sdk=' + encodeURIComponent(String(listContext.filterOsSdk));
        }
        if (listContext.filterOsRelease) {
          qs += '&filter_os_release=' + encodeURIComponent(listContext.filterOsRelease);
        }
        if (listContext.filterAppVersion) {
          qs += '&filter_app_version=' + encodeURIComponent(listContext.filterAppVersion);
        }
        if (listContext.filterBuild) {
          qs += '&filter_build=' + encodeURIComponent(listContext.filterBuild);
        }
        if (listContext.filterTags && listContext.filterTags.length) {
          listContext.filterTags.forEach((tagId) => {
            qs += '&tag=' + encodeURIComponent(tagId);
          });
          qs += '&tag_mode=' + encodeURIComponent(listContext.tagMode || 'and');
        }
      }
      const xhr = new XMLHttpRequest();
      xhr.open('GET', basePath() + '/api/reports.php?' + qs, true);
      xhr.onload = function () {
        loading = false;
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          statusEl.textContent = 'Invalid response';
          return;
        }
        if (!data.ok) {
          statusEl.textContent = data.error || 'Load failed';
          return;
        }
        lastItems = data.items || [];
        refreshTableChrome();
        renderPagination(Number(data.total) || 0);
        statusEl.textContent =
          'Showing ' +
          (data.items?.length || 0) +
          ' of ' +
          (data.total || 0) +
          (grouped ? ' (grouped)' : '');
      };
      xhr.onerror = function () {
        loading = false;
        statusEl.textContent = 'Network error';
      };
      xhr.send();
    }

    initTagEditButtons(tbody, load);

    const searchInput = document.getElementById('reports-search-input');
    const searchSuggest = document.getElementById('reports-search-suggest');
    const searchClear = document.getElementById('reports-search-clear');
    let suggestTimer = null;

    function hideSuggest() {
      if (searchSuggest) {
        searchSuggest.hidden = true;
        searchSuggest.innerHTML = '';
      }
    }

    function applySearchQuery(q, pushUrl) {
      listContext.similarTo = 0;
      listContext.filterDevice = '';
      listContext.filterAbi = '';
      listContext.filterOsSdk = 0;
      listContext.filterOsRelease = '';
      listContext.filterAppVersion = '';
      listContext.filterBuild = '';
      listContext.searchQuery = (q || '').trim();
      grouped = false;
      app.setAttribute('data-grouped', '0');
      page = 1;
      updateFilterBanner(listContext, filterBanner);
      if (searchInput) searchInput.value = listContext.searchQuery;
      if (searchClear) searchClear.hidden = !listContext.searchQuery;
      if (pushUrl) {
        const url =
          basePath() +
          '/?view=reports' +
          (listContext.searchQuery ? '&q=' + encodeURIComponent(listContext.searchQuery) : '');
        history.replaceState(null, '', url);
      }
      load();
    }

    function fetchSuggest(q) {
      if (!searchSuggest || q.length < 2) {
        hideSuggest();
        return;
      }
      const xhr = new XMLHttpRequest();
      xhr.open(
        'GET',
        basePath() +
          '/api/reports.php?suggest=1&q=' +
          encodeURIComponent(q) +
          '&per_page=10',
        true
      );
      xhr.onload = function () {
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          hideSuggest();
          return;
        }
        if (!data.ok || !data.suggestions || !data.suggestions.length) {
          hideSuggest();
          return;
        }
        searchSuggest.innerHTML = data.suggestions
          .map(
            (s) =>
              '<li role="option"><button type="button" class="suggest-item" data-suggest="' +
              escapeHtml(s) +
              '">' +
              escapeHtml(s) +
              '</button></li>'
          )
          .join('');
        searchSuggest.hidden = false;
      };
      xhr.send();
    }

    if (searchInput) {
      if (listContext.searchQuery) {
        searchInput.value = listContext.searchQuery;
        if (searchClear) searchClear.hidden = false;
      }
      searchInput.addEventListener('input', () => {
        const q = searchInput.value;
        if (searchClear) searchClear.hidden = !q.trim();
        hideSuggest();
        clearTimeout(suggestTimer);
        const trimmed = q.trim();
        if (trimmed.length < 2) return;
        suggestTimer = setTimeout(() => fetchSuggest(trimmed), 1000);
      });
      searchInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
          e.preventDefault();
          clearTimeout(suggestTimer);
          hideSuggest();
          applySearchQuery(searchInput.value.trim(), true);
        }
        if (e.key === 'Escape') {
          hideSuggest();
        }
      });
    }
    function selectSearchSuggestion(term) {
      if (!searchInput) return;
      const t = (term || '').trim();
      if (!t) return;
      const raw = searchInput.value;
      const withoutTrailing = raw.replace(/\s+$/, '');
      const lastSpace = withoutTrailing.lastIndexOf(' ');
      searchInput.value =
        lastSpace < 0
          ? t
          : withoutTrailing.slice(0, lastSpace + 1) + t;
      if (searchClear) searchClear.hidden = !searchInput.value.trim();
      searchInput.focus();
    }
    if (searchSuggest) {
      searchSuggest.addEventListener('click', (e) => {
        const btn = e.target.closest('[data-suggest]');
        if (!btn) return;
        const term = btn.getAttribute('data-suggest') || '';
        selectSearchSuggestion(term);
        hideSuggest();
      });
    }
    if (searchClear) {
      searchClear.addEventListener('click', () => {
        if (searchInput) searchInput.value = '';
        searchClear.hidden = true;
        hideSuggest();
        applySearchQuery('', true);
      });
    }

    applyColgroup(grouped);
    load();
  }

  const TAG_QUICK_COLORS = [
    '#5c6b82', '#3d8bfd', '#6d28d9', '#b45309', '#047857',
    '#dc2626', '#d97706', '#0891b2', '#be185d', '#4f46e5',
  ];

  function canTagEdit() {
    return document.body.getAttribute('data-can-tag-edit') === '1';
  }

  function slugTagId(label) {
    return String(label)
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 32);
  }

  function renderTagPill(tag, removable) {
    let html =
      '<span class="report-tag" style="--tag-bg:' +
      escapeHtml(tag.bg || '#5c6b82') +
      '">' +
      escapeHtml(tag.label || tag.id);
    if (removable) {
      html +=
        '<button type="button" class="tag-remove" data-remove-id="' +
        escapeHtml(tag.id) +
        '" aria-label="' + escapeHtml(t('tag.remove')) + '">×</button>';
    }
    return html + '</span>';
  }

  function tagsApiUrl(entityType) {
    return basePath() + (entityType === 'ticket' ? '/api/ticket_tags.php' : '/api/report_tags.php');
  }

  const TICKET_META_IDS = ['ticket', 'issue'];

  function isTicketMetaId(id) {
    return TICKET_META_IDS.indexOf(id) >= 0;
  }

  function stripTicketMetaTags(tags) {
    return (tags || []).filter((t) => !isTicketMetaId(t.id));
  }

  function tagsJson(tags) {
    return JSON.stringify(tags || []);
  }

  function createTagEditor(cfg) {
    const entityType = cfg.entityType === 'ticket' ? 'ticket' : 'report';
    const state = {
      tags: [...(cfg.initialTags || [])],
      workflow: [],
      presets: [],
      entityId: cfg.entityId ?? cfg.reportId ?? cfg.ticketId ?? 0,
      entityType,
    };
    const els = cfg.elements;

    function renderList() {
      if (!els.list) return;
      if (!state.tags.length) {
        els.list.innerHTML = '<li class="muted">No custom tags yet.</li>';
        return;
      }
      els.list.innerHTML = state.tags
        .map(
          (t, i) =>
            '<li class="tag-editor-item">' +
            renderTagPill(t, true) +
            '<label class="tag-color-edit">Color <input type="color" data-color-idx="' +
            i +
            '" value="' +
            escapeHtml(t.bg || '#5c6b82') +
            '"></label></li>'
        )
        .join('');
      els.list.querySelectorAll('.tag-remove').forEach((btn) => {
        btn.addEventListener('click', () => {
          const id = btn.getAttribute('data-remove-id');
          state.tags = state.tags.filter((t) => t.id !== id);
          renderAll();
        });
      });
      els.list.querySelectorAll('input[data-color-idx]').forEach((inp) => {
        inp.addEventListener('input', () => {
          const idx = Number(inp.getAttribute('data-color-idx'));
          if (state.tags[idx]) state.tags[idx].bg = inp.value;
          renderPreview();
        });
      });
    }

    function renderPreview() {
      if (!els.preview) return;
      els.preview.innerHTML = state.tags.length
        ? state.tags.map((t) => renderTagPill(t, false)).join('')
        : '<span class="muted">No custom tags</span>';
    }

    function presetChipHtml(p) {
      return (
        '<button type="button" class="tag-preset-chip" data-preset-id="' +
        escapeHtml(p.id) +
        '" style="--tag-bg:' +
        escapeHtml(p.bg) +
        '">' +
        escapeHtml(p.label) +
        '</button>'
      );
    }

    function wirePresetChips(root) {
      if (!root) return;
      root.querySelectorAll('.tag-preset-chip').forEach((btn) => {
        btn.addEventListener('click', () => {
          const id = btn.getAttribute('data-preset-id');
          const p =
            state.workflow.find((x) => x.id === id) ||
            state.presets.find((x) => x.id === id);
          if (!p || state.tags.some((t) => t.id === p.id)) return;
          if (entityType === 'ticket' && isTicketMetaId(p.id)) {
            state.tags = stripTicketMetaTags(state.tags);
          }
          state.tags.push({ ...p });
          renderAll();
        });
      });
    }

    function renderPresets() {
      if (!els.presetBar || !els.presetChips) return;
      const wf = state.workflow || [];
      const hist = (state.presets || []).filter(
        (p) => !wf.some((w) => w.id === p.id)
      );
      if (!wf.length && !hist.length) {
        els.presetBar.hidden = true;
        return;
      }
      els.presetBar.hidden = false;
      let html = '';
      if (wf.length) {
        html +=
          '<div class="tag-preset-group"><span class="muted tag-preset-group-label">' +
          escapeHtml(t('tag.workflow')) +
          '</span><div class="tag-preset-chips">' +
          wf.map(presetChipHtml).join('') +
          '</div></div>';
      }
      if (hist.length) {
        html +=
          '<div class="tag-preset-group"><span class="muted tag-preset-group-label">' +
          escapeHtml(t('tag.suggestions')) +
          '</span><div class="tag-preset-chips">' +
          hist.map(presetChipHtml).join('') +
          '</div></div>';
      }
      els.presetChips.innerHTML = html;
      wirePresetChips(els.presetChips);
    }

    function renderAll() {
      renderList();
      renderPreview();
    }

    function setStatus(msg, ok) {
      if (!els.status) return;
      els.status.textContent = msg || '';
      els.status.classList.toggle('tag-editor-status--err', ok === false);
    }

    function addFromForm(form) {
      const label = (form.label?.value || '').trim();
      const bg = form.bg?.value || '#5c6b82';
      if (!label) return;
      const id = slugTagId(label);
      if (!id) return;
      const reserved = ['new', 'java', 'ndk', 'android'];
      if (reserved.includes(id)) {
        setStatus('Reserved tag id: ' + id, false);
        return;
      }
      const existing = state.tags.findIndex((t) => t.id === id);
      const entry = { id, label, bg };
      if (existing >= 0) state.tags[existing] = entry;
      else state.tags.push(entry);
      form.label.value = '';
      renderAll();
      setStatus('', true);
    }

    if (els.form) {
      els.form.addEventListener('submit', (e) => {
        e.preventDefault();
        addFromForm(els.form);
      });
    }

    if (els.saveBtn) {
      els.saveBtn.addEventListener('click', () => {
        setStatus(t('tag.saving'), true);
        const sentTags = state.tags.map((t) => ({ ...t }));
        const xhr = new XMLHttpRequest();
        xhr.open('POST', tagsApiUrl(state.entityType), true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
          let data;
          try {
            data = JSON.parse(xhr.responseText);
          } catch {
            setStatus('Invalid response', false);
            return;
          }
          if (!data.ok) {
            setStatus(data.hint || data.error || 'Save failed', false);
            return;
          }
          state.tags = data.tags || [];
          renderAll();
          const adjusted = tagsJson(sentTags) !== tagsJson(state.tags);
          setStatus(adjusted ? 'Saved (type/status tags normalized)' : 'Saved', true);
          if (cfg.onSaved) cfg.onSaved(state.tags, state.entityId);
        };
        xhr.onerror = function () {
          setStatus(t('reports.network_error'), false);
        };
        xhr.send(JSON.stringify({ id: state.entityId, tags: state.tags }));
      });
    }

    function loadFromApi() {
      const xhr = new XMLHttpRequest();
      xhr.open('GET', tagsApiUrl(state.entityType) + '?id=' + state.entityId, true);
      xhr.onload = function () {
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          return;
        }
        if (data.ok) {
          state.tags = data.tags || [];
          state.workflow = data.workflow || [];
          state.presets = data.presets || [];
          renderAll();
          renderPresets();
        }
      };
      xhr.send();
    }

    renderAll();
    if (cfg.fetchPresets) loadFromApi();

    return {
      setTags(tags) {
        state.tags = tags || [];
        renderAll();
      },
      getTags() {
        return state.tags;
      },
      reload() {
        loadFromApi();
      },
    };
  }

  let tagModalEditor = null;

  function openTagModal(entityId, entityType) {
    const modal = document.getElementById('tag-modal');
    const editorEl = document.getElementById('tag-editor-modal');
    if (!modal || !editorEl || !canTagEdit()) return;
    const et = entityType === 'ticket' ? 'ticket' : 'report';
    editorEl.setAttribute('data-report-id', et === 'report' ? String(entityId) : '0');
    editorEl.setAttribute('data-ticket-id', et === 'ticket' ? String(entityId) : '0');
    editorEl.setAttribute('data-entity-type', et);
    modal.hidden = false;
    modal.setAttribute('aria-hidden', 'false');
    tagModalEditor = createTagEditor({
      entityId: Number(entityId),
      entityType: et,
      initialTags: [],
      fetchPresets: true,
      elements: {
        list: document.getElementById('tag-modal-list'),
        preview: document.getElementById('tag-modal-preview'),
        form: document.getElementById('tag-modal-add'),
        presetBar: document.getElementById('tag-modal-presets'),
        presetChips: document.getElementById('tag-modal-preset-chips'),
        saveBtn: document.getElementById('tag-modal-save'),
        status: document.getElementById('tag-modal-status'),
      },
      onSaved() {
        if (typeof reportsReloadRef === 'function') reportsReloadRef();
        if (typeof window.ticketsReloadRef === 'function') window.ticketsReloadRef();
      },
    });
  }
  window.openTagModal = openTagModal;

  function closeTagModal() {
    const modal = document.getElementById('tag-modal');
    if (!modal) return;
    modal.hidden = true;
    modal.setAttribute('aria-hidden', 'true');
    tagModalEditor = null;
  }

  let reportsReloadRef = null;

  function initTagModal() {
    document.querySelectorAll('[data-tag-modal-close]').forEach((el) => {
      el.addEventListener('click', closeTagModal);
    });
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') closeTagModal();
    });
  }

  function initTagEditorReadOnly() {
    const preview = document.getElementById('tag-editor-preview');
    const dataEl =
      document.getElementById('ticket-custom-tags-data') ||
      document.getElementById('report-custom-tags-data');
    if (!preview || !dataEl || canTagEdit()) return;
    try {
      const tags = JSON.parse(dataEl.textContent || '[]');
      preview.innerHTML = tags.length
        ? tags.map((t) => renderTagPill(t, false)).join('')
        : '<span class="muted">No custom tags</span>';
    } catch {
      preview.innerHTML = '<span class="muted">—</span>';
    }
  }

  function initTagEditorDetail() {
    const root = document.getElementById('tag-editor');
    if (!root) return;
    const entityType = root.getAttribute('data-entity-type') === 'ticket' ? 'ticket' : 'report';
    const entityId = Number(
      root.getAttribute(entityType === 'ticket' ? 'data-ticket-id' : 'data-report-id')
    );
    const canEdit = entityType === 'ticket' ? !!root.querySelector('#tag-editor-list') : canTagEdit();
    if (!canEdit) {
      initTagEditorReadOnly();
      return;
    }
    let initial = [];
    const dataEl =
      document.getElementById('ticket-custom-tags-data') ||
      document.getElementById('report-custom-tags-data');
    if (dataEl) {
      try {
        initial = JSON.parse(dataEl.textContent || '[]');
      } catch { /* ignore */ }
    }
    createTagEditor({
      entityId,
      entityType,
      initialTags: initial,
      fetchPresets: true,
      elements: {
        list: document.getElementById('tag-editor-list'),
        preview: document.getElementById('tag-editor-preview'),
        form: document.getElementById('tag-editor-add'),
        presetBar: document.getElementById('tag-preset-bar'),
        presetChips: document.getElementById('tag-preset-chips'),
        saveBtn: document.getElementById('tag-editor-save'),
        status: document.getElementById('tag-editor-status'),
      },
    });
  }

  function initTicketDetail() {
    const id = document.body.getAttribute('data-ticket-id');
    if (!id) return;
    const tree = document.getElementById('detail-tree');
    if (tree) bindDetailTree(tree);

    let assignees = [];
    const assigneeDataEl = document.getElementById('ticket-assignees-data');
    if (assigneeDataEl) {
      try {
        assignees = JSON.parse(assigneeDataEl.textContent || '[]');
        if (!Array.isArray(assignees)) assignees = [];
      } catch {
        assignees = [];
      }
    }

    const assigneeEditor = document.getElementById('assignee-editor');
    const assigneePick = document.getElementById('assignee-user-pick');
    const assigneeAddBtn = document.getElementById('assignee-add-btn');

    function renderAssignees() {
      if (!assigneeEditor) return;
      if (!assignees.length) {
        assigneeEditor.innerHTML = '<span class="muted">—</span>';
        return;
      }
      assigneeEditor.innerHTML = assignees
        .map(function (a, idx) {
          const role = a.role ? ' <span class="muted">(' + escapeHtml(a.role) + ')</span>' : '';
          return (
            '<span class="assignee-pill">' +
            escapeHtml(a.username) +
            role +
            ' <button type="button" class="assignee-remove" data-idx="' +
            idx +
            '" aria-label="Remove">×</button></span>'
          );
        })
        .join('');
      assigneeEditor.querySelectorAll('.assignee-remove').forEach(function (btn) {
        btn.addEventListener('click', function () {
          const i = Number(btn.getAttribute('data-idx'));
          assignees.splice(i, 1);
          renderAssignees();
        });
      });
    }

    function loadUsersForAssignees() {
      if (!assigneePick) return;
      const xhr = new XMLHttpRequest();
      xhr.open('GET', basePath() + '/api/users.php', true);
      xhr.onload = function () {
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          return;
        }
        if (!data.ok || !Array.isArray(data.users)) return;
        data.users.forEach(function (u) {
          const opt = document.createElement('option');
          opt.value = u.username;
          opt.textContent = u.username + (u.role ? ' (' + u.role + ')' : '');
          opt.dataset.role = u.role || '';
          assigneePick.appendChild(opt);
        });
      };
      xhr.send();
    }

    if (assigneeEditor) {
      renderAssignees();
      loadUsersForAssignees();
      if (assigneeAddBtn && assigneePick) {
        assigneeAddBtn.addEventListener('click', function () {
          const username = assigneePick.value;
          if (!username) return;
          const role = assigneePick.selectedOptions[0]?.dataset.role || '';
          if (assignees.some(function (a) { return a.username === username; })) return;
          assignees.push({ username: username, role: role });
          renderAssignees();
        });
      }
    }

    const attachmentList = document.getElementById('ticket-attachment-list');
    const linkForm = document.getElementById('ticket-link-form');
    const fileForm = document.getElementById('ticket-file-form');
    const linkStatus = document.getElementById('ticket-link-status');
    const fileStatus = document.getElementById('ticket-file-status');

    function renderAttachmentItem(att) {
      const li = document.createElement('li');
      li.className = 'ticket-attachment ticket-attachment--' + (att.kind || 'file');
      li.dataset.attachmentId = String(att.id);
      let main = '';
      const label = escapeHtml(att.provider_label || 'File');
      if (att.kind === 'link' && att.external_url) {
        main =
          '<a href="' +
          escapeHtml(att.external_url) +
          '" target="_blank" rel="noopener noreferrer">' +
          escapeHtml(att.title || att.external_url) +
          '</a>';
      } else if (att.download_url) {
        const size =
          att.size_bytes > 0
            ? ' <span class="muted">(' + (att.size_bytes / 1024).toFixed(1) + ' KB)</span>'
            : '';
        main =
          '<a href="' +
          escapeHtml(att.download_url) +
          '">' +
          escapeHtml(att.title || att.original_name || 'file') +
          '</a>' +
          size;
      } else {
        main = '<span>' + escapeHtml(att.title || '') + '</span>';
      }
      li.innerHTML =
        '<span class="attachment-provider">' +
        label +
        '</span> ' +
        main +
        ' <span class="muted attachment-meta">' +
        escapeHtml(att.created_by || '') +
        '</span>' +
        ' <button type="button" class="btn btn-small attachment-remove" data-id="' +
        att.id +
        '">×</button>';
      li.querySelector('.attachment-remove').addEventListener('click', function () {
        if (!confirm('Remove attachment?')) return;
        const xhr = new XMLHttpRequest();
        xhr.open('DELETE', basePath() + '/api/ticket_attachments.php?id=' + att.id, true);
        xhr.onload = function () {
          let data;
          try {
            data = JSON.parse(xhr.responseText);
          } catch {
            return;
          }
          if (data.ok) li.remove();
        };
        xhr.send();
      });
      return li;
    }

    if (linkForm) {
      linkForm.addEventListener('submit', function (e) {
        e.preventDefault();
        const fd = new FormData(linkForm);
        const url = String(fd.get('url') || '').trim();
        if (!url) return;
        if (linkStatus) linkStatus.textContent = 'Saving…';
        const xhr = new XMLHttpRequest();
        xhr.open('POST', basePath() + '/api/ticket_attachments.php', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
          let data;
          try {
            data = JSON.parse(xhr.responseText);
          } catch {
            if (linkStatus) linkStatus.textContent = 'Invalid response';
            return;
          }
          if (!data.ok) {
            if (linkStatus) linkStatus.textContent = data.error || 'Failed';
            return;
          }
          if (attachmentList && data.attachment) {
            attachmentList.appendChild(renderAttachmentItem(data.attachment));
          }
          linkForm.reset();
          if (linkStatus) linkStatus.textContent = 'Attached';
        };
        xhr.onerror = function () {
          if (linkStatus) linkStatus.textContent = t('reports.network_error');
        };
        xhr.send(
          JSON.stringify({
            ticket_id: Number(id),
            title: fd.get('title'),
            url: url,
          })
        );
      });
    }

    if (fileForm) {
      fileForm.addEventListener('submit', function (e) {
        e.preventDefault();
        const fd = new FormData(fileForm);
        fd.append('ticket_id', id);
        if (fileStatus) fileStatus.textContent = 'Uploading…';
        const xhr = new XMLHttpRequest();
        xhr.open('POST', basePath() + '/api/ticket_attachments.php', true);
        xhr.onload = function () {
          let data;
          try {
            data = JSON.parse(xhr.responseText);
          } catch {
            if (fileStatus) fileStatus.textContent = 'Invalid response';
            return;
          }
          if (!data.ok) {
            if (fileStatus) fileStatus.textContent = data.error || 'Failed';
            return;
          }
          if (attachmentList && data.attachment) {
            attachmentList.appendChild(renderAttachmentItem(data.attachment));
          }
          fileForm.reset();
          if (fileStatus) fileStatus.textContent = 'Uploaded';
        };
        xhr.onerror = function () {
          if (fileStatus) fileStatus.textContent = t('reports.network_error');
        };
        xhr.send(fd);
      });
    }

    if (attachmentList) {
      attachmentList.querySelectorAll('.attachment-remove').forEach(function (btn) {
        btn.addEventListener('click', function () {
          const attId = btn.getAttribute('data-id');
          if (!confirm('Remove attachment?')) return;
          const xhr = new XMLHttpRequest();
          xhr.open('DELETE', basePath() + '/api/ticket_attachments.php?id=' + attId, true);
          xhr.onload = function () {
            let data;
            try {
              data = JSON.parse(xhr.responseText);
            } catch {
              return;
            }
            if (data.ok) btn.closest('.ticket-attachment')?.remove();
          };
          xhr.send();
        });
      });
    }

    const commentForm = document.getElementById('ticket-comment-form');
    const commentStatus = document.getElementById('ticket-comment-status');
    const commentList = document.getElementById('ticket-comment-list');
    if (commentForm) {
      commentForm.addEventListener('submit', function (e) {
        e.preventDefault();
        const fd = new FormData(commentForm);
        const bodyText = String(fd.get('body') || '').trim();
        if (!bodyText) return;
        if (commentStatus) commentStatus.textContent = 'Posting…';
        const xhr = new XMLHttpRequest();
        xhr.open('POST', basePath() + '/api/ticket_comments.php', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = function () {
          let data;
          try {
            data = JSON.parse(xhr.responseText);
          } catch {
            if (commentStatus) commentStatus.textContent = 'Invalid response';
            return;
          }
          if (!data.ok) {
            if (commentStatus) commentStatus.textContent = data.error || 'Failed';
            return;
          }
          const c = data.comment;
          if (commentList && c) {
            const li = document.createElement('li');
            li.className = 'ticket-comment';
            const when = c.created_at_ms
              ? new Date(Number(c.created_at_ms)).toISOString().replace('T', ' ').slice(0, 19)
              : '';
            li.innerHTML =
              '<div class="ticket-comment-meta"><strong>' +
              escapeHtml(c.author_username) +
              '</strong> <span class="muted">' +
              escapeHtml(when) +
              '</span></div><pre class="ticket-comment-body"></pre>';
            li.querySelector('.ticket-comment-body').textContent = c.body;
            commentList.appendChild(li);
          }
          commentForm.reset();
          if (commentStatus) commentStatus.textContent = 'Posted';
        };
        xhr.onerror = function () {
          if (commentStatus) commentStatus.textContent = t('reports.network_error');
        };
        xhr.send(JSON.stringify({ ticket_id: Number(id), body: bodyText }));
      });
    }

    const form = document.getElementById('ticket-edit-form');
    if (!form) return;
    const statusEl = document.getElementById('ticket-save-status');
    const ratingSel = form.querySelector('select[name="rating"]');
    const starsPreview = form.querySelector('.ticket-stars-preview .star-rating');
    if (ratingSel && starsPreview) {
      ratingSel.addEventListener('change', () => {
        const n = Math.max(0, Math.min(5, Number(ratingSel.value) || 0));
        let html = '';
        for (let i = 1; i <= 5; i++) {
          html += '<span class="star' + (i <= n ? ' star--on' : '') + '"></span>';
        }
        starsPreview.innerHTML = html;
      });
    }
    form.addEventListener('submit', (e) => {
      e.preventDefault();
      if (statusEl) statusEl.textContent = 'Saving…';
      const fd = new FormData(form);
      const body = {
        id: Number(form.getAttribute('data-ticket-id')),
        title: fd.get('title'),
        brief: fd.get('brief'),
        body: fd.get('body'),
        owner_username: fd.get('owner_username'),
        verified_by_username: fd.get('verified_by_username'),
        rating: Number(fd.get('rating')),
        workflow_state: fd.get('workflow_state'),
        assignees: assignees,
      };
      const xhr = new XMLHttpRequest();
      xhr.open('PUT', basePath() + '/api/ticket_update.php', true);
      xhr.setRequestHeader('Content-Type', 'application/json');
      xhr.onload = function () {
        let data;
        try {
          data = JSON.parse(xhr.responseText);
        } catch {
          if (statusEl) statusEl.textContent = 'Invalid response';
          return;
        }
        if (!data.ok) {
          if (statusEl) statusEl.textContent = data.error || 'Save failed';
          return;
        }
        if (statusEl) statusEl.textContent = 'Saved';
      };
      xhr.onerror = function () {
        if (statusEl) statusEl.textContent = t('reports.network_error');
      };
      xhr.send(JSON.stringify(body));
    });
  }

  function initTagFilterBar(listContext) {
    const chipsEl = document.getElementById('tag-filter-chips');
    const modeWrap = document.getElementById('tag-mode-wrap');
    const modeSel = document.getElementById('tag-mode-select');
    if (!chipsEl) return;

    function renderFilterChips(workflow) {
      const active = listContext.filterTags || [];
      chipsEl.innerHTML = (workflow || [])
        .map((tag) => {
          const on = active.includes(tag.id);
          return (
            '<a class="tag-filter-chip' +
            (on ? ' tag-filter-chip--active' : '') +
            '" href="' +
            escapeHtml(reportsUrlWithTagToggle(tag.id, listContext.tagMode)) +
            '" style="--tag-bg:' +
            escapeHtml(tag.bg) +
            '">' +
            escapeHtml(tag.label) +
            '</a>'
          );
        })
        .join('');
    }

    if (modeSel) {
      modeSel.value = listContext.tagMode === 'or' ? 'or' : 'and';
      modeSel.addEventListener('change', () => {
        if (!(listContext.filterTags && listContext.filterTags.length)) return;
        const p = new URLSearchParams(window.location.search);
        p.set('view', 'reports');
        p.set('tag_mode', modeSel.value === 'or' ? 'or' : 'and');
        p.delete('page');
        window.location.href = basePath() + '/?' + p.toString();
      });
    }

    const xhr = new XMLHttpRequest();
    xhr.open('GET', basePath() + '/api/tag_catalog.php', true);
    xhr.onload = function () {
      let data;
      try {
        data = JSON.parse(xhr.responseText);
      } catch {
        return;
      }
      if (!data.ok) return;
      renderFilterChips(data.workflow || []);
      if (modeWrap) {
        modeWrap.hidden = !(listContext.filterTags && listContext.filterTags.length);
      }
    };
    xhr.send();
  }

  function initTagEditButtons(tbody, reloadFn) {
    reportsReloadRef = reloadFn;
    if (!tbody) return;
    tbody.addEventListener('click', (e) => {
      const btn = e.target.closest('.tag-edit-btn');
      if (!btn) return;
      e.stopPropagation();
      e.preventDefault();
      openTagModal(Number(btn.getAttribute('data-report-id')), 'report');
    });
  }

  function boot() {
    initTheme();
    initNav();
    initTagModal();
    initReportDetail();
    initTicketDetail();
    initTagEditorDetail();
    initReportsApp();
  }

  function onReady(fn) {
    const run = () => {
      if (window.CrashI18n && window.CrashI18n.ready) {
        window.CrashI18n.ready.then(fn);
      } else {
        fn();
      }
    };
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', run);
    } else {
      run();
    }
  }

  onReady(boot);
})();
