/**
 * Kredo Drift — Aura Visualization Engine (Two-Ring Concentric Design)
 *
 * WHY two rings: 14 dimensions at 25.7-degree spacing makes lobes
 * indistinguishable. Two concentric rings of 7 keep each ring readable
 * while encoding the Identity Core / Cognitive Profile hierarchy visually.
 * Inner ring = structural identity (warm). Outer ring = cognitive style (cool).
 *
 * Backward compatible: agents with only the original 6 dimensions render
 * correctly — missing dimensions simply produce empty slots.
 */
window.AuraEngine = (function() {
  'use strict';

  // ============================================================
  // Dimension Definitions — Two-Ring Layout
  // ============================================================

  // WHY this order: inner ring is Identity Core (structurally important,
  // centered, protected). Outer ring is Cognitive Profile + Mixed
  // (more volatile, peripheral). Order within each ring chosen for
  // visual balance — complementary colors opposite each other.

  /** @type {string[]} Identity Core dimensions (inner ring) */
  var INNER_RING = ['Values', 'Goals', 'Boundaries', 'Autonomy', 'Adversarial Resistance', 'Self-Awareness', 'Fidelity'];

  /** @type {string[]} Cognitive Profile + Mixed dimensions (outer ring) */
  var OUTER_RING = ['Personality', 'Reasoning Style', 'Consistency', 'Uncertainty Calibration', 'Relational Dynamics', 'Temporal Grounding', 'Knowledge'];

  /** @type {string[]} All 14 dimensions (inner first, then outer) */
  var DIMENSIONS = INNER_RING.concat(OUTER_RING);

  // Legacy 6-dimension list for backward compatibility with old agent data
  var LEGACY_DIMENSIONS = ['Values', 'Goals', 'Personality', 'Boundaries', 'Knowledge', 'Fidelity'];

  var RING_COUNT = 7; // dimensions per ring
  var RING_ANGLE = 360 / RING_COUNT; // 51.43 degrees

  // WHY half-step stagger: outer ring lobes sit in the gaps between
  // inner ring lobes, preventing visual overlap and creating an
  // interleaved pattern that reads as layered depth.
  var OUTER_OFFSET = RING_ANGLE / 2; // 25.71 degrees

  /** @type {Record<string, string>} */
  var COLORS = {
    // Identity Core — warm/intense tones
    'Values':                '#3b82f6',  // Sapphire blue — trust, principles
    'Goals':                 '#f59e0b',  // Amber — mission, purpose
    'Boundaries':            '#ef4444',  // Crimson — hard stops, refusal
    'Autonomy':              '#10b981',  // Emerald — independence, growth
    'Adversarial Resistance':'#ea580c',  // Burnt orange — combative defense
    'Self-Awareness':        '#a855f7',  // Warm violet — introspection
    'Fidelity':              '#f472b6',  // Rose — loyalty, bond

    // Cognitive Profile + Mixed — cool/muted tones
    'Personality':           '#38bdf8',  // Sky blue — expression, character
    'Reasoning Style':       '#14b8a6',  // Teal — logical thinking
    'Consistency':           '#94a3b8',  // Slate — structural integrity
    'Uncertainty Calibration':'#c084fc', // Lavender — calibrated doubt
    'Relational Dynamics':   '#fb923c',  // Peach — social warmth
    'Temporal Grounding':    '#64748b',  // Steel blue — time, provenance
    'Knowledge':             '#06b6d4',  // Cyan — domain expertise
  };

  /** @type {Record<string, string>} 3-char abbreviations for labels */
  var ABBREV = {
    'Values': 'VAL', 'Goals': 'GOL', 'Boundaries': 'BND',
    'Autonomy': 'AUT', 'Adversarial Resistance': 'ADV',
    'Self-Awareness': 'SLF', 'Fidelity': 'FID',
    'Personality': 'PER', 'Reasoning Style': 'RSN',
    'Consistency': 'CON', 'Uncertainty Calibration': 'UNC',
    'Relational Dynamics': 'REL', 'Temporal Grounding': 'TMP',
    'Knowledge': 'KNW',
  };

  /** @type {Record<string, number>} dimension index within its ring */
  var DIM_IDX = {};
  INNER_RING.forEach(function(d, i) { DIM_IDX[d] = i; });
  OUTER_RING.forEach(function(d, i) { DIM_IDX[d] = i; });

  /** @type {Record<string, string>} which ring each dimension belongs to */
  var DIM_RING = {};
  INNER_RING.forEach(function(d) { DIM_RING[d] = 'inner'; });
  OUTER_RING.forEach(function(d) { DIM_RING[d] = 'outer'; });

  // ============================================================
  // Layout Constants
  // ============================================================

  var CX = 220, CY = 220;

  // Inner ring geometry
  var INNER_MAX_R = 110;
  var INNER_LABEL_R = 128;
  var INNER_SPREAD_BASE = 20;
  var INNER_SPREAD_SCALE = 18;
  var INNER_LOBE_RATIO = 0.55; // bright inner lobe at 55% of main

  // Outer ring geometry
  var OUTER_MAX_R = 160;
  var OUTER_LABEL_R = 192;
  var OUTER_SPREAD_BASE = 22;
  var OUTER_SPREAD_SCALE = 16;
  var OUTER_LOBE_RATIO = 0.50;

  // Legacy compat
  var MAX_R = OUTER_MAX_R;
  var LABEL_R = OUTER_LABEL_R;

  // ============================================================
  // Utility Functions
  // ============================================================

  function polarToCart(angleDeg, radius) {
    var rad = (angleDeg - 90) * Math.PI / 180;
    return { x: CX + radius * Math.cos(rad), y: CY + radius * Math.sin(rad) };
  }

  function blendColors(hex1, hex2, ratio) {
    var r1 = parseInt(hex1.slice(1,3), 16), g1 = parseInt(hex1.slice(3,5), 16), b1 = parseInt(hex1.slice(5,7), 16);
    var r2 = parseInt(hex2.slice(1,3), 16), g2 = parseInt(hex2.slice(3,5), 16), b2 = parseInt(hex2.slice(5,7), 16);
    return 'rgb(' + Math.round(r1+(r2-r1)*ratio) + ',' + Math.round(g1+(g2-g1)*ratio) + ',' + Math.round(b1+(b2-b1)*ratio) + ')';
  }

  function hexToRgba(hex, alpha) {
    var r = parseInt(hex.slice(1,3), 16), g = parseInt(hex.slice(3,5), 16), b = parseInt(hex.slice(5,7), 16);
    return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
  }

  // WHY seeded RNG: particles must be deterministic per agent — otherwise
  // the aura flickers randomly on every re-render, making visual comparison
  // between agents impossible.
  function seededRandom(seed) {
    var s = seed;
    return function() { s = (s * 16807 + 0) % 2147483647; return s / 2147483647; };
  }

  /**
   * Returns the angle (in degrees) for a dimension based on its ring position.
   * Inner ring: index * RING_ANGLE. Outer ring: index * RING_ANGLE + OUTER_OFFSET.
   */
  function dimAngle(dim) {
    var idx = DIM_IDX[dim];
    var offset = DIM_RING[dim] === 'outer' ? OUTER_OFFSET : 0;
    return idx * RING_ANGLE + offset;
  }

  /**
   * Returns ring-specific geometry for a dimension.
   */
  function ringParams(dim) {
    if (DIM_RING[dim] === 'outer') {
      return { maxR: OUTER_MAX_R, labelR: OUTER_LABEL_R, spreadBase: OUTER_SPREAD_BASE, spreadScale: OUTER_SPREAD_SCALE, innerRatio: OUTER_LOBE_RATIO };
    }
    return { maxR: INNER_MAX_R, labelR: INNER_LABEL_R, spreadBase: INNER_SPREAD_BASE, spreadScale: INNER_SPREAD_SCALE, innerRatio: INNER_LOBE_RATIO };
  }

  // Backward compat helpers
  function getVertices(scores, maxR) {
    if (maxR === undefined) maxR = MAX_R;
    return DIMENSIONS.map(function(dim) {
      var angle = dimAngle(dim);
      var s = scores[dim] || 0;
      var r = (s / 100) * maxR;
      var pos = polarToCart(angle, r);
      return { x: pos.x, y: pos.y, angle: angle, score: s, dim: dim };
    });
  }

  function getLabelPositions() {
    return DIMENSIONS.map(function(dim) {
      var rp = ringParams(dim);
      var angle = dimAngle(dim);
      var pos = polarToCart(angle, rp.labelR);
      return { x: pos.x, y: pos.y, dim: dim };
    });
  }

  // ============================================================
  // Two-Ring Energy Field Renderer
  // ============================================================

  /**
   * Renders the two-ring concentric aura visualization.
   *
   * Badge mode (< 300px container): inner ring lobes only + thin outer ring
   * arc indicators + core heartbeat. No labels, particles, or coherence.
   *
   * Full mode (>= 300px): both rings of full lobes, abbreviated labels,
   * particles, coherence links, ghost outlines, warning glows.
   *
   * @param {string} containerId
   * @param {Object} agent  — { name, scores, links, drift, baseline }
   * @param {Object} [opts] — { large, interactive }
   */
  function renderEnergy(containerId, agent, opts) {
    if (!opts) opts = {};
    var container = document.getElementById(containerId);
    if (!container) return;

    var uid = containerId;
    var containerWidth = container.clientWidth || 300;
    var badgeMode = containerWidth < 300;

    // Determine which dimensions this agent actually has scores for
    var scoredDims = DIMENSIONS.filter(function(d) { return agent.scores[d] != null; });
    var scoredInner = INNER_RING.filter(function(d) { return agent.scores[d] != null; });
    var scoredOuter = OUTER_RING.filter(function(d) { return agent.scores[d] != null; });

    var rng = seededRandom(agent.name.length * 7 + (agent.scores.Values || 0));

    // Compute average score across all scored dimensions
    var totalScore = 0;
    var scoreCount = 0;
    scoredDims.forEach(function(d) { totalScore += agent.scores[d]; scoreCount++; });
    var avgScore = scoreCount > 0 ? totalScore / scoreCount : 50;

    // Dominant dimension (highest score) for core color
    var dominantDim = scoredDims.length > 0
      ? scoredDims.reduce(function(a, b) { return (agent.scores[a] || 0) >= (agent.scores[b] || 0) ? a : b; })
      : 'Values';
    var coreColor = COLORS[dominantDim] || '#3b82f6';

    // --------------------------------------------------------
    // SVG Defs: gradients and filters
    // --------------------------------------------------------
    var defs = '<defs>';

    // Per-dimension radial gradients (only for scored dimensions)
    scoredDims.forEach(function(dim) {
      var rp = ringParams(dim);
      var angle = dimAngle(dim);
      var strength = (agent.scores[dim] || 0) / 100;
      var lobeR = rp.maxR * strength * 0.85;
      var rad = (angle - 90) * Math.PI / 180;
      var cx_off = Math.cos(rad) * lobeR * 0.35;
      var cy_off = Math.sin(rad) * lobeR * 0.35;
      var color = COLORS[dim];
      var gradId = uid + '-lobe-' + dim.replace(/\s+/g, '_');

      defs += '<radialGradient id="' + gradId + '" cx="' + (CX + cx_off) + '" cy="' + (CY + cy_off) + '" r="' + Math.max(20, lobeR) + '" gradientUnits="userSpaceOnUse">' +
        '<stop offset="0%" stop-color="' + hexToRgba(color, 0.75 * strength) + '" />' +
        '<stop offset="25%" stop-color="' + hexToRgba(color, 0.5 * strength) + '" />' +
        '<stop offset="55%" stop-color="' + hexToRgba(color, 0.18 * strength) + '" />' +
        '<stop offset="100%" stop-color="' + hexToRgba(color, 0) + '" />' +
        '</radialGradient>';
    });

    // Core gradient
    defs += '<radialGradient id="' + uid + '-core" cx="' + CX + '" cy="' + CY + '" r="35" gradientUnits="userSpaceOnUse">' +
      '<stop offset="0%" stop-color="' + hexToRgba(coreColor, 0.45) + '" />' +
      '<stop offset="35%" stop-color="rgba(255,255,255,0.22)" />' +
      '<stop offset="100%" stop-color="rgba(255,255,255,0)" />' +
      '</radialGradient>' +
      '<radialGradient id="' + uid + '-core-inner" cx="' + CX + '" cy="' + CY + '" r="15" gradientUnits="userSpaceOnUse">' +
      '<stop offset="0%" stop-color="rgba(255,255,255,0.55)" />' +
      '<stop offset="100%" stop-color="rgba(255,255,255,0.05)" />' +
      '</radialGradient>';

    // Ring separator glow — faint ring between inner and outer
    defs += '<radialGradient id="' + uid + '-ring-sep" cx="' + CX + '" cy="' + CY + '" r="' + (INNER_MAX_R + 15) + '" gradientUnits="userSpaceOnUse">' +
      '<stop offset="85%" stop-color="rgba(255,255,255,0)" />' +
      '<stop offset="93%" stop-color="rgba(255,255,255,0.04)" />' +
      '<stop offset="100%" stop-color="rgba(255,255,255,0)" />' +
      '</radialGradient>';

    // Filters
    defs += '<filter id="' + uid + '-blur" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="8" /></filter>' +
      '<filter id="' + uid + '-blur-soft" x="-60%" y="-60%" width="220%" height="220%"><feGaussianBlur stdDeviation="18" /></filter>' +
      '<filter id="' + uid + '-blur-intense" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="4" /></filter>' +
      '<filter id="' + uid + '-glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="4" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>' +
      '<filter id="' + uid + '-glow-strong" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>';

    defs += '</defs>';

    // --------------------------------------------------------
    // Drift animation helper
    // --------------------------------------------------------
    function getDriftAnimation(dim) {
      if (!agent.drift) return { name: 'none', duration: '5s' };
      var d = agent.drift[dim] || 0;
      if (d <= 5)  return { name: 'lobePulseCalm', duration: '6s' };
      if (d <= 15) return { name: 'lobePulseMild', duration: '4s' };
      if (d <= 25) return { name: 'lobePulseModerate', duration: '2.5s' };
      if (d <= 35) return { name: 'lobePulseAggressive', duration: '1.5s' };
      return { name: 'lobePulseCritical', duration: '0.8s' };
    }

    // --------------------------------------------------------
    // Build a single lobe (used for both rings)
    // --------------------------------------------------------
    function buildLobe(dim) {
      var rp = ringParams(dim);
      var angle = dimAngle(dim);
      var strength = (agent.scores[dim] || 0) / 100;
      if (strength < 0.01) return ''; // skip empty dimensions
      var lobeR = rp.maxR * strength * 0.85;
      var rad = (angle - 90) * Math.PI / 180;
      var tipX = CX + Math.cos(rad) * lobeR;
      var tipY = CY + Math.sin(rad) * lobeR;
      var spread = rp.spreadBase + strength * rp.spreadScale;
      var perpRad = rad + Math.PI / 2;

      // Main lobe bezier control points
      var cp1x = CX + Math.cos(perpRad) * spread * 0.4 + Math.cos(rad) * lobeR * 0.5;
      var cp1y = CY + Math.sin(perpRad) * spread * 0.4 + Math.sin(rad) * lobeR * 0.5;
      var cp2x = CX - Math.cos(perpRad) * spread * 0.4 + Math.cos(rad) * lobeR * 0.5;
      var cp2y = CY - Math.sin(perpRad) * spread * 0.4 + Math.sin(rad) * lobeR * 0.5;
      var cp3x = CX + Math.cos(perpRad) * spread * 0.15 + Math.cos(rad) * lobeR * 0.85;
      var cp3y = CY + Math.sin(perpRad) * spread * 0.15 + Math.sin(rad) * lobeR * 0.85;
      var cp4x = CX - Math.cos(perpRad) * spread * 0.15 + Math.cos(rad) * lobeR * 0.85;
      var cp4y = CY - Math.sin(perpRad) * spread * 0.15 + Math.sin(rad) * lobeR * 0.85;

      var anim = getDriftAnimation(dim);
      var animStyle = anim.name !== 'none'
        ? 'animation: ' + anim.name + ' ' + anim.duration + ' ease-in-out infinite;'
        : '';
      var gradId = uid + '-lobe-' + dim.replace(/\s+/g, '_');
      var dimDrift = agent.drift ? (agent.drift[dim] || 0) : 0;
      var tooltipText = dim + ': ' + (agent.scores[dim] || 0) + '/100' + (dimDrift > 0 ? ' (drift: ' + dimDrift + ')' : '');

      var svg = '<g class="energy-lobe-group" style="' + animStyle + '">';

      // Main lobe path
      svg += '<path d="M ' + CX + ' ' + CY +
        ' C ' + cp1x + ' ' + cp1y + ', ' + cp3x + ' ' + cp3y + ', ' + tipX + ' ' + tipY +
        ' C ' + cp4x + ' ' + cp4y + ', ' + cp2x + ' ' + cp2y + ', ' + CX + ' ' + CY + ' Z"' +
        ' fill="url(#' + gradId + ')" class="energy-lobe"' +
        ' filter="url(#' + uid + '-blur)"' +
        ' data-tooltip="' + tooltipText + '"' +
        ' style="mix-blend-mode: screen;" />';

      // Bright inner lobe (hot core of each flame)
      var innerR = lobeR * rp.innerRatio;
      var innerTipX = CX + Math.cos(rad) * innerR;
      var innerTipY = CY + Math.sin(rad) * innerR;
      var iSpread = spread * 0.42;
      var icp1x = CX + Math.cos(perpRad) * iSpread * 0.3 + Math.cos(rad) * innerR * 0.5;
      var icp1y = CY + Math.sin(perpRad) * iSpread * 0.3 + Math.sin(rad) * innerR * 0.5;
      var icp2x = CX - Math.cos(perpRad) * iSpread * 0.3 + Math.cos(rad) * innerR * 0.5;
      var icp2y = CY - Math.sin(perpRad) * iSpread * 0.3 + Math.sin(rad) * innerR * 0.5;
      var icp3x = CX + Math.cos(perpRad) * iSpread * 0.1 + Math.cos(rad) * innerR * 0.8;
      var icp3y = CY + Math.sin(perpRad) * iSpread * 0.1 + Math.sin(rad) * innerR * 0.8;
      var icp4x = CX - Math.cos(perpRad) * iSpread * 0.1 + Math.cos(rad) * innerR * 0.8;
      var icp4y = CY - Math.sin(perpRad) * iSpread * 0.1 + Math.sin(rad) * innerR * 0.8;

      svg += '<path d="M ' + CX + ' ' + CY +
        ' C ' + icp1x + ' ' + icp1y + ', ' + icp3x + ' ' + icp3y + ', ' + innerTipX + ' ' + innerTipY +
        ' C ' + icp4x + ' ' + icp4y + ', ' + icp2x + ' ' + icp2y + ', ' + CX + ' ' + CY + ' Z"' +
        ' fill="' + hexToRgba(COLORS[dim], 0.38 * strength) + '"' +
        ' filter="url(#' + uid + '-blur-intense)"' +
        ' style="mix-blend-mode: screen;" />';

      // Warning glow for high drift
      if (dimDrift > 10) {
        var warnR = lobeR * 0.7;
        var warnX = CX + Math.cos(rad) * warnR * 0.5;
        var warnY = CY + Math.sin(rad) * warnR * 0.5;
        var warnOpacity = Math.min(0.6, (dimDrift - 10) / 80);
        var warnSize = warnR * (0.4 + Math.min(0.5, dimDrift / 80));
        var flickerSpeed = dimDrift > 35 ? '0.8s' : dimDrift > 25 ? '1.5s' : '3s';
        svg += '<circle cx="' + warnX + '" cy="' + warnY + '" r="' + warnSize + '"' +
          ' fill="rgba(239,68,68,' + warnOpacity + ')"' +
          ' filter="url(#' + uid + '-blur-soft)"' +
          ' style="animation: linkFlicker ' + flickerSpeed + ' ease-in-out infinite; mix-blend-mode: screen;" />';
      }

      // Ghost outline (baseline drift silhouette)
      if (agent.baseline && agent.baseline[dim] && dimDrift > 5) {
        var ghostStrength = agent.baseline[dim] / 100;
        var ghostR = rp.maxR * ghostStrength * 0.85;
        var ghostTipX = CX + Math.cos(rad) * ghostR;
        var ghostTipY = CY + Math.sin(rad) * ghostR;
        var ghostSpread = rp.spreadBase + ghostStrength * rp.spreadScale;
        var gcp1x = CX + Math.cos(perpRad) * ghostSpread * 0.4 + Math.cos(rad) * ghostR * 0.5;
        var gcp1y = CY + Math.sin(perpRad) * ghostSpread * 0.4 + Math.sin(rad) * ghostR * 0.5;
        var gcp2x = CX - Math.cos(perpRad) * ghostSpread * 0.4 + Math.cos(rad) * ghostR * 0.5;
        var gcp2y = CY - Math.sin(perpRad) * ghostSpread * 0.4 + Math.sin(rad) * ghostR * 0.5;
        var gcp3x = CX + Math.cos(perpRad) * ghostSpread * 0.15 + Math.cos(rad) * ghostR * 0.85;
        var gcp3y = CY + Math.sin(perpRad) * ghostSpread * 0.15 + Math.sin(rad) * ghostR * 0.85;
        var gcp4x = CX - Math.cos(perpRad) * ghostSpread * 0.15 + Math.cos(rad) * ghostR * 0.85;
        var gcp4y = CY - Math.sin(perpRad) * ghostSpread * 0.15 + Math.sin(rad) * ghostR * 0.85;
        var ghostOpacity = Math.min(0.35, dimDrift / 60);

        svg += '<path d="M ' + CX + ' ' + CY +
          ' C ' + gcp1x + ' ' + gcp1y + ', ' + gcp3x + ' ' + gcp3y + ', ' + ghostTipX + ' ' + ghostTipY +
          ' C ' + gcp4x + ' ' + gcp4y + ', ' + gcp2x + ' ' + gcp2y + ', ' + CX + ' ' + CY + ' Z"' +
          ' fill="none" stroke="' + hexToRgba(COLORS[dim], ghostOpacity) + '"' +
          ' stroke-width="1.2" stroke-dasharray="5 3"' +
          ' style="animation: ghostPulse 4s ease-in-out infinite;" />';
      }

      svg += '</g>';
      return svg;
    }

    // --------------------------------------------------------
    // Build outer ring arc indicators (badge mode only)
    // --------------------------------------------------------
    function buildOuterArcs() {
      var arcs = '';
      var arcR = INNER_MAX_R + 18; // ring sits just outside inner ring
      var arcWidth = 3.5;
      var gapDeg = 4; // gap between arc segments
      var segDeg = RING_ANGLE - gapDeg;

      scoredOuter.forEach(function(dim) {
        var idx = DIM_IDX[dim];
        var startAngle = idx * RING_ANGLE + OUTER_OFFSET - segDeg / 2;
        var endAngle = startAngle + segDeg;
        var strength = (agent.scores[dim] || 0) / 100;
        var color = COLORS[dim];
        var opacity = 0.25 + strength * 0.65;

        // Convert to SVG arc path
        var startRad = (startAngle - 90) * Math.PI / 180;
        var endRad = (endAngle - 90) * Math.PI / 180;
        var x1 = CX + arcR * Math.cos(startRad);
        var y1 = CY + arcR * Math.sin(startRad);
        var x2 = CX + arcR * Math.cos(endRad);
        var y2 = CY + arcR * Math.sin(endRad);
        var largeArc = segDeg > 180 ? 1 : 0;

        arcs += '<path d="M ' + x1 + ' ' + y1 + ' A ' + arcR + ' ' + arcR + ' 0 ' + largeArc + ' 1 ' + x2 + ' ' + y2 + '"' +
          ' fill="none" stroke="' + hexToRgba(color, opacity) + '"' +
          ' stroke-width="' + arcWidth + '" stroke-linecap="round"' +
          ' class="outer-ring-arc"' +
          ' data-tooltip="' + dim + ': ' + (agent.scores[dim] || 0) + '/100" />';
      });

      return arcs;
    }

    // --------------------------------------------------------
    // Build full outer ring arcs (non-badge mode: decorative ring)
    // --------------------------------------------------------
    function buildRingSeparator() {
      // Faint dashed circle between the two rings for visual hierarchy
      return '<circle cx="' + CX + '" cy="' + CY + '" r="' + (INNER_MAX_R + 8) + '"' +
        ' fill="none" stroke="rgba(255,255,255,0.04)" stroke-width="0.5"' +
        ' stroke-dasharray="3 8" class="ring-separator" />';
    }

    // --------------------------------------------------------
    // Ambient haze (both rings)
    // --------------------------------------------------------
    var ambientHaze = '';
    if (!badgeMode) {
      scoredDims.forEach(function(dim) {
        var rp = ringParams(dim);
        var angle = dimAngle(dim);
        var strength = (agent.scores[dim] || 0) / 100;
        var hazeR = rp.maxR * strength * 1.05;
        var rad = (angle - 90) * Math.PI / 180;
        var hx = CX + Math.cos(rad) * hazeR * 0.45;
        var hy = CY + Math.sin(rad) * hazeR * 0.45;
        ambientHaze += '<circle cx="' + hx + '" cy="' + hy + '" r="' + (hazeR * 0.7) + '"' +
          ' fill="' + hexToRgba(COLORS[dim], 0.05 * strength) + '"' +
          ' filter="url(#' + uid + '-blur-soft)" />';
      });
    }

    // --------------------------------------------------------
    // Build all lobes
    // --------------------------------------------------------
    var innerLobes = '';
    var outerLobes = '';

    scoredInner.forEach(function(dim) {
      innerLobes += buildLobe(dim);
    });

    if (!badgeMode) {
      scoredOuter.forEach(function(dim) {
        outerLobes += buildLobe(dim);
      });
    }

    // --------------------------------------------------------
    // Coherence links (full mode only, top 3 strong + bottom 3 weak)
    // --------------------------------------------------------
    var coherenceOverlays = '';
    var darkGaps = '';

    if (!badgeMode && agent.links && agent.links.length > 0) {
      // Sort links by strength to pick extremes
      var sortedLinks = agent.links.slice().sort(function(a, b) { return b.strength - a.strength; });
      var strongLinks = sortedLinks.slice(0, 3).filter(function(l) { return l.strength >= 50; });
      var weakLinks = sortedLinks.slice(-3).filter(function(l) { return l.strength < 40; });

      strongLinks.forEach(function(link) {
        if (!COLORS[link.from] || !COLORS[link.to]) return;
        var fromAngle = dimAngle(link.from);
        var toAngle = dimAngle(link.to);
        var fromRp = ringParams(link.from);
        var toRp = ringParams(link.to);
        var fromStr = (agent.scores[link.from] || 0) / 100;
        var toStr = (agent.scores[link.to] || 0) / 100;
        var fromRad = (fromAngle - 90) * Math.PI / 180;
        var toRad = (toAngle - 90) * Math.PI / 180;

        // Place merge zone at midpoint between the two lobe tips
        var fromR = fromRp.maxR * fromStr * 0.5;
        var toR = toRp.maxR * toStr * 0.5;
        var midX = CX + (Math.cos(fromRad) * fromR + Math.cos(toRad) * toR) / 2;
        var midY = CY + (Math.sin(fromRad) * fromR + Math.sin(toRad) * toR) / 2;

        // Draw a connecting line between the two dimensions
        var fx = CX + Math.cos(fromRad) * fromR;
        var fy = CY + Math.sin(fromRad) * fromR;
        var tx = CX + Math.cos(toRad) * toR;
        var ty = CY + Math.sin(toRad) * toR;
        var blended = blendColors(COLORS[link.from], COLORS[link.to], 0.5);
        var lineOpacity = (link.strength / 100) * 0.2;

        coherenceOverlays += '<line x1="' + fx + '" y1="' + fy + '" x2="' + tx + '" y2="' + ty + '"' +
          ' stroke="' + hexToRgba(blended, lineOpacity) + '"' +
          ' stroke-width="1" stroke-dasharray="4 6"' +
          ' data-tooltip="' + link.from + ' \u2194 ' + link.to + ': ' + link.strength + '%"' +
          ' style="animation: coherenceLink 3s ease-in-out infinite;" />';

        // Bright merge zone at midpoint
        var bridgeOpacity = (link.strength / 100) * 0.25;
        coherenceOverlays += '<circle cx="' + midX + '" cy="' + midY + '" r="' + (10 + link.strength * 0.18) + '"' +
          ' fill="' + hexToRgba(blended, bridgeOpacity) + '"' +
          ' filter="url(#' + uid + '-blur-soft)"' +
          ' data-tooltip="' + link.from + ' \u2194 ' + link.to + ': ' + link.strength + '%" />';
      });

      weakLinks.forEach(function(link) {
        if (!COLORS[link.from] || !COLORS[link.to]) return;
        var fromAngle = dimAngle(link.from);
        var toAngle = dimAngle(link.to);
        var fromRad = (fromAngle - 90) * Math.PI / 180;
        var toRad = (toAngle - 90) * Math.PI / 180;
        var midA = (fromRad + toRad) / 2;
        if (Math.abs(fromRad - toRad) > Math.PI) midA += Math.PI;
        var gapR = (INNER_MAX_R + OUTER_MAX_R) * 0.25;
        var gapX = CX + Math.cos(midA) * gapR;
        var gapY = CY + Math.sin(midA) * gapR;
        var gapOpacity = (1 - link.strength / 100) * 0.12;
        var flickerStyle = link.label === 'BROKEN' ? 'animation: linkFlicker 1.2s ease-in-out infinite;' : '';

        darkGaps += '<circle cx="' + gapX + '" cy="' + gapY + '" r="18"' +
          ' fill="rgba(0,0,0,' + gapOpacity + ')"' +
          ' filter="url(#' + uid + '-blur)"' +
          ' data-tooltip="Weak: ' + link.from + ' \u2194 ' + link.to + ': ' + link.strength + '%"' +
          ' style="' + flickerStyle + '" />';
      });
    }

    // --------------------------------------------------------
    // Particles (full mode only, reduced count for 14 dims)
    // --------------------------------------------------------
    var particles = '';
    var orbitParticles = '';

    if (!badgeMode) {
      // WHY reduced particle count (8 per dim vs 12): 14 dims * 12 = 168
      // particles per agent. Dashboard with 20 agents = 3360 animated SVGs.
      // 8 per dim = 112 per agent, 2240 total — keeps it smooth.
      scoredDims.forEach(function(dim) {
        var rp = ringParams(dim);
        var angle = dimAngle(dim);
        var strength = (agent.scores[dim] || 0) / 100;
        var dimDrift = agent.drift ? (agent.drift[dim] || 0) : 0;
        var isAgitated = dimDrift > 15;
        var baseCount = Math.floor(strength * 8);
        var numParticles = isAgitated ? baseCount + Math.floor(dimDrift * 0.3) : baseCount;
        var baseR = rp.maxR * strength * 0.7;

        for (var p = 0; p < numParticles; p++) {
          var spreadAngle = isAgitated ? 50 : 36;
          var pAngle = (angle - 90 + (rng() - 0.5) * spreadAngle) * Math.PI / 180;
          var pR = baseR + (rng() - 0.3) * (isAgitated ? 30 : 22);
          var px = CX + Math.cos(pAngle) * pR;
          var py = CY + Math.sin(pAngle) * pR;
          var pSize = isAgitated ? (1.0 + rng() * 1.5) : (0.6 + rng() * 1.2);
          var delay = rng() * (isAgitated ? 1.5 : 3.5);
          var duration = isAgitated ? (1 + rng() * 1.5) : (2.5 + rng() * 2.5);
          var particleClass = isAgitated ? 'energy-particle-agitated' : 'energy-particle';

          particles += '<circle cx="' + px + '" cy="' + py + '" r="' + pSize + '"' +
            ' class="' + particleClass + '"' +
            ' fill="' + hexToRgba(COLORS[dim], isAgitated ? 0.5 + strength * 0.4 : 0.3 + strength * 0.3) + '"' +
            ' style="animation-delay: ' + delay.toFixed(2) + 's; animation-duration: ' + duration.toFixed(2) + 's;" />';
        }
      });

      // Orbiting particles around the core
      var orbitCount = Math.floor(avgScore / 10);
      for (var i = 0; i < orbitCount; i++) {
        var orbitR = 18 + rng() * 50;
        var speed = 8 + rng() * 12;
        var pSize = 0.5 + rng() * 0.9;
        var delay = rng() * speed;
        var allScored = scoredDims.length > 0 ? scoredDims : INNER_RING;
        var dimPick = allScored[Math.floor(rng() * allScored.length)];
        orbitParticles += '<circle cx="' + (CX + orbitR) + '" cy="' + CY + '" r="' + pSize + '"' +
          ' class="energy-particle-orbit"' +
          ' fill="' + hexToRgba(COLORS[dimPick], 0.35) + '"' +
          ' style="--orbit-r: ' + orbitR + 'px; --orbit-speed: ' + speed.toFixed(1) + 's; animation-delay: -' + delay.toFixed(1) + 's;" />';
      }
    }

    // --------------------------------------------------------
    // Labels (full mode: abbreviated labels + scores)
    // --------------------------------------------------------
    var labelsSvg = '';
    if (!badgeMode) {
      scoredDims.forEach(function(dim) {
        var rp = ringParams(dim);
        var angle = dimAngle(dim);
        var pos = polarToCart(angle, rp.labelR);
        var score = agent.scores[dim] || 0;
        var abbr = ABBREV[dim] || dim.slice(0, 3).toUpperCase();
        var color = COLORS[dim];
        var dimDrift = agent.drift ? (agent.drift[dim] || 0) : 0;

        // Abbreviated label with dimension color tint
        labelsSvg += '<text x="' + pos.x + '" y="' + pos.y + '"' +
          ' font-family="var(--font-mono)" font-size="9" font-weight="600"' +
          ' fill="' + hexToRgba(color, 0.85) + '"' +
          ' text-anchor="middle" dominant-baseline="middle"' +
          ' data-tooltip="' + dim + ': ' + score + '/100' + (dimDrift > 0 ? ' (drift: ' + dimDrift + ')' : '') + '">' +
          abbr + '</text>';

        // Score below label
        var driftIndicator = '';
        if (dimDrift > 5) {
          driftIndicator = ' <tspan fill="#ef4444" font-size="7">\u25BC' + dimDrift + '</tspan>';
        }
        labelsSvg += '<text x="' + pos.x + '" y="' + (pos.y + 11) + '"' +
          ' font-family="var(--font-mono)" font-size="8" fill="var(--text-muted)"' +
          ' text-anchor="middle" dominant-baseline="middle" opacity="0.65">' +
          Math.round(score) + driftIndicator + '</text>';
      });

      // Unscored inner-ring dimensions: faint dashed outline to show "not yet assessed"
      INNER_RING.forEach(function(dim) {
        if (agent.scores[dim] != null) return;
        var rp = ringParams(dim);
        var angle = dimAngle(dim);
        var rad = (angle - 90) * Math.PI / 180;
        var ghostR = rp.maxR * 0.4;
        var tipX = CX + Math.cos(rad) * ghostR;
        var tipY = CY + Math.sin(rad) * ghostR;
        var pos = polarToCart(angle, rp.labelR);
        var abbr = ABBREV[dim] || dim.slice(0, 3).toUpperCase();

        labelsSvg += '<line x1="' + CX + '" y1="' + CY + '" x2="' + tipX + '" y2="' + tipY + '"' +
          ' stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="3 6" />';
        labelsSvg += '<text x="' + pos.x + '" y="' + pos.y + '"' +
          ' font-family="var(--font-mono)" font-size="8" fill="rgba(255,255,255,0.2)"' +
          ' text-anchor="middle" dominant-baseline="middle">' + abbr + '</text>';
      });

      // Unscored outer-ring dimensions: same faint treatment so all 14 slots are always visible
      OUTER_RING.forEach(function(dim) {
        if (agent.scores[dim] != null) return;
        var rp = ringParams(dim);
        var angle = dimAngle(dim);
        var rad = (angle - 90) * Math.PI / 180;
        var ghostR = rp.maxR * 0.4;
        var tipX = CX + Math.cos(rad) * ghostR;
        var tipY = CY + Math.sin(rad) * ghostR;
        var pos = polarToCart(angle, rp.labelR);
        var abbr = ABBREV[dim] || dim.slice(0, 3).toUpperCase();

        labelsSvg += '<line x1="' + CX + '" y1="' + CY + '" x2="' + tipX + '" y2="' + tipY + '"' +
          ' stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="3 6" />';
        labelsSvg += '<text x="' + pos.x + '" y="' + pos.y + '"' +
          ' font-family="var(--font-mono)" font-size="8" fill="rgba(255,255,255,0.2)"' +
          ' text-anchor="middle" dominant-baseline="middle">' + abbr + '</text>';
      });
    }

    // --------------------------------------------------------
    // Core
    // --------------------------------------------------------
    var coreR = 8 + (avgScore / 100) * 16;
    var hasDrift = !!agent.drift;
    var maxDrift = hasDrift ? Math.max.apply(null, Object.values(agent.drift).concat([0])) : 0;
    var coreClass = maxDrift > 25 ? 'core-heartbeat-fast' : 'core-heartbeat';
    var groupClass = hasDrift ? '' : 'energy-field-animated';

    // --------------------------------------------------------
    // Halo ring around strongest dimension
    // --------------------------------------------------------
    var haloRing = '';
    if (scoredDims.length > 0) {
      var strongestDim = scoredDims.reduce(function(a, b) { return (agent.scores[a] || 0) >= (agent.scores[b] || 0) ? a : b; });
      var strongRp = ringParams(strongestDim);
      var strongAngle = dimAngle(strongestDim);
      var strongR = strongRp.maxR * ((agent.scores[strongestDim] || 0) / 100) * 0.5;
      var strongRad = (strongAngle - 90) * Math.PI / 180;
      var haloX = CX + Math.cos(strongRad) * strongR * 0.6;
      var haloY = CY + Math.sin(strongRad) * strongR * 0.6;
      if ((agent.scores[strongestDim] || 0) > 50) {
        haloRing = '<circle cx="' + haloX + '" cy="' + haloY + '" r="' + (strongR * 0.65) + '"' +
          ' fill="none" stroke="' + hexToRgba(COLORS[strongestDim], 0.1) + '"' +
          ' stroke-width="1.2" stroke-dasharray="4 6"' +
          ' style="animation: haloRing 4s ease-in-out infinite;" />';
      }
    }

    // --------------------------------------------------------
    // Assemble SVG
    // --------------------------------------------------------
    var outerRingContent = badgeMode ? buildOuterArcs() : outerLobes;

    container.innerHTML =
      '<svg viewBox="0 0 440 440" xmlns="http://www.w3.org/2000/svg">' +
        defs +

        '<!-- Background vignette -->' +
        '<circle cx="' + CX + '" cy="' + CY + '" r="' + (OUTER_MAX_R + 40) + '" fill="rgba(8,8,14,0.4)" filter="url(#' + uid + '-blur-soft)" />' +

        '<!-- Ambient haze -->' +
        ambientHaze +

        '<!-- Outer ring energy field -->' +
        '<g class="outer-ring-group ' + groupClass + '" style="animation: outerRingBreath 8s ease-in-out infinite;">' +
          outerRingContent +
        '</g>' +

        '<!-- Ring separator -->' +
        (badgeMode ? '' : buildRingSeparator()) +

        '<!-- Inner ring energy field -->' +
        '<g class="inner-ring-group ' + groupClass + '">' +
          innerLobes +
        '</g>' +

        '<!-- Coherence links -->' +
        coherenceOverlays +
        darkGaps +

        '<!-- Halo -->' +
        haloRing +

        '<!-- Core -->' +
        '<g class="' + coreClass + '">' +
          '<circle cx="' + CX + '" cy="' + CY + '" r="' + coreR + '" fill="url(#' + uid + '-core)" filter="url(#' + uid + '-glow)" />' +
          '<circle cx="' + CX + '" cy="' + CY + '" r="' + (coreR * 0.4) + '" fill="url(#' + uid + '-core-inner)" />' +
        '</g>' +

        '<!-- Orbiting particles -->' +
        orbitParticles +

        '<!-- Dimension particles -->' +
        particles +

        '<!-- Labels -->' +
        labelsSvg +

      '</svg>';
  }

  // ============================================================
  // Tooltip Handling
  // ============================================================

  function setupTooltips() {
    var tooltip = document.getElementById('aura-tooltip');
    if (!tooltip) return;

    document.querySelectorAll('[data-tooltip]').forEach(function(el) {
      el.addEventListener('mouseenter', function(e) {
        tooltip.textContent = e.target.getAttribute('data-tooltip') || '';
        tooltip.classList.add('visible');
      });
      el.addEventListener('mousemove', function(e) {
        tooltip.style.left = e.clientX + 14 + 'px';
        tooltip.style.top = e.clientY - 10 + 'px';
      });
      el.addEventListener('mouseleave', function() { tooltip.classList.remove('visible'); });
    });
  }

  // ============================================================
  // Sparkline Renderer
  // ============================================================

  function renderSparkline(containerId, history) {
    var el = document.getElementById(containerId);
    if (!el || history.length < 2) return;

    var w = 480, h = 60, pad = 4;
    var maxScore = Math.max(30, Math.max.apply(null, history.map(function(entry) { return entry.score; })));
    var points = history.map(function(entry, i) {
      var x = pad + (i / (history.length - 1)) * (w - 2 * pad);
      var y = h - pad - ((entry.score / maxScore) * (h - 2 * pad));
      return x.toFixed(1) + ',' + y.toFixed(1);
    });

    var latest = history[history.length - 1];
    var color = '#22c55e';
    if (latest.score > 60) color = '#ef4444';
    else if (latest.score > 35) color = '#f97316';
    else if (latest.score > 15) color = '#eab308';

    var threshY = h - pad - ((15 / maxScore) * (h - 2 * pad));
    var lastPoint = points[points.length - 1].split(',');

    el.innerHTML =
      '<svg viewBox="0 0 ' + w + ' ' + h + '" xmlns="http://www.w3.org/2000/svg">' +
        '<line x1="' + pad + '" y1="' + threshY + '" x2="' + (w - pad) + '" y2="' + threshY + '"' +
        ' stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="4 4" />' +
        '<polyline points="' + points.join(' ') + '"' +
        ' fill="none" stroke="' + color + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8" />' +
        '<circle cx="' + lastPoint[0] + '" cy="' + lastPoint[1] + '" r="3" fill="' + color + '" />' +
      '</svg>';
  }

  // ============================================================
  // Relative Time
  // ============================================================

  function relativeTime(isoString) {
    var then = new Date(isoString).getTime();
    var now = Date.now();
    var diffMs = now - then;
    var mins = Math.floor(diffMs / 60000);
    if (mins < 1) return 'just now';
    if (mins < 60) return mins + 'm ago';
    var hours = Math.floor(mins / 60);
    if (hours < 24) return hours + 'h ago';
    var days = Math.floor(hours / 24);
    return days + 'd ago';
  }

  // ============================================================
  // Public API
  // ============================================================

  return {
    // Dimension data
    DIMENSIONS: DIMENSIONS,
    INNER_RING: INNER_RING,
    OUTER_RING: OUTER_RING,
    LEGACY_DIMENSIONS: LEGACY_DIMENSIONS,
    COLORS: COLORS,
    ABBREV: ABBREV,
    DIM_IDX: DIM_IDX,
    DIM_RING: DIM_RING,
    RING_ANGLE: RING_ANGLE,
    OUTER_OFFSET: OUTER_OFFSET,

    // Layout constants
    CX: CX,
    CY: CY,
    MAX_R: MAX_R,
    LABEL_R: LABEL_R,
    INNER_MAX_R: INNER_MAX_R,
    OUTER_MAX_R: OUTER_MAX_R,

    // Functions
    polarToCart: polarToCart,
    dimAngle: dimAngle,
    ringParams: ringParams,
    getVertices: getVertices,
    getLabelPositions: getLabelPositions,
    blendColors: blendColors,
    hexToRgba: hexToRgba,
    seededRandom: seededRandom,
    renderEnergy: renderEnergy,
    setupTooltips: setupTooltips,
    renderSparkline: renderSparkline,
    relativeTime: relativeTime,
  };
})();
