/**
 * accessmate.ai frontend overlay — axe-core powered, no framework.
 *
 * Two modes:
 *  1. Interactive  — slide-in panel for the current page (admin bar button)
 *  2. Auto-scan    — headless iframe scan triggered by PageScanner admin view
 *                    (activated when ?aai_scan=1 is in the URL)
 */

import axe from 'axe-core';

// ── Types ─────────────────────────────────────────────────────

interface OverlayConfig {
  nonce: string;
  restBase: string;
  pageUrl: string;
}

interface AaiIssueNode {
  element: string;
  selector: string;
}

interface AaiIssue {
  id: string;
  rule: string;
  wcag: string;
  severity: 'critical' | 'warning' | 'notice';
  title: string;
  description: string;
  element: string;
  selector: string;
  nodes: AaiIssueNode[];
  count: number;
  fixed: boolean;
  ai_fixable: boolean;
  help_url: string;  // axe helpUrl — always a valid Deque University link
}

interface ScanPayload {
  url: string;
  score: number;
  grade: string;
  passed: number;
  failed: number;
  issues: AaiIssue[];
}

declare global {
  interface Window {
    accessmateOverlayConfig: OverlayConfig;
    accessmateOverlay: { open(): void; close(): void };
  }
}

// ── Bootstrap ─────────────────────────────────────────────────

(function () {
  const cfg: OverlayConfig = window.accessmateOverlayConfig;
  if (!cfg) return;

  // Any non-empty aai_scan value means headless iframe mode.
  // The value is used as a scanId so the parent can match the response
  // without fragile URL comparison (URLs may differ in trailing slash, etc.).
  const scanId      = new URLSearchParams(window.location.search).get('aai_scan') ?? '';
  const autoScan    = new URLSearchParams(window.location.search).get('accessmate_scan') ?? '';

  if (scanId) {
    // Headless iframe mode — scan silently, store results, signal parent.
    runAutoScan(cfg, scanId);
  } else {
    // Interactive mode — slide-in panel for the current page.
    buildOverlay(cfg);

    // If accessmate_scan param is present, auto-open and start scanning immediately.
    if (autoScan) {
      requestAnimationFrame(() => window.accessmateOverlay?.open());
    }
  }
})();

// ── axe helpers ───────────────────────────────────────────────

/** Run axe-core against the whole document (WCAG 2.x A + AA). */
async function runAxe(): Promise<axe.AxeResults> {
  return axe.run(
    { include: [['html']], exclude: [['#wpadminbar'], ['#aai-ov-host']] },
    { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] }, resultTypes: ['violations', 'passes'] },
  );
}

/** Transform axe violations into our simplified Issue format. */
function axeToIssues(violations: axe.Result[]): AaiIssue[] {
  const aiFixableRules = new Set([
    'image-alt', 'label', 'color-contrast', 'link-name',
    'button-name', 'input-image-alt', 'area-alt',
  ]);

  return violations.map((v) => {
    // Extract WCAG number from tags like 'wcag143' → '1.4.3'
    const wcagTag = v.tags.find((t) => /^wcag\d{3,}$/.test(t));
    let wcag = '';
    if (wcagTag) {
      const m = wcagTag.match(/^wcag(\d)(\d)(\d+)$/);
      if (m) wcag = `${m[1]}.${m[2]}.${m[3]}`;
    }

    const severity: AaiIssue['severity'] =
      v.impact === 'critical' || v.impact === 'serious' ? 'critical' :
      v.impact === 'moderate' ? 'warning' : 'notice';

    const firstNode = v.nodes[0];
    const nodes = v.nodes.map((n) => ({
      element:  n.html ?? '',
      selector: n.target?.join(', ') ?? '',
    }));

    return {
      id:          `${v.id}_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
      rule:        v.id,
      wcag,
      severity,
      title:       v.help,
      description: v.description,
      element:     firstNode?.html ?? '',
      selector:    firstNode?.target?.join(', ') ?? '',
      nodes,
      count:       v.nodes.length,
      fixed:       false,
      ai_fixable:  aiFixableRules.has(v.id),
      help_url:    v.helpUrl ?? '',
    };
  });
}

function calcScore(issues: AaiIssue[]): number {
  if (!issues.length) return 100;
  let deductions = 0;
  for (const iss of issues) {
    const w = iss.severity === 'critical' ? 10 : 5;
    deductions += Math.min(w * iss.count, 25);
  }
  return Math.max(0, 100 - deductions);
}

function calcGrade(score: number): string {
  return score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 55 ? 'C' : score >= 35 ? 'D' : 'F';
}

function scoreColor(s: number): string {
  return s >= 90 ? '#22c55e' : s >= 75 ? '#84cc16' : s >= 55 ? '#f59e0b' : s >= 35 ? '#f97316' : '#ef4444';
}

/** POST computed scan payload to the /axe-scan REST endpoint. */
async function storeScan(cfg: OverlayConfig, payload: ScanPayload): Promise<void> {
  await fetch(`${cfg.restBase}/axe-scan`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce },
    body: JSON.stringify(payload),
  });
}

// ── Auto-scan mode (iframe) ───────────────────────────────────

async function runAutoScan(cfg: OverlayConfig, scanId: string): Promise<void> {
  // Wait for full page render before running axe.
  await new Promise<void>((resolve) => {
    if (document.readyState === 'complete') resolve();
    else window.addEventListener('load', () => resolve(), { once: true });
  });

  // Small delay so JS-rendered content settles.
  await new Promise((r) => setTimeout(r, 800));

  try {
    const results = await runAxe();
    const issues  = axeToIssues(results.violations);
    const score   = calcScore(issues);
    const grade   = calcGrade(score);
    const payload: ScanPayload = {
      url:    cfg.pageUrl,
      score,
      grade,
      passed: results.passes.length,
      failed: results.violations.length,
      issues,
    };

    await storeScan(cfg, payload);

    window.parent.postMessage(
      { type: 'aai-scan-complete', scanId, url: cfg.pageUrl, score, grade, failed: results.violations.length },
      window.location.origin,
    );
  } catch (err: unknown) {
    window.parent.postMessage(
      { type: 'aai-scan-error', scanId, url: cfg.pageUrl, error: String(err) },
      window.location.origin,
    );
  }
}

// ── Interactive overlay UI ────────────────────────────────────

// ── Element highlight helpers ─────────────────────────────────

function injectHighlightStyle(): void {
  if (document.getElementById('aai-hl-style')) return;
  const s = document.createElement('style');
  s.id = 'aai-hl-style';
  s.textContent =
    '.aai-hl{outline:3px solid #3858e9!important;outline-offset:3px!important;' +
    'box-shadow:0 0 0 8px rgba(56,88,233,.18)!important;scroll-margin-top:80px!important}' +
    '@keyframes aai-hl-pulse{0%{box-shadow:0 0 0 4px rgba(56,88,233,.6)!important}' +
    '100%{box-shadow:0 0 0 18px rgba(56,88,233,0)!important}}' +
    '.aai-hl--pulse{animation:aai-hl-pulse .7s ease-out!important}';
  document.head.appendChild(s);
}

let _hlEl: Element | null = null;
let _hlTimer = 0;

function highlightEl(selector: string, scroll = false): void {
  clearHl();
  if (!selector) return;
  try {
    const el = document.querySelector(selector);
    if (!el) return;
    el.classList.add('aai-hl');
    _hlEl = el;
    if (scroll) {
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.remove('aai-hl--pulse');
      void (el as HTMLElement).offsetWidth;
      el.classList.add('aai-hl--pulse');
      _hlTimer = window.setTimeout(() => el.classList.remove('aai-hl--pulse'), 800);
    }
  } catch { /* invalid selector — ignore */ }
}

function clearHl(): void {
  clearTimeout(_hlTimer);
  if (_hlEl) { _hlEl.classList.remove('aai-hl', 'aai-hl--pulse'); _hlEl = null; }
}

// ── Overlay UI ────────────────────────────────────────────────

function buildOverlay(cfg: OverlayConfig): void {
  const PANEL_W = 380;
  const adminBarH = (document.getElementById('wpadminbar') as HTMLElement | null)?.offsetHeight ?? 0;

  injectHighlightStyle();

  // Smooth page-push transition — applied once to html element
  document.documentElement.style.transition = `margin-right .28s cubic-bezier(.4,0,.2,1)`;

  // ── Shadow host ───────────────────────────────────────────────
  const host = document.createElement('div');
  host.id = 'aai-ov-host';
  host.style.cssText = `position:fixed;top:${adminBarH}px;right:0;width:${PANEL_W}px;height:calc(100dvh - ${adminBarH}px);z-index:2147483648;pointer-events:none`;
  document.body.appendChild(host);
  const shadow = host.attachShadow({ mode: 'open' });

  // ── Styles ─────────────────────────────────────────────────
  const style = document.createElement('style');
  style.textContent = [
    ':host{display:block}',
    '*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}',
    'button{all:unset;display:inline-flex;align-items:center;cursor:pointer;letter-spacing:normal;line-height:normal;text-transform:none;word-spacing:normal;font:inherit}',
    'a{all:unset;cursor:pointer}',
    /* Panel fills host — host is fixed, panel slides in from right */
    '#aai-ov{position:absolute;inset:0;background:#fff;display:flex;flex-direction:column;pointer-events:none;',
    'box-shadow:-4px 0 28px rgba(0,0,0,.18);',
    'transform:translateX(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);',
    'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;color:#111827;',
    'letter-spacing:normal;word-spacing:normal;text-transform:none;line-height:1.5}',
    '#aai-ov.open{transform:translateX(0);pointer-events:auto}',
    '#aai-ov-hd{padding:14px 18px;background:#3858e9;color:#fff;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}',
    '#aai-ov-hd h2{margin:0;font-size:14px;font-weight:600;display:flex;align-items:center;gap:6px}',
    '#aai-ov-cls{color:#fff;font-size:22px;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px}',
    '#aai-ov-cls:hover{background:rgba(255,255,255,.15)}',
    '#aai-ov-body{flex:1;overflow:hidden;position:relative}',
    '#aai-ov-ft{padding:10px 16px;padding-bottom:max(10px,env(safe-area-inset-bottom));border-top:1px solid #e5e7eb;flex-shrink:0}',
    /* list / detail view panes — slide transition */
    '#aai-ov-list-view{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;padding:14px 16px;',
    'transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1)}',
    '#aai-ov-list-view.slide-out{transform:translateX(-100%)}',
    '#aai-ov-detail-view{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;padding:14px 16px;',
    'transform:translateX(100%);transition:transform .25s cubic-bezier(.4,0,.2,1)}',
    '#aai-ov-detail-view.slide-in{transform:translateX(0)}',
    /* back button */
    '#aai-ov-back{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:#6b7280;margin-bottom:14px;cursor:pointer;padding:4px 0}',
    '#aai-ov-back:hover{color:#3858e9}',
    /* detail badge row */
    '.aai-ov-d-badge-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}',
    '.aai-ov-d-badge{display:inline-block;padding:2px 10px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase}',
    '.aai-ov-d-badge.critical{background:#fee2e2;color:#dc2626}',
    '.aai-ov-d-badge.warning{background:#fef3c7;color:#d97706}',
    '.aai-ov-d-badge.notice{background:#dbeafe;color:#2563eb}',
    '.aai-ov-d-wcag{font-size:11px;color:#6b7280;background:#f3f4f6;padding:2px 8px;border-radius:10px}',
    /* detail body */
    '#aai-ov-detail-title{font-size:15px;font-weight:700;line-height:1.3;margin-bottom:8px;color:#111827}',
    '#aai-ov-detail-desc{font-size:13px;color:#374151;line-height:1.55;margin-bottom:14px}',
    '.aai-ov-d-section{margin-bottom:12px}',
    '.aai-ov-d-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#9ca3af;margin-bottom:4px}',
    '.aai-ov-d-code{display:block;background:#f3f4f6;padding:6px 8px;border-radius:4px;font-size:12px;font-family:ui-monospace,SFMono-Regular,monospace;word-break:break-all;color:#374151}',
    '.aai-ov-d-pre{display:block;background:#1e1e2e;color:#cdd6f4;padding:8px 10px;border-radius:6px;font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0}',
    '#aai-ov-detail-learn{display:inline-block;font-size:12px;color:#3858e9;margin-bottom:14px}',
    '#aai-ov-detail-learn:hover{text-decoration:underline}',
    '.aai-ov-d-actions{display:flex;gap:8px;padding-top:14px;border-top:1px solid #e5e7eb;flex-wrap:wrap}',
    '#aai-ov-detail-fix{padding:8px 14px;background:#3858e9;color:#fff;border-radius:6px;font-size:13px;font-weight:500;transition:background .15s}',
    '#aai-ov-detail-fix:hover{background:#2642c7}',
    '#aai-ov-detail-fix:disabled{opacity:.6;cursor:not-allowed}',
    '#aai-ov-detail-explain{padding:8px 14px;background:#fff;color:#3858e9;border:1.5px solid #3858e9;border-radius:6px;font-size:13px;font-weight:500;transition:all .15s}',
    '#aai-ov-detail-explain:hover{background:#eef2ff}',
    '#aai-ov-detail-explain:disabled{opacity:.6;cursor:not-allowed}',
    '#aai-ov-detail-ai-out{margin-top:12px;padding:12px;background:#f8f7ff;border:1px solid #ddd6fe;border-radius:6px;font-size:12px;line-height:1.55;color:#374151}',
    '.aai-ov-ai-err{color:#ef4444;font-size:12px}',
    '.aai-ov-score{display:flex;align-items:center;gap:12px;margin-bottom:14px;padding:12px;background:#f9fafb;border-radius:8px}',
    '.aai-ov-grade{font-size:26px;font-weight:700;width:48px;height:48px;border-radius:50%;',
    'display:flex;align-items:center;justify-content:center;color:#fff;flex-shrink:0}',
    '.aai-ov-sv{font-size:22px;font-weight:700;line-height:1.1}',
    '.aai-ov-sv small{font-size:13px;font-weight:400;color:#6b7280}',
    '.aai-ov-sc{font-size:12px;color:#6b7280;margin-top:3px}',
    '.aai-ov-url{font-size:11px;color:#9ca3af;margin-bottom:14px;word-break:break-all}',
    /* skeleton loader */
    '@keyframes aai-shimmer{0%{background-position:-400px 0}100%{background-position:400px 0}}',
    '.aai-ov-skel{background:linear-gradient(90deg,#f0f0f0 25%,#e8e8e8 50%,#f0f0f0 75%);',
    'background-size:400px 100%;animation:aai-shimmer 1.4s ease-in-out infinite;border-radius:4px}',
    '.aai-ov-skel-score{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding:12px;background:#f9fafb;border-radius:8px}',
    '.aai-ov-skel-circle{width:48px;height:48px;border-radius:50%;flex-shrink:0}',
    '.aai-ov-skel-lines{flex:1;display:flex;flex-direction:column;gap:7px}',
    '.aai-ov-skel-card{border-left:3px solid #e5e7eb;margin-bottom:8px;border-radius:0 6px 6px 0;',
    'padding:10px 12px;background:rgba(0,0,0,.018);display:flex;flex-direction:column;gap:7px}',
    /* card entry animation */
    '@keyframes aai-card-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
    '.aai-ov-issue{animation:aai-card-in .22s ease-out both}',
    '.aai-ov-pass-item{animation:aai-card-in .22s ease-out both}',
    /* issue cards — backend-matching design */
    '.aai-ov-issue{border-left:3px solid #e5e7eb;margin-bottom:8px;border-radius:8px;display:flex;align-items:center;gap:10px;padding:12px 14px;cursor:pointer;transition:background .15s}',
    '.aai-ov-issue.critical{border-color:#ef4444;background:#fff5f5}',
    '.aai-ov-issue.warning{border-color:#f59e0b;background:#fffbeb}',
    '.aai-ov-issue.notice{border-color:#3b82f6;background:#eff6ff}',
    '.aai-ov-issue:hover{filter:brightness(.97)}',
    '.aai-ov-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}',
    '.aai-ov-dot.critical{background:#ef4444}',
    '.aai-ov-dot.warning{background:#f59e0b}',
    '.aai-ov-dot.notice{background:#3b82f6}',
    '.aai-ov-issue-text{flex:1;min-width:0}',
    '.aai-ov-issue-text strong{display:block;font-size:13px;font-weight:600;line-height:1.3}',
    '.aai-ov-meta{display:block;font-size:12px;color:#6b7280;margin-top:2px}',
    '.aai-ov-details-btn{flex-shrink:0;padding:4px 12px;border:1.5px solid #3858e9;color:#3858e9;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;background:#fff;transition:all .15s;white-space:nowrap}',
    '.aai-ov-details-btn:hover{background:#3858e9;color:#fff}',
    /* filters */
    '.aai-ov-filter{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap}',
    '.aai-ov-filter-btn{padding:3px 10px;border:1px solid #d1d5db;border-radius:20px;background:#f3f4f6;color:#374151;font-size:12px;cursor:pointer;transition:all .15s}',
    '.aai-ov-filter-btn:hover{border-color:#3858e9;color:#3858e9;background:#eaedfc}',
    '.aai-ov-filter-btn.active{background:#3858e9;color:#fff;border-color:#3858e9}',
    '.aai-ov-filter-btn--pass{background:#f0fdf4;border-color:#86efac;color:#15803d}',
    '.aai-ov-filter-btn--pass:hover{background:#dcfce7;border-color:#22c55e;color:#15803d}',
    '.aai-ov-filter-btn--pass.active{background:#22c55e;border-color:#22c55e;color:#fff}',
    /* passed items */
    '.aai-ov-pass-item{animation:aai-card-in .2s ease-out both;border-left:3px solid #22c55e;margin-bottom:6px;border-radius:0 6px 6px 0;overflow:hidden;cursor:pointer}',
    '.aai-ov-pass-item-hd{display:flex;align-items:flex-start;gap:8px;padding:8px 10px;background:#f0fdf4;user-select:none}',
    '.aai-ov-pass-item-hd-text{flex:1}',
    '.aai-ov-pass-item-hd strong{display:block;font-size:12px;font-weight:600;color:#166534;line-height:1.3}',
    '.aai-ov-pass-item-meta{display:block;font-size:11px;color:#4ade80;margin-top:1px}',
    '.aai-ov-pass-item-body{display:none;padding:8px 10px;border-top:1px solid #bbf7d0;background:#fff;font-size:12px;color:#374151}',
    '.aai-ov-pass-item.open .aai-ov-pass-item-body{display:block}',
    '.aai-ov-pass-item.open .aai-ov-chevron::before{content:"▲"}',
    '.aai-ov-pass-item:not(.open) .aai-ov-chevron::before{content:"▼"}',
    /* misc */
    '.aai-ov-msg{color:#6b7280;font-size:13px;text-align:center;padding:24px 0}',
    '.aai-ov-ok{color:#22c55e;font-size:13px;text-align:center;padding:24px 0}',
    '.aai-ov-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:9px 16px;background:#3858e9;color:#fff;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;width:100%;transition:background .15s}',
    '.aai-ov-btn:hover{background:#2642c7}',
    '.aai-ov-btn:disabled{opacity:.6;cursor:not-allowed}',
    '.aai-ov-pre{display:block;background:#1e1e2e;color:#cdd6f4;padding:8px 10px;border-radius:4px;font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;overflow-x:auto;white-space:pre-wrap;word-break:break-word;margin:0}',
    '@keyframes aai-spin{to{transform:rotate(360deg)}}',
    '.aai-ov-spin{display:inline-block;width:12px;height:12px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:aai-spin .7s linear infinite;vertical-align:middle;margin-right:5px}',
    /* affected-elements accordion */
    '.aai-ov-node-list{display:flex;flex-direction:column;gap:6px;margin-top:6px}',
    '.aai-ov-node-item{border:1.5px solid #e5e7eb;border-radius:8px;overflow:hidden;transition:border-color .15s}',
    '.aai-ov-node-item.open{border-color:var(--nc,#ef4444)}',
    '.aai-ov-node-hd{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:pointer;background:#fff;user-select:none}',
    '.aai-ov-node-hd:hover{background:#fafafa}',
    '.aai-ov-node-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;background:var(--nc,#ef4444)}',
    '.aai-ov-node-sel{flex:1;font-size:12px;font-family:ui-monospace,SFMono-Regular,monospace;color:#374151;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}',
    '.aai-ov-node-chevron{font-size:13px;color:#9ca3af;flex-shrink:0;transition:transform .2s;line-height:1}',
    '.aai-ov-node-item.open .aai-ov-node-chevron{transform:rotate(90deg)}',
    '.aai-ov-node-body{display:none;border-top:1px solid #e5e7eb}',
    '.aai-ov-node-item.open .aai-ov-node-body{display:block}',
  ].join('');
  shadow.appendChild(style);

  // ── Markup ─────────────────────────────────────────────────
  const panel = document.createElement('div');
  panel.id = 'aai-ov';
  panel.setAttribute('role', 'dialog');
  panel.setAttribute('aria-label', 'Page accessibility scan');
  panel.setAttribute('aria-modal', 'true');
  panel.innerHTML =
    '<div id="aai-ov-hd">' +
      '<h2>' +
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true" style="flex-shrink:0"><circle cx="12" cy="3.5" r="2"/><path d="M17 7.5H7a1 1 0 0 0 0 2h3.5v3L8 17.5a1 1 0 0 0 1.8.9L12 14l2.2 4.4a1 1 0 0 0 1.8-.9L13.5 12.5v-3H17a1 1 0 0 0 0-2z"/></svg>' +
        'accessmate.ai Scan' +
      '</h2>' +
      '<button id="aai-ov-cls" aria-label="Close panel">&times;</button>' +
    '</div>' +
    '<div id="aai-ov-body">' +
      /* List view — initial prompt message, replaced with results after scan */
      '<div id="aai-ov-list-view">' +
        '<p class="aai-ov-msg">Click <strong>Scan for issues</strong> to run automated accessibility checks on this page.</p>' +
      '</div>' +
      /* Detail view — shown when Details button clicked, hidden by default */
      '<div id="aai-ov-detail-view">' +
        '<button id="aai-ov-back">' +
          '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M10 3L5 8l5 5"/></svg>' +
          'Back to issues' +
        '</button>' +
        '<div class="aai-ov-d-badge-row">' +
          '<span class="aai-ov-d-badge" id="aai-ov-detail-badge"></span>' +
          '<span class="aai-ov-d-wcag" id="aai-ov-detail-wcag"></span>' +
        '</div>' +
        '<h3 id="aai-ov-detail-title"></h3>' +
        '<p id="aai-ov-detail-desc"></p>' +
        '<div class="aai-ov-d-section" id="aai-ov-detail-el-sec">' +
          '<div id="aai-ov-detail-el"></div>' +
        '</div>' +
        '<div class="aai-ov-d-section" id="aai-ov-detail-sel-sec">' +
          '<div class="aai-ov-d-label">Selector</div>' +
          '<code class="aai-ov-d-code" id="aai-ov-detail-sel"></code>' +
        '</div>' +
        '<div class="aai-ov-d-section">' +
          '<div class="aai-ov-d-label">Instances</div>' +
          '<span id="aai-ov-detail-count" style="font-size:13px"></span>' +
        '</div>' +
        '<a id="aai-ov-detail-learn" href="#" target="_blank" rel="noopener noreferrer" style="display:none"></a>' +
        '<div class="aai-ov-d-actions">' +
          '<button id="aai-ov-detail-fix">Generate fix with AI</button>' +
          '<button id="aai-ov-detail-explain">Explain</button>' +
        '</div>' +
        '<div id="aai-ov-detail-ai-out" style="display:none"></div>' +
      '</div>' +
    '</div>' +
    '<div id="aai-ov-ft">' +
      '<button class="aai-ov-btn" id="aai-ov-scan">Scan for issues</button>' +
    '</div>';
  shadow.appendChild(panel);

  const ovBody    = shadow.querySelector<HTMLElement>('#aai-ov-body')!;
  const listView  = shadow.querySelector<HTMLElement>('#aai-ov-list-view')!;
  const detailView = shadow.querySelector<HTMLElement>('#aai-ov-detail-view')!;
  const btn   = shadow.querySelector<HTMLButtonElement>('#aai-ov-scan')!;
  const close = shadow.querySelector<HTMLButtonElement>('#aai-ov-cls')!;

  // ── Panel control ──────────────────────────────────────────
  let scanStarted = false;

  function openPanel(): void {
    document.documentElement.style.marginRight = PANEL_W + 'px';
    panel.classList.add('open');
    close.focus();
    if (!scanStarted) { scanStarted = true; setTimeout(triggerScan, 350); }
  }
  function closePanel(): void {
    document.documentElement.style.marginRight = '';
    panel.classList.remove('open');
    clearHl();
  }

  close.addEventListener('click', closePanel);
  document.addEventListener('keydown', (e: KeyboardEvent) => {
    if (e.key === 'Escape' && panel.classList.contains('open')) closePanel();
  });

  // ── Scan state ────────────────────────────────────────────
  let lastIssues: AaiIssue[]   = [];
  let lastPasses: axe.Result[] = [];
  let activeFilter = 'all';

  // ── Detail view logic ─────────────────────────────────────
  let currentIssue: AaiIssue | null = null;

  function openDetail(iss: AaiIssue): void {
    currentIssue = iss;

    const badge = shadow.querySelector<HTMLElement>('#aai-ov-detail-badge')!;
    badge.textContent = iss.severity;
    badge.className = `aai-ov-d-badge ${iss.severity}`;

    const wcagEl = shadow.querySelector<HTMLElement>('#aai-ov-detail-wcag')!;
    wcagEl.textContent = iss.wcag ? `WCAG ${iss.wcag}` : '';
    wcagEl.style.display = iss.wcag ? '' : 'none';

    shadow.querySelector<HTMLElement>('#aai-ov-detail-title')!.textContent = iss.title;
    shadow.querySelector<HTMLElement>('#aai-ov-detail-desc')!.textContent  = iss.description;

    const elSec = shadow.querySelector<HTMLElement>('#aai-ov-detail-el-sec')!;
    const allNodes = iss.nodes.length > 0 ? iss.nodes : (iss.element ? [{ element: iss.element, selector: iss.selector }] : []);
    const nodeColor = iss.severity === 'critical' ? '#ef4444' : iss.severity === 'warning' ? '#f59e0b' : '#3b82f6';
    if (allNodes.length > 0) {
      const label = allNodes.length > 1 ? `Affected elements (${allNodes.length})` : 'Affected element';
      shadow.querySelector<HTMLElement>('#aai-ov-detail-el')!.innerHTML =
        `<p style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;margin:0 0 6px">${label}</p>` +
        `<div class="aai-ov-node-list">` +
        allNodes.map((n, i) => {
          const displayLabel = esc(n.selector || n.element.slice(0, 60));
          return (
            `<div class="aai-ov-node-item${i === 0 ? ' open' : ''}" data-ni="${i}" data-selector="${esc(n.selector)}" style="--nc:${nodeColor}">` +
              `<div class="aai-ov-node-hd">` +
                `<span class="aai-ov-node-dot"></span>` +
                `<span class="aai-ov-node-sel">${displayLabel}</span>` +
                `<span class="aai-ov-node-chevron">&#x203A;</span>` +
              `</div>` +
              `<div class="aai-ov-node-body">` +
                `<pre style="margin:0;padding:10px 12px;background:#f3f4f6;font-size:11px;font-family:ui-monospace,SFMono-Regular,monospace;white-space:pre-wrap;word-break:break-word;color:#374151;line-height:1.5"><code>${esc(n.element)}</code></pre>` +
              `</div>` +
            `</div>`
          );
        }).join('') +
        `</div>`;
      elSec.style.display = '';
    } else { elSec.style.display = 'none'; }

    const selSec = shadow.querySelector<HTMLElement>('#aai-ov-detail-sel-sec')!;
    selSec.style.display = 'none';

    shadow.querySelector<HTMLElement>('#aai-ov-detail-count')!.textContent =
      `${iss.count} element${iss.count !== 1 ? 's' : ''}`;

    const learn = shadow.querySelector<HTMLAnchorElement>('#aai-ov-detail-learn')!;
    if (iss.help_url) {
      learn.href = iss.help_url;
      learn.textContent = (iss.wcag ? `WCAG ${iss.wcag} — ` : '') + 'View guidance ↗';
      learn.style.display = 'inline-block';
    } else { learn.style.display = 'none'; }

    // Reset AI section
    const aiOut = shadow.querySelector<HTMLElement>('#aai-ov-detail-ai-out')!;
    aiOut.style.display = 'none';
    aiOut.innerHTML = '';
    const fixBtn     = shadow.querySelector<HTMLButtonElement>('#aai-ov-detail-fix')!;
    const explainBtn = shadow.querySelector<HTMLButtonElement>('#aai-ov-detail-explain')!;
    fixBtn.innerHTML = 'Generate fix with AI';
    explainBtn.innerHTML = 'Explain';
    fixBtn.disabled = explainBtn.disabled = false;

    // Switch views with slide animation
    listView.classList.add('slide-out');
    detailView.classList.add('slide-in');
    detailView.scrollTop = 0;

    // Highlight the first node by default
    const firstSelector = allNodes[0]?.selector ?? iss.selector;
    if (firstSelector) highlightEl(firstSelector, true);
  }

  function closeDetail(): void {
    listView.classList.remove('slide-out');
    detailView.classList.remove('slide-in');
    clearHl();
  }

  async function callDetailAI(task: 'fix' | 'explain'): Promise<void> {
    const iss = currentIssue;
    if (!iss) return;
    const fixBtn     = shadow.querySelector<HTMLButtonElement>('#aai-ov-detail-fix')!;
    const explainBtn = shadow.querySelector<HTMLButtonElement>('#aai-ov-detail-explain')!;
    const aiOut      = shadow.querySelector<HTMLElement>('#aai-ov-detail-ai-out')!;
    fixBtn.disabled = explainBtn.disabled = true;
    if (task === 'fix') fixBtn.innerHTML     = '<span class="aai-ov-spin"></span>Thinking…';
    else                explainBtn.innerHTML = '<span class="aai-ov-spin"></span>Working…';
    aiOut.style.display = 'none';
    aiOut.innerHTML = '';
    try {
      const res  = await fetch(`${cfg.restBase}/ai/proxy`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce },
        body: JSON.stringify({ task, params: { title: iss.title, wcag: iss.wcag, html: iss.element, context: iss.description } }),
      });
      const data = await res.json() as { text?: string; error?: string };
      aiOut.innerHTML = data.error
        ? `<span class="aai-ov-ai-err">${esc(data.error)}</span>`
        : ovMd(data.text ?? '');
      aiOut.style.display = 'block';
    } catch (err) {
      aiOut.innerHTML = `<span class="aai-ov-ai-err">${esc(String(err))}</span>`;
      aiOut.style.display = 'block';
    } finally {
      fixBtn.disabled = explainBtn.disabled = false;
      fixBtn.innerHTML     = 'Generate fix with AI';
      explainBtn.innerHTML = 'Explain';
    }
  }

  shadow.querySelector('#aai-ov-back')!.addEventListener('click', closeDetail);
  shadow.querySelector('#aai-ov-detail-fix')!.addEventListener('click',     () => callDetailAI('fix'));
  shadow.querySelector('#aai-ov-detail-explain')!.addEventListener('click', () => callDetailAI('explain'));

  // Accordion toggle for affected-elements nodes — also highlights the element on page
  detailView.addEventListener('click', (e: Event) => {
    const hd = (e.target as Element).closest<HTMLElement>('.aai-ov-node-hd');
    if (!hd) return;
    const item = hd.closest<HTMLElement>('.aai-ov-node-item');
    if (!item) return;
    const list = item.closest<HTMLElement>('.aai-ov-node-list');
    const isOpen = item.classList.contains('open');
    list?.querySelectorAll('.aai-ov-node-item').forEach((el) => el.classList.remove('open'));
    if (!isOpen) {
      item.classList.add('open');
      const selector = item.dataset.selector ?? '';
      if (selector) highlightEl(selector, true);
    } else {
      clearHl();
    }
  });

  // ── Render helpers ────────────────────────────────────────
  function esc(s: string): string {
    return String(s)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;')
      .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  }

  /** Minimal markdown → HTML for AI output in the overlay. */
  function ovMd(text: string): string {
    const blocks: string[] = [];
    const wp = text.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_m, _l, code: string) => {
      const i = blocks.length;
      blocks.push(`<pre class="aai-ov-pre"><code>${esc(code.replace(/\n$/, ''))}</code></pre>`);
      return `\x00C${i}\x00`;
    });
    const inl = (s: string) => s
      .replace(/`([^`\n]+)`/g, '<code style="background:#1e1e2e;color:#cdd6f4;padding:1px 4px;border-radius:3px;font-size:12px">$1</code>')
      .replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>')
      .replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
    const out: string[] = [];
    for (const block of wp.split(/\n{2,}/)) {
      const b = block.trim();
      if (!b) continue;
      const cp = b.match(/^\x00C(\d+)\x00$/);
      if (cp) { out.push(blocks[+cp[1]]); continue; }
      const h3 = b.match(/^###\s+(.+)/); if (h3) { out.push(`<p style="font-weight:700;font-size:13px;margin:8px 0 4px">${inl(esc(h3[1]))}</p>`); continue; }
      const h2 = b.match(/^##\s+(.+)/);  if (h2) { out.push(`<p style="font-weight:700;font-size:14px;margin:8px 0 4px">${inl(esc(h2[1]))}</p>`); continue; }
      const h1 = b.match(/^#\s+(.+)/);   if (h1) { out.push(`<p style="font-weight:700;font-size:15px;margin:8px 0 4px">${inl(esc(h1[1]))}</p>`); continue; }
      const lines = b.split('\n');
      const parts: string[] = [];
      let lt: 'ul' | 'ol' | null = null;
      const cl = () => { if (lt) { parts.push(lt === 'ul' ? '</ul>' : '</ol>'); lt = null; } };
      for (const line of lines) {
        const t = line.trim();
        const icp = t.match(/^\x00C(\d+)\x00$/);
        if (icp) { cl(); parts.push(blocks[+icp[1]]); continue; }
        if (/^[-*+]\s/.test(t)) {
          if (lt !== 'ul') { cl(); parts.push('<ul style="margin:4px 0 4px 16px;padding:0">'); lt = 'ul'; }
          parts.push(`<li style="margin:2px 0">${inl(esc(t.replace(/^[-*+]\s+/, '')))}</li>`);
        } else if (/^\d+\.\s/.test(t)) {
          if (lt !== 'ol') { cl(); parts.push('<ol style="margin:4px 0 4px 16px;padding:0">'); lt = 'ol'; }
          parts.push(`<li style="margin:2px 0">${inl(esc(t.replace(/^\d+\.\s+/, '')))}</li>`);
        } else { cl(); if (t) parts.push(`<p style="margin:0 0 6px">${inl(esc(t))}</p>`); }
      }
      cl();
      out.push(parts.join(''));
    }
    return out.join('');
  }

  function buildCard(iss: AaiIssue, idx: number): string {
    const sev = esc(iss.severity);
    const metaParts = [
      iss.severity.charAt(0).toUpperCase() + iss.severity.slice(1),
      iss.wcag ? `WCAG ${esc(iss.wcag)}` : '',
      `${iss.count} element${iss.count !== 1 ? 's' : ''}`,
    ].filter(Boolean).join(' · ');
    return (
      `<div class="aai-ov-issue ${sev}" data-selector="${esc(iss.selector)}" data-idx="${idx}">` +
        `<span class="aai-ov-dot ${sev}"></span>` +
        `<div class="aai-ov-issue-text">` +
          `<strong>${esc(iss.title)}</strong>` +
          `<span class="aai-ov-meta">${metaParts}</span>` +
        `</div>` +
        `<button class="aai-ov-details-btn" data-idx="${idx}">Details</button>` +
      `</div>`
    );
  }

  function buildPassCard(pass: axe.Result): string {
    const count = pass.nodes.length;
    const bodyParts = [`<p style="margin:0 0 6px">${esc(pass.description)}</p>`];
    if (pass.helpUrl) bodyParts.push(
      `<a style="font-size:12px;color:#3858e9;cursor:pointer" href="${esc(pass.helpUrl)}" target="_blank" rel="noopener noreferrer">Learn more ↗</a>`,
    );
    return (
      `<div class="aai-ov-pass-item">` +
        `<div class="aai-ov-pass-item-hd">` +
          `<div class="aai-ov-pass-item-hd-text">` +
            `<strong>${esc(pass.help)}</strong>` +
            `<span class="aai-ov-pass-item-meta">${count} element${count !== 1 ? 's' : ''} checked</span>` +
          `</div>` +
          `<span class="aai-ov-chevron" style="font-size:10px;color:#4ade80;padding-top:3px;flex-shrink:0"></span>` +
        `</div>` +
        `<div class="aai-ov-pass-item-body">${bodyParts.join('')}</div>` +
      `</div>`
    );
  }

  function staggerCards(container: Element): void {
    container.querySelectorAll<HTMLElement>('.aai-ov-issue,.aai-ov-pass-item').forEach((card, i) => {
      card.style.animationDelay = `${i * 35}ms`;
    });
  }

  function renderIssueList(): void {
    const list = shadow.querySelector<HTMLElement>('#aai-ov-issue-list');
    if (!list) return;
    if (activeFilter === 'passed') {
      list.innerHTML = lastPasses.length === 0
        ? '<p class="aai-ov-ok">&#10003; All automated checks passed.</p>'
        : lastPasses.map(buildPassCard).join('');
      staggerCards(list);
      return;
    }
    const filtered = activeFilter === 'all'
      ? lastIssues
      : lastIssues.filter((i) => i.severity === activeFilter);
    list.innerHTML = filtered.length === 0
      ? '<p class="aai-ov-ok">&#10003; No issues in this category!</p>'
      : filtered.map((iss, i) => buildCard(iss, i)).join('');
    staggerCards(list);
  }

  function renderResults(issues: AaiIssue[], score: number, grade: string): void {
    const color    = scoreColor(score);
    const critical = issues.filter((i) => i.severity === 'critical').length;
    const warning  = issues.filter((i) => i.severity === 'warning').length;
    const notice   = issues.filter((i) => i.severity === 'notice').length;

    const initialList = issues.length === 0
      ? '<p class="aai-ov-ok">&#10003; No issues detected</p><p class="aai-ov-ok" style="font-size:12px;padding-top:0">Some issues may require manual testing.</p>'
      : issues.map((iss, i) => buildCard(iss, i)).join('');

    listView.innerHTML =
      `<div class="aai-ov-score">` +
        `<div class="aai-ov-grade" style="background:${color}">${esc(grade)}</div>` +
        `<div>` +
          `<div class="aai-ov-sv">${score}<small>/100</small></div>` +
          `<div class="aai-ov-sc">${issues.length} issue${issues.length !== 1 ? 's' : ''} &middot; ${lastPasses.length} passed</div>` +
        `</div>` +
      `</div>` +
      `<div class="aai-ov-url">${esc(cfg.pageUrl)}</div>` +
      `<div class="aai-ov-filter">` +
        `<button class="aai-ov-filter-btn active" data-filter="all">All (${issues.length})</button>` +
        (critical ? `<button class="aai-ov-filter-btn" data-filter="critical">Critical (${critical})</button>` : '') +
        (warning  ? `<button class="aai-ov-filter-btn" data-filter="warning">Warning (${warning})</button>` : '') +
        (notice   ? `<button class="aai-ov-filter-btn" data-filter="notice">Notice (${notice})</button>` : '') +
        `<button class="aai-ov-filter-btn aai-ov-filter-btn--pass" data-filter="passed">Passed (${lastPasses.length})</button>` +
      `</div>` +
      `<div id="aai-ov-issue-list">${initialList}</div>`;

    const listEl = shadow.querySelector<HTMLElement>('#aai-ov-issue-list');
    if (listEl) staggerCards(listEl);
  }

  // ── Event delegation on listView (registered once) ────────
  // Filter tabs
  listView.addEventListener('click', (e: Event) => {
    const fbtn = (e.target as Element).closest<HTMLElement>('.aai-ov-filter-btn');
    if (fbtn) {
      listView.querySelectorAll('.aai-ov-filter-btn').forEach((b) => b.classList.remove('active'));
      fbtn.classList.add('active');
      activeFilter = fbtn.dataset.filter ?? 'all';
      renderIssueList();
      return;
    }
    // Pass item accordion
    const passHd = (e.target as Element).closest<HTMLElement>('.aai-ov-pass-item-hd');
    if (passHd) { passHd.closest('.aai-ov-pass-item')?.classList.toggle('open'); return; }
    // Details button → open detail view
    const detailsBtn = (e.target as Element).closest<HTMLElement>('.aai-ov-details-btn');
    if (detailsBtn) {
      e.stopPropagation();
      const idx = Number(detailsBtn.dataset.idx);
      if (lastIssues[idx]) openDetail(lastIssues[idx]);
      return;
    }
    // Card click (not Details button) → scroll to + highlight element
    const card = (e.target as Element).closest<HTMLElement>('.aai-ov-issue[data-selector]');
    if (card) highlightEl(card.dataset.selector ?? '', true);
  });


  // ── Scan trigger ──────────────────────────────────────────
  async function triggerScan(): Promise<void> {
    btn.disabled = true;
    btn.textContent = 'Scanning…';
    // Reset to list view during scan
    listView.classList.remove('slide-out');
    detailView.classList.remove('slide-in');

    const skCard = (w1: number, w2: number, color: string) =>
      `<div class="aai-ov-skel-card" style="border-color:${color}">` +
        `<div class="aai-ov-skel" style="height:13px;width:${w1}%"></div>` +
        `<div class="aai-ov-skel" style="height:10px;width:${w2}%"></div>` +
      `</div>`;
    listView.innerHTML =
      `<div class="aai-ov-skel-score"><div class="aai-ov-skel aai-ov-skel-circle"></div>` +
      `<div class="aai-ov-skel-lines"><div class="aai-ov-skel" style="height:14px;width:55%"></div>` +
      `<div class="aai-ov-skel" style="height:11px;width:40%"></div></div></div>` +
      `<div class="aai-ov-skel" style="height:28px;margin-bottom:14px;border-radius:6px"></div>` +
      skCard(72, 48, '#fca5a5') + skCard(65, 55, '#fca5a5') +
      skCard(80, 42, '#fcd34d') + skCard(60, 50, '#93c5fd') + skCard(75, 38, '#93c5fd');

    try {
      const minDelay = new Promise<void>((r) => setTimeout(r, 3000 + Math.random() * 1000));
      const [results] = await Promise.all([runAxe(), minDelay]);
      lastIssues   = axeToIssues(results.violations);
      lastPasses   = results.passes;
      activeFilter = 'all';
      const score  = calcScore(lastIssues);
      const grade  = calcGrade(score);
      renderResults(lastIssues, score, grade);
      storeScan(cfg, {
        url: cfg.pageUrl, score, grade,
        passed: results.passes.length, failed: results.violations.length, issues: lastIssues,
      }).catch(() => {});
    } catch (err: unknown) {
      const msg = err instanceof Error ? err.message : 'axe-core failed';
      listView.innerHTML = `<p class="aai-ov-msg" style="color:#ef4444">Error: ${esc(msg)}</p>`;
    } finally {
      btn.disabled = false;
      btn.textContent = 'Re-scan page';
    }
  }

  btn.addEventListener('click', triggerScan);

  // ── Public API ────────────────────────────────────────────
  window.accessmateOverlay = { open: openPanel, close: closePanel };

  function hookAdminBar(): void {
    const node = document.getElementById('wp-admin-bar-accessmate-scan');
    if (!node) return;
    // Target the <a> directly so preventDefault stops navigation before the browser acts on href="#"
    const anchor = node.querySelector<HTMLElement>('a.ab-item') ?? node;
    anchor.addEventListener('click', (e) => { e.preventDefault(); openPanel(); });
  }

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