/* global React, ReactDOM */

const { clampZoom, shouldShowBridge, shouldShowPractices } = window.AlignmentMapLogic;

// ── Geometry ──────────────────────────────────────────────────────────────
const R_ANCHOR = 380, R_VALUE_2 = 270, R_VALUE_1 = 170, R_INNER = 70;
const ANCHOR_BADGE_R = 26, PRACTICE_ORBIT = 46;
const TAU = Math.PI * 2;
const toRad = (d) => (d * Math.PI) / 180;
function polar(deg, r) { const a = toRad(deg); return { x: Math.cos(a) * r, y: Math.sin(a) * r }; }

const DOMAIN_CFG = {
  inner:         { angle: 270, rgb: '130,98,186'  },
  physical:      { angle: 330, rgb: '93,156,118'  },
  creative:      { angle:  30, rgb: '149,163,56'  },
  interpersonal: { angle:  90, rgb: '212,114,131' },
  adventure:     { angle: 150, rgb: '71,149,173'  },
  admin:         { angle: 210, rgb: '143,138,136' },
};

function domainColor(id, soft = false) {
  const map = {
    inner:         soft ? 'var(--dom-inner-soft)'         : 'var(--dom-inner)',
    physical:      soft ? 'var(--dom-physical-soft)'      : 'var(--dom-physical)',
    creative:      soft ? 'var(--dom-creative-soft)'      : 'var(--dom-creative)',
    interpersonal: soft ? 'var(--dom-interpersonal-soft)' : 'var(--dom-interpersonal)',
    adventure:     soft ? 'var(--dom-adventure-soft)'     : 'var(--dom-adventure)',
    admin:         soft ? 'var(--dom-admin-soft)'         : 'var(--dom-admin)',
  };
  return map[id] || 'var(--ink-3)';
}

const RING_SLOTS = [
  { ring: 2, offset: -14 }, { ring: 1, offset: 16 },
  { ring: 2, offset:  14 }, { ring: 1, offset: -16 },
];

function transformAPIData(apiDomains) {
  const domains = [], values = [], practices = [];
  apiDomains.forEach(apid => {
    const cfg = DOMAIN_CFG[apid.id];
    if (!cfg) return;
    domains.push({ id: apid.id, name: apid.label, angle: cfg.angle });
    (apid.values || []).forEach((apiv, idx) => {
      const slot = RING_SLOTS[idx % RING_SLOTS.length];
      values.push({
        id: apiv.id, name: apiv.name, domain: apid.id,
        ring: slot.ring, offset: slot.offset,
        velocity: apiv.velocity || 0, blurb: apiv.description || '',
        bridge: apiv.secondary_domain && apiv.secondary_domain !== apid.id ? apiv.secondary_domain : null,
      });
      (apiv.practices || []).forEach(apip => {
        practices.push({ id: apip.id, name: apip.label, value: apiv.id, velocity: apip.velocity || 0 });
      });
    });
  });
  return { domains, values, practices };
}

// ── SVG helpers ───────────────────────────────────────────────────────────
function starPath(r, n = 0.22) { const p = n * r; return `M 0 ${-r} L ${p} ${-p} L ${r} 0 L ${p} ${p} L 0 ${r} L ${-p} ${p} L ${-r} 0 L ${-p} ${-p} Z`; }
function tinyStar(r) { const p = r * 0.30; return `M 0 ${-r} L ${p} ${-p} L ${r} 0 L ${p} ${p} L 0 ${r} L ${-p} ${p} L ${-r} 0 L ${-p} ${-p} Z`; }

function velocityState(vel) {
  if (vel > 0.7) return 'Resonant';
  if (vel > 0.4) return 'Active';
  if (vel > 0.2) return 'Quiet';
  return 'Uncharted';
}

// star radius scales with velocity + number of connected practices (capped at 6)
function starRadius(velocity, practiceCount) {
  return 13 + velocity * 6 + Math.min(practiceCount, 6) * 2;
}

// stable 0..4s pulse phase per id so glows twinkle independently, never strobe in unison
function phaseFor(id) {
  let h = 0; const str = String(id);
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0;
  return (h % 400) / 100;
}

const DUST = (() => {
  const out = []; let s = 9123;
  const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
  for (let i = 0; i < 60; i++) {
    const r = R_INNER + 8 + rand() * (R_ANCHOR - R_INNER - 24), a = rand() * TAU;
    out.push({ x: Math.cos(a) * r, y: Math.sin(a) * r, r: rand() < 0.88 ? 0.6 : 1.2, o: 0.14 + rand() * 0.22 });
  }
  return out;
})();

// ── Graph components ──────────────────────────────────────────────────────
function SectorCone({ d, opacity = 1 }) {
  const a1 = toRad(d.angle - 30), a2 = toRad(d.angle + 30);
  const rOut = R_ANCHOR + 10, rIn = R_INNER * 0.4;
  const p1 = { x: Math.cos(a1)*rOut, y: Math.sin(a1)*rOut };
  const p2 = { x: Math.cos(a2)*rOut, y: Math.sin(a2)*rOut };
  const p3 = { x: Math.cos(a2)*rIn,  y: Math.sin(a2)*rIn  };
  const p4 = { x: Math.cos(a1)*rIn,  y: Math.sin(a1)*rIn  };
  const pt = polar(d.angle, R_ANCHOR);
  const id = `cone-${d.id}`;
  const rgb = (DOMAIN_CFG[d.id] || {}).rgb || '150,150,150';
  return (
    <React.Fragment>
      <defs>
        <linearGradient id={id} x1={pt.x} y1={pt.y} x2={0} y2={0} gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor={`rgb(${rgb})`} stopOpacity={0.42 * opacity} />
          <stop offset="40%"  stopColor={`rgb(${rgb})`} stopOpacity={0.18 * opacity} />
          <stop offset="100%" stopColor={`rgb(${rgb})`} stopOpacity={0} />
        </linearGradient>
      </defs>
      <path className="sector-cone"
        d={`M ${p1.x} ${p1.y} A ${rOut} ${rOut} 0 0 1 ${p2.x} ${p2.y} L ${p3.x} ${p3.y} A ${rIn} ${rIn} 0 0 0 ${p4.x} ${p4.y} Z`}
        fill={`url(#${id})`} />
    </React.Fragment>
  );
}

function PolarGrid({ showDust }) {
  return (
    <g>
      {Array.from({ length: 6 }).map((_, i) => {
        const a = toRad(-60 + i * 60);
        return <line key={i} className="grid-radial"
          x1={Math.cos(a)*R_INNER} y1={Math.sin(a)*R_INNER}
          x2={Math.cos(a)*(R_ANCHOR+30)} y2={Math.sin(a)*(R_ANCHOR+30)} />;
      })}
      {[R_INNER, R_VALUE_1, R_VALUE_2, R_ANCHOR].map((r, i) => (
        <circle key={i} cx="0" cy="0" r={r}
          className={`grid-ring ${i===3?'grid-ring--outer':''} ${i===2?'grid-ring--accent-gold':''}`} />
      ))}
      <circle cx={0} cy={0} r={6} fill="var(--gold-soft)" />
      <circle cx={0} cy={0} r={2.5} fill="var(--gold-1)" />
      {showDust && DUST.map((d, i) => (
        <circle key={`d${i}`} cx={d.x} cy={d.y} r={d.r} fill={`rgba(120,113,108,${d.o})`} />
      ))}
    </g>
  );
}

function DomainAnchor({ d, selected, onSelect }) {
  const { x, y } = polar(d.angle, R_ANCHOR);
  const out = polar(d.angle, R_ANCHOR + 52);
  const G = ANCHOR_BADGE_R * 0.55;
  let glyph;
  switch (d.id) {
    case 'inner': glyph = (<g fill="none" stroke="#fff" strokeWidth="1.4"><circle r={G}/><circle r={G*.55}/><circle r="1.5" fill="#fff" stroke="none"/></g>); break;
    case 'physical': glyph = (<g fill="none" stroke="#fff" strokeWidth="1.4" strokeLinecap="round"><path d={`M ${-G*.7} 0 A ${G*.7} ${G*.7} 0 1 1 ${G*.5} ${G*.5}`}/><path d={`M ${G*.2} ${G*.6} L ${G*.5} ${G*.5} L ${G*.45} ${G*.18}`}/></g>); break;
    case 'creative': glyph = (<path d={tinyStar(G*.8)} fill="#fff"/>); break;
    case 'interpersonal': glyph = (<g fill="none" stroke="#fff" strokeWidth="1.4"><circle cx={-G*.35} cy={0} r={G*.45}/><circle cx={G*.35} cy={0} r={G*.45}/></g>); break;
    case 'adventure': glyph = (<g fill="none" stroke="#fff" strokeWidth="1.4" strokeLinecap="round"><line x1={-G*.55} y1={G*.55} x2={G*.55} y2={-G*.55}/><polyline points={`${G*.15},${-G*.55} ${G*.55},${-G*.55} ${G*.55},${-G*.15}`}/></g>); break;
    case 'admin': glyph = (<g fill="none" stroke="#fff" strokeWidth="1.3"><rect x={-G*.6} y={-G*.6} width={G*.5} height={G*.5}/><rect x={G*.1} y={-G*.6} width={G*.5} height={G*.5}/><rect x={-G*.6} y={G*.1} width={G*.5} height={G*.5}/><rect x={G*.1} y={G*.1} width={G*.5} height={G*.5}/></g>); break;
    default: glyph = null;
  }
  const a = ((d.angle % 360) + 360) % 360;
  const anchor = (a > 90 && a < 270) ? 'end' : 'start';
  return (
    <g className={`domain ${selected ? 'domain--selected' : ''}`}
      onClick={(e) => { e.stopPropagation(); onSelect(d); }}>
      {selected && (
        <React.Fragment>
          <circle className="select-ring select-ring--outer" cx={x} cy={y} r={ANCHOR_BADGE_R + 12}
            fill="none" stroke="var(--gold-1)" strokeWidth="1" strokeOpacity="0.4" />
          <circle className="select-ring" cx={x} cy={y} r={ANCHOR_BADGE_R + 6}
            fill="none" stroke="var(--gold-1)" strokeWidth="2" />
        </React.Fragment>
      )}
      <circle className="domain__badge" cx={x} cy={y} r={ANCHOR_BADGE_R} fill={domainColor(d.id)} />
      <g transform={`translate(${x},${y})`}>{glyph}</g>
      <text className="domain__name" x={out.x} y={out.y+4} textAnchor={anchor}>{d.name}</text>
    </g>
  );
}

function StructuralLine({ v, domain, accent }) {
  const a  = polar(domain.angle, R_ANCHOR - ANCHOR_BADGE_R);
  const vp = polar(domain.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2);

  // control point: pulled inward along domain radial axis for the swoop
  const ax   = polar(domain.angle, (R_ANCHOR - ANCHOR_BADGE_R) * 0.72);
  const mid  = { x: (a.x+vp.x)/2, y: (a.y+vp.y)/2 };
  const ctrlX = (mid.x + ax.x) / 2, ctrlY = (mid.y + ax.y) / 2;

  // perpendicular at the domain-anchor end for taper width
  const dx = vp.x - a.x, dy = vp.y - a.y;
  const len = Math.sqrt(dx*dx + dy*dy);
  const px = -dy/len, py = dx/len;
  const hw = accent ? 14 : 9;

  const al = { x: a.x + px*hw, y: a.y + py*hw };
  const ar = { x: a.x - px*hw, y: a.y - py*hw };
  // both edges share the same control point → tapered teardrop swoop
  const pathD = `M ${al.x} ${al.y} Q ${ctrlX} ${ctrlY} ${vp.x} ${vp.y} Q ${ctrlX} ${ctrlY} ${ar.x} ${ar.y} Z`;

  const gid = `sl-grad-${v.id}`;
  const rgb = (DOMAIN_CFG[domain.id] || {}).rgb || '150,150,150';
  return (
    <React.Fragment>
      <defs>
        <linearGradient id={gid} x1={a.x} y1={a.y} x2={vp.x} y2={vp.y} gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor={`rgb(${rgb})`} stopOpacity={accent ? 0.85 : 0.62} />
          <stop offset="70%"  stopColor={`rgb(${rgb})`} stopOpacity={0.35} />
          <stop offset="100%" stopColor={`rgb(${rgb})`} stopOpacity={0.18} />
        </linearGradient>
      </defs>
      <path className={`structural-line ${accent ? 'structural-line--accent' : ''}`}
        d={pathD} fill={`url(#${gid})`} />
    </React.Fragment>
  );
}

function ValueNode({ v, domain, selected, hovered, auraIntensity, practiceCount, scale, onSelect, onHover }) {
  const ba = domain.angle + v.offset, r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2;
  const { x, y } = polar(ba, r);
  const sr = starRadius(v.velocity, practiceCount);
  // gentle size growth as zoom increases — cube-root curve, not linear
  const s = scale || 1;
  const displaySr = sr * (s > 1 ? 1 + 0.18 * Math.cbrt(s - 1) : 1);
  const auraR = 26 + v.velocity * 26 + Math.min(practiceCount, 6) * 2;
  const rgb = (DOMAIN_CFG[v.domain] || {}).rgb || '150,150,150';
  const iStop = (0.45 + v.velocity * 0.45) * auraIntensity;
  const mStop = (0.18 + v.velocity * 0.30) * auraIntensity;
  const gid  = `vglow-${v.id}`;
  const sgid = `vstar-${v.id}`;
  // gradient opacity grows with zoom but eases off — log2 curve, capped
  const gradOpacity = Math.min(0.62, 0.08 + 0.22 * Math.log2(Math.max(1, s)));
  return (
    <g className={`value ${selected ? 'value--selected' : ''}`}
      onClick={(e) => { e.stopPropagation(); onSelect(v); }}
      onMouseEnter={() => onHover(v.id)} onMouseLeave={() => onHover(null)}>
      {auraIntensity > 0 && (
        <React.Fragment>
          <defs>
            <radialGradient id={gid}>
              <stop offset="0%"   stopColor={`rgb(${rgb})`} stopOpacity={iStop} />
              <stop offset="45%"  stopColor={`rgb(${rgb})`} stopOpacity={mStop} />
              <stop offset="100%" stopColor={`rgb(${rgb})`} stopOpacity="0" />
            </radialGradient>
          </defs>
          <circle className="value__glow" cx={x} cy={y} r={auraR} fill={`url(#${gid})`}
            style={{ '--aura-min': v.velocity > 0.6 ? 0.80 : 0.60, '--aura-max': 1.0,
                     animationDelay: `${phaseFor(v.id)}s` }} />
        </React.Fragment>
      )}
      <g transform={`translate(${x},${y})`}>
        <defs>
          <radialGradient id={sgid} gradientUnits="userSpaceOnUse" cx="0" cy="0" r={displaySr}>
            <stop offset="0%"   stopColor={`rgb(${rgb})`} stopOpacity={gradOpacity} />
            <stop offset="55%"  stopColor={`rgb(${rgb})`} stopOpacity={gradOpacity * 0.35} />
            <stop offset="100%" stopColor={`rgb(${rgb})`} stopOpacity="0" />
          </radialGradient>
        </defs>
        {selected && (
          <React.Fragment>
            <circle className="select-ring select-ring--outer" cx="0" cy="0" r={displaySr + 16}
              fill="none" stroke="var(--gold-1)" strokeWidth="1" strokeOpacity="0.35" />
            <circle className="select-ring" cx="0" cy="0" r={displaySr + 8}
              fill="none" stroke="var(--gold-1)" strokeWidth="2" />
          </React.Fragment>
        )}
        <path className="value__star-fill" d={starPath(displaySr)} stroke={domainColor(v.domain)}
          fill={`url(#${sgid})`} />
      </g>
    </g>
  );
}

// angle normalised to (-180, 180] relative to a base
function relAngle(a, base) { return ((a - base + 180) % 360 + 360) % 360 - 180; }

function practicePos(v, domain, idx, total) {
  const r   = v.ring === 1 ? R_VALUE_1 : R_VALUE_2;
  const vc  = polar(domain.angle + v.offset, r);
  const sr  = starRadius(v.velocity, total);
  const orbitR = v.ring === 1 ? r - sr - 30 : r + sr + 50;

  // domain slice is ±28° from domain.angle (leaving 2° buffer at each boundary)
  const SLICE = 28;
  // fan width scales with practice count but never exceeds the slice
  const span = Math.min(SLICE * 1.6, 10 + total * 11);

  // start fan centered on v.offset within the domain
  let centerOffset = v.offset;

  // if this value bridges to a secondary domain, pull the fan toward that boundary
  if (v.bridge && DOMAIN_CFG[v.bridge]) {
    const bridgeAngle = DOMAIN_CFG[v.bridge].angle;
    const diff = relAngle(bridgeAngle, domain.angle); // + = bridge is CW from domain
    const nearBoundary = diff > 0 ? SLICE : -SLICE;
    // shift center 35% of the way toward the near boundary
    centerOffset += (nearBoundary - centerOffset) * 0.35;
  }

  // clamp fan so it stays inside the slice
  const halfSpan = span / 2;
  const lo = Math.max(-SLICE, centerOffset - halfSpan);
  const hi = Math.min( SLICE, centerOffset + halfSpan);
  const actualSpan = hi - lo;

  const angleOffset = total === 1 ? (lo + hi) / 2 : lo + (actualSpan / (total - 1)) * idx;
  const angle = domain.angle + angleOffset;
  const pos = polar(angle, orbitR);
  return { x: pos.x, y: pos.y, vx: vc.x, vy: vc.y, angle };
}

function computeAdjustedPracticePositions(values, domains, practicesByValue) {
  const SLICE = 28;

  // per-value safe zone (domain-relative offsets) — practices can't cross
  // into a neighbouring value's structural stream within the same domain
  const valueSafeZones = {};
  domains.forEach(d => {
    const dVals = values.filter(v => v.domain === d.id)
                        .sort((a, b) => a.offset - b.offset);
    dVals.forEach((v, i) => {
      const prev = dVals[i - 1];
      const next = dVals[i + 1];
      const lo = prev ? (v.offset + prev.offset) / 2 : -SLICE;
      const hi = next ? (v.offset + next.offset) / 2 :  SLICE;
      valueSafeZones[v.id] = { lo: Math.max(-SLICE, lo - 1), hi: Math.min(SLICE, hi + 1) };
    });
  });

  // label bounding boxes — what practice dots must avoid
  const labelBoxes = values.map(v => {
    const d = domains.find(dd => dd.id === v.domain); if (!d) return null;
    const ba = d.angle + v.offset;
    const r  = v.ring === 1 ? R_VALUE_1 : R_VALUE_2;
    const pracs = practicesByValue[v.id] || [];
    const sr = starRadius(v.velocity, pracs.length);
    const lp = polar(ba, r + sr + 16);
    const lines = splitLabel(v.name);
    const maxLen = lines[1] ? Math.max(lines[0].length, lines[1].length) : lines[0].length;
    return { x: lp.x, y: lp.y, valueId: v.id,
             hw: maxLen * 4.5 + 12, hh: lines[1] ? 22 : 12 };
  }).filter(Boolean);

  // initial dot positions + orbit metadata
  const slots = [];
  values.forEach(v => {
    const d = domains.find(dd => dd.id === v.domain); if (!d) return;
    const pracs = practicesByValue[v.id] || [];
    const r  = v.ring === 1 ? R_VALUE_1 : R_VALUE_2;
    const sr = starRadius(v.velocity, pracs.length);
    const orbitR = v.ring === 1 ? r - sr - 30 : r + sr + 50;
    const vc = polar(d.angle + v.offset, r);
    pracs.forEach((p, i) => {
      const raw = practicePos(v, d, i, pracs.length);
      const angleDeg = Math.atan2(raw.y, raw.x) * 180 / Math.PI;
      slots.push({ pid: p.id, x: raw.x, y: raw.y, vx: vc.x, vy: vc.y,
                   orbitR, angleDeg, domAngle: d.angle, valueId: v.id });
    });
  });

  // repulsion — push dots away from value labels AND away from each other
  const MIN_LABEL_DIST = 50;
  const MIN_DOT_DIST   = 24; // 2 * dot radius + comfortable gap
  for (let iter = 0; iter < 60; iter++) {
    let moved = false;
    // label repulsion
    slots.forEach(s => {
      labelBoxes.forEach(lb => {
        const dx = s.x - lb.x, dy = s.y - lb.y;
        const bx = Math.max(0, Math.abs(dx) - lb.hw);
        const by = Math.max(0, Math.abs(dy) - lb.hh);
        const dist = Math.sqrt(bx * bx + by * by);
        if (dist < MIN_LABEL_DIST) {
          moved = true;
          const labelDeg = Math.atan2(lb.y, lb.x) * 180 / Math.PI;
          const nd = ((s.angleDeg - labelDeg + 180) % 360 + 360) % 360 - 180;
          s.angleDeg += nd >= 0 ? 3 : -3;
          const np = polar(s.angleDeg, s.orbitR);
          s.x = np.x; s.y = np.y;
        }
      });
    });
    // dot-to-dot repulsion
    for (let i = 0; i < slots.length; i++) {
      for (let j = i + 1; j < slots.length; j++) {
        const a = slots[i], b = slots[j];
        const dx = a.x - b.x, dy = a.y - b.y;
        const dist = Math.sqrt(dx * dx + dy * dy);
        if (dist < MIN_DOT_DIST && dist > 0) {
          moved = true;
          // push each dot along its orbit arc away from the other
          const toward_b = Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
          const na = relAngle(toward_b, a.angleDeg);
          a.angleDeg += na >= 0 ? -2 : 2;
          const ap = polar(a.angleDeg, a.orbitR);
          a.x = ap.x; a.y = ap.y;

          const toward_a = Math.atan2(a.y - b.y, a.x - b.x) * 180 / Math.PI;
          const nb = relAngle(toward_a, b.angleDeg);
          b.angleDeg += nb >= 0 ? -2 : 2;
          const bp = polar(b.angleDeg, b.orbitR);
          b.x = bp.x; b.y = bp.y;
        }
      }
    }
    if (!moved) break;
  }

  // clamp each dot to its value's safe zone (respects domain boundary + no stream-crossing)
  slots.forEach(s => {
    const zone = valueSafeZones[s.valueId] || { lo: -SLICE, hi: SLICE };
    const offset = relAngle(s.angleDeg, s.domAngle);
    const clamped = Math.max(zone.lo, Math.min(zone.hi, offset));
    if (Math.abs(offset - clamped) > 0.01) {
      s.angleDeg = s.domAngle + clamped;
      const np = polar(s.angleDeg, s.orbitR);
      s.x = np.x; s.y = np.y;
    }
  });

  const map = {};
  slots.forEach(s => { map[s.pid] = { x: s.x, y: s.y, vx: s.vx, vy: s.vy, angle: s.angleDeg }; });
  return map;
}

function PracticeNode({ p, v, domain, idx, total, pos, visible, selected, onSelect }) {
  const { x, y, vx, vy } = pos || practicePos(v, domain, idx, total);
  const rgb = (DOMAIN_CFG[v.domain] || {}).rgb || '150,150,150';
  const handleClick = onSelect ? (e) => { e.stopPropagation(); onSelect(p); } : undefined;
  return (
    <g className="practice"
      style={{ opacity: visible ? 1 : 0, pointerEvents: visible ? 'auto' : 'none', cursor: onSelect ? 'pointer' : 'default' }}
      onClick={handleClick}>
      <line className="practice__line" x1={vx} y1={vy} x2={x} y2={y}
        stroke={domainColor(v.domain)} style={{ opacity: 0.5 }} />
      {selected && (
        <React.Fragment>
          <circle className="select-ring select-ring--outer" cx={x} cy={y} r={13}
            fill="none" stroke="var(--gold-1)" strokeWidth="1" strokeOpacity="0.4" />
          <circle className="select-ring" cx={x} cy={y} r={8}
            fill="none" stroke="var(--gold-1)" strokeWidth="2" />
        </React.Fragment>
      )}
      <circle className="practice__dot" cx={x} cy={y} r={4.5}
        fill={p.velocity > 0.6 ? `rgb(${rgb})` : 'var(--ink-3)'} />
    </g>
  );
}

function Bridge({ v, fromDomain, toDomain, visible }) {
  const from = polar(fromDomain.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2);
  const to   = polar(toDomain.angle, R_ANCHOR - ANCHOR_BADGE_R);
  const cx = (from.x+to.x)*.3, cy = (from.y+to.y)*.3;
  return <path className={`bridge ${visible ? 'bridge--visible' : ''}`}
    d={`M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`}
    stroke={domainColor(toDomain.id)} strokeDasharray="5 5" />;
}


// ── Label helpers ────────────────────────────────────────────────────────────
function splitLabel(text, maxChars = 13) {
  const words = text.split(' ');
  if (text.length <= maxChars || words.length < 2) return [text, null];
  let best = 1, bestDiff = Infinity;
  for (let i = 1; i < words.length; i++) {
    const diff = Math.abs(words.slice(0,i).join(' ').length - words.slice(i).join(' ').length);
    if (diff < bestDiff) { bestDiff = diff; best = i; }
  }
  return [words.slice(0, best).join(' '), words.slice(best).join(' ')];
}

// ── Label collision avoidance ─────────────────────────────────────────────
function computeRawLabels(values, domains, practicesByValue, practicePositions) {
  const labels = [];
  values.forEach(v => {
    const d = domains.find(dd => dd.id === v.domain);
    if (!d) return;
    const ba = d.angle + v.offset;
    const r  = v.ring === 1 ? R_VALUE_1 : R_VALUE_2;
    const pracs = practicesByValue[v.id] || [];
    const sr = starRadius(v.velocity, pracs.length);
    const lp = polar(ba, r + sr + 16);
    const a  = ((ba % 360) + 360) % 360;
    const lines = splitLabel(v.name);
    labels.push({
      id: `v-${v.id}`, x: lp.x, y: lp.y + (lines[1] ? -8 : 4),
      text: v.name, lines, anchor: 'middle',
      cls: 'value__label', type: 'value', charW: 8.5, valueId: v.id,
    });
    pracs.forEach((p, i) => {
      const { x, y, angle } = (practicePositions && practicePositions[p.id]) || practicePos(v, d, i, pracs.length);
      const pa = ((angle % 360) + 360) % 360;
      const anchor = (pa > 90 && pa < 270) ? 'end' : 'start';
      labels.push({
        id: `p-${p.id}`, x: anchor === 'end' ? x - 14 : x + 14, y: y + 4,
        text: p.name, anchor,
        cls: 'practice__label', type: 'practice', charW: 6.2, valueId: v.id,
      });
    });
  });
  return labels;
}

function resolveCollisions(labels, iters = 28) {
  if (!labels.length) return labels;
  const LINE_H = 18, PAD = 3;
  const b = labels.map(l => {
    const maxLen = l.lines
      ? Math.max(l.lines[0].length, l.lines[1] ? l.lines[1].length : 0)
      : l.text.length;
    const w  = maxLen * l.charW + PAD * 2;
    const h  = (l.lines && l.lines[1]) ? LINE_H * 2 + 6 : LINE_H;
    const lx = l.anchor === 'end'    ? l.x - w + PAD
             : l.anchor === 'middle' ? l.x - w / 2
             :                         l.x - PAD;
    // store original position so we can clamp drift
    return { ...l, w, h, lx, ty: l.y - LINE_H + 2, ox: l.x, oy: l.y };
  });
  for (let it = 0; it < iters; it++) {
    let moved = false;
    for (let i = 0; i < b.length; i++) {
      for (let j = i + 1; j < b.length; j++) {
        const a = b[i], c = b[j];
        const ox = Math.min(a.lx + a.w, c.lx + c.w) - Math.max(a.lx, c.lx);
        const oy = Math.min(a.ty + a.h, c.ty + c.h) - Math.max(a.ty, c.ty);
        if (ox > 0 && oy > 0) {
          moved = true;
          if (oy <= ox) {
            const push = oy / 2 + 1;
            if (a.y <= c.y) { b[i].y -= push; b[i].ty -= push; b[j].y += push; b[j].ty += push; }
            else             { b[i].y += push; b[i].ty += push; b[j].y -= push; b[j].ty -= push; }
          } else {
            const push = ox / 2 + 1;
            if (a.lx <= c.lx) { b[i].x -= push; b[i].lx -= push; b[j].x += push; b[j].lx += push; }
            else               { b[i].x += push; b[i].lx += push; b[j].x -= push; b[j].lx -= push; }
          }
        }
      }
    }
    if (!moved) break;
  }
  // clamp each label to max drift from its home — keeps labels anchored near their star/dot
  b.forEach(l => {
    const maxDrift = l.type === 'value' ? 42 : 22;
    const dx = l.x - l.ox, dy = l.y - l.oy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist > maxDrift) {
      const s = maxDrift / dist;
      const nx = l.ox + dx * s, ny = l.oy + dy * s;
      const ddx = nx - l.x, ddy = ny - l.y;
      l.x = nx; l.y = ny; l.lx += ddx; l.ty += ddy;
    }
  });
  return b;
}

function LabelLayer({ values, domains, practicesByValue, practicePositions, zoom, scale, selectedValueId, hoveredValueId, showValues, showPractices }) {
  const labels = React.useMemo(() => {
    const raw = computeRawLabels(values, domains, practicesByValue, practicePositions);
    return resolveCollisions(raw);
  }, [values, domains, practicesByValue, practicePositions]);

  return (
    <g style={{ pointerEvents: 'none' }}>
      {labels.map(l => {
        const active = l.valueId === selectedValueId || l.valueId === hoveredValueId;
        const zoomThreshold = l.type === 'value' ? 1.1 : 1.6;
        const isPractice = l.type === 'practice';
        const visible = isPractice ? showPractices : showValues;
        const opacity = visible && (active || zoom >= zoomThreshold) ? 1 : 0;
        const isVal = l.type === 'value';
        const fs = (isVal ? 15 : 11) / scale;
        const sw = (isVal ? 2.5 : 1.5) / scale;
        const dy = 20 / scale;
        if (isVal && l.lines && l.lines[1]) {
          return (
            <text key={l.id} className={l.cls} textAnchor="middle"
              style={{ transform: `translate(${l.x}px,${l.y}px)`, opacity,
                       transition: 'opacity 250ms ease, transform 320ms cubic-bezier(0.2,0,0.2,1)',
                       fontSize: `${fs}px`, strokeWidth: sw }}
              x="0" y="0">
              <tspan x="0" dy="0">{l.lines[0]}</tspan>
              <tspan x="0" dy={dy}>{l.lines[1]}</tspan>
            </text>
          );
        }
        return (
          <text key={l.id} className={l.cls} textAnchor={l.anchor}
            style={{ transform: `translate(${l.x}px,${l.y}px)`, opacity,
                     transition: 'opacity 250ms ease, transform 320ms cubic-bezier(0.2,0,0.2,1)',
                     fontSize: `${fs}px`, strokeWidth: sw }}
            x="0" y="0">{l.text}</text>
        );
      })}
    </g>
  );
}

// ── AlignmentGraph ────────────────────────────────────────────────────────
function AlignmentGraph({
  domains, values, practices,
  selectedValueId, selectedDomainId, selectedPracticeId, hoveredValueId,
  zoom, pan, onZoomChange, onPanChange,
  showValues, showPractices,
  onSelectValue, onSelectDomain, onHoverValue, onClearSelection, onSelectPractice,
}) {
  const svgRef = React.useRef(null);
  const drag   = React.useRef(null);
  const [transitioning, setTransitioning] = React.useState(false);
  const [size, setSize] = React.useState({ w: 1000, h: 800 });

  const practicesByValue = React.useMemo(() => {
    const map = {};
    practices.forEach(p => { if (!map[p.value]) map[p.value] = []; map[p.value].push(p); });
    return map;
  }, [practices]);

  const practicePositions = React.useMemo(() =>
    computeAdjustedPracticePositions(values, domains, practicesByValue),
    [values, domains, practicesByValue]
  );

  React.useEffect(() => {
    const update = () => {
      if (!svgRef.current) return;
      const r = svgRef.current.getBoundingClientRect();
      setSize({ w: r.width, h: r.height });
    };
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  React.useEffect(() => {
    if (!selectedValueId && !selectedDomainId && !selectedPracticeId) return;
    const autoFit = Math.max(0.5, Math.min(1, (Math.min(size.h/2, size.w/2) - 96) / R_ANCHOR));
    const scale = autoFit * zoom;
    let tx = null, ty = null;
    if (selectedValueId) {
      const v = values.find(vv => vv.id === selectedValueId);
      const d = v && domains.find(dd => dd.id === v.domain);
      if (v && d) { const p = polar(d.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2); tx = p.x; ty = p.y; }
    } else if (selectedDomainId) {
      const d = domains.find(dd => dd.id === selectedDomainId);
      if (d) { const p = polar(d.angle, R_ANCHOR * 0.65); tx = p.x; ty = p.y; }
    } else if (selectedPracticeId) {
      const p = practices.find(pp => pp.id === selectedPracticeId);
      const v = p && values.find(vv => vv.id === p.value);
      const d = v && domains.find(dd => dd.id === v.domain);
      if (p && v && d) {
        const pracs = practicesByValue[v.id] || [];
        const idx = pracs.findIndex(pp => pp.id === p.id);
        const pos = practicePos(v, d, idx < 0 ? 0 : idx, pracs.length);
        tx = pos.x; ty = pos.y;
      }
    }
    if (tx !== null) {
      setTransitioning(true);
      onPanChange({ x: -tx * scale, y: -ty * scale });
      const t = setTimeout(() => setTransitioning(false), 520);
      return () => clearTimeout(t);
    }
  }, [selectedValueId, selectedDomainId, selectedPracticeId]);

  const onMouseDown = (e) => {
    if (e.button !== 0) return;
    drag.current = { x: e.clientX, y: e.clientY, panX: pan.x, panY: pan.y, moved: 0 };
    e.currentTarget.classList.add('dragging');
  };
  const onMouseMove = (e) => {
    if (!drag.current) return;
    const dx = e.clientX - drag.current.x, dy = e.clientY - drag.current.y;
    drag.current.moved = Math.hypot(dx, dy);
    if (drag.current.moved > 3) setTransitioning(false);
    onPanChange({ x: drag.current.panX + dx, y: drag.current.panY + dy });
  };
  const onMouseUp = (e) => {
    if (!drag.current) return;
    const moved = drag.current.moved; drag.current = null;
    e.currentTarget.classList.remove('dragging');
    if (moved < 4 && e.target.tagName === 'svg') onClearSelection();
  };
  const onWheel = (e) => {
    e.preventDefault();
    setTransitioning(false);
    onZoomChange(clampZoom(zoom * (1 + -e.deltaY * 0.0015)));
  };

  const autoFit = Math.max(0.5, Math.min(1, (Math.min(size.h/2, size.w/2) - 96) / R_ANCHOR));
  const scale = autoFit * zoom;
  const cx = size.w/2 + pan.x, cy = size.h/2 + pan.y;

  return (
    <svg ref={svgRef} className="graph"
      onMouseDown={onMouseDown} onMouseMove={onMouseMove}
      onMouseUp={onMouseUp} onMouseLeave={() => { drag.current = null; }}
      onWheel={onWheel}>
      <g style={{
        transform: `translate(${cx}px,${cy}px) scale(${scale})`,
        transformOrigin: '0px 0px',
        transition: transitioning ? 'transform 480ms cubic-bezier(0.4,0,0.2,1)' : 'none',
      }}>
        {domains.map(d => <SectorCone key={d.id} d={d} opacity={1} />)}
        <PolarGrid showDust={true} />

        {showValues && values.map(v => {
          const d = domains.find(dd => dd.id === v.domain); if (!d) return null;
          const accent = selectedValueId === v.id || hoveredValueId === v.id || selectedDomainId === v.domain;
          return <StructuralLine key={`sl-${v.id}`} v={v} domain={d} accent={accent} />;
        })}

        {showValues && values.filter(v => v.bridge).map(v => {
          const from = domains.find(d => d.id === v.domain);
          const to   = domains.find(d => d.id === v.bridge);
          if (!from || !to) return null;
          return <Bridge key={`br-${v.id}`} v={v} fromDomain={from} toDomain={to}
                   visible={shouldShowBridge({ valueId: v.id, hoveredValueId, selectedValueId })} />;
        })}

        {/* Domain anchors rendered before value/practice nodes so value labels appear on top */}
        {domains.map(d => (
          <DomainAnchor key={d.id} d={d} selected={selectedDomainId === d.id} onSelect={onSelectDomain} />
        ))}

        {showPractices && values.map(v => {
          const d = domains.find(dd => dd.id === v.domain); if (!d) return null;
          return (practicesByValue[v.id] || []).map((p, i, arr) => (
            <PracticeNode key={p.id} p={p} v={v} domain={d} idx={i} total={arr.length}
              pos={practicePositions[p.id]}
              visible={shouldShowPractices({ zoomLevel: zoom, selectedValueId, valueId: v.id })}
              selected={selectedPracticeId === p.id}
              onSelect={onSelectPractice} />
          ));
        })}

        {showValues && values.map(v => {
          const d = domains.find(dd => dd.id === v.domain); if (!d) return null;
          return <ValueNode key={v.id} v={v} domain={d}
                   selected={selectedValueId === v.id} hovered={hoveredValueId === v.id}
                   auraIntensity={1} practiceCount={(practicesByValue[v.id] || []).length}
                   scale={scale}
                   onSelect={onSelectValue} onHover={onHoverValue} />;
        })}

        <LabelLayer values={values} domains={domains}
          practicesByValue={practicesByValue} practicePositions={practicePositions}
          zoom={zoom} scale={scale}
          selectedValueId={selectedValueId} hoveredValueId={hoveredValueId}
          showValues={showValues} showPractices={showPractices} />
      </g>
    </svg>
  );
}

// ── Navigation sidebar ────────────────────────────────────────────────────
function NavSidebar() {
  const items = [
    { icon: 'dashboard',  label: 'Dashboard',       href: '/dashboard-ui/' },
    { icon: 'star-maps',  label: 'Alignment Map',   href: '/dashboard-ui/map', active: true },
    { icon: 'manuscripts',label: 'Weekly Review',   href: '#' },
    { icon: 'book',       label: 'Monthly Review',  href: '#' },
    { icon: 'star-maps',  label: 'Quarterly Review',href: '#' },
  ];
  return (
    <aside className="sidebar">
      <div className="sidebar__lock">
        <h1>SOULITICS</h1>
        <p>Cartographer's Console</p>
      </div>
      <nav className="sidenav">
        {items.map(item => (
          <a key={item.label} href={item.href}
            className={`sidenav__item${item.active ? ' is-active' : ''}`}>
            <span className="l">
              <span className="sidenav__ico" style={{ '--ico': `url(/dashboard-ui/assets/icons/${item.icon}.svg)` }} />
              <span className="sidenav__lbl">{item.label}</span>
            </span>
            {item.active && <span className="dot" />}
          </a>
        ))}
      </nav>
      <button className="sidebar__cta">New Entry</button>
      <div className="sidebar__footer">
        <a href="#" className="sidenav__item">
          <span className="l">
            <span className="sidenav__ico" style={{ '--ico': 'url(/dashboard-ui/assets/icons/settings.svg)' }} />
            <span className="sidenav__lbl">Settings</span>
          </span>
        </a>
        <a href="#" className="sidenav__item">
          <span className="l">
            <span className="sidenav__ico" style={{ '--ico': 'url(/dashboard-ui/assets/icons/support.svg)' }} />
            <span className="sidenav__lbl">Support</span>
          </span>
        </a>
      </div>
    </aside>
  );
}

// ── Right panel ───────────────────────────────────────────────────────────
function RightPanel({
  selectedValue, selectedDomain, selectedPractice,
  domains, values, practices, unmappedItems,
  editMode, onToggleEdit, onClose,
  pickedUnmappedId, onPickUnmapped, onAssign,
  totalValues, resonantValues,
  onSelectValue, onSelectDomain, onSelectPractice,
}) {
  const pickedItem = unmappedItems.find(i => i.id === pickedUnmappedId);

  let head;
  if (selectedPractice) {
    const parentValue = values.find(v => v.id === selectedPractice.value);
    const parentDomain = parentValue ? domains.find(d => d.id === parentValue.domain) : null;
    head = (
      <React.Fragment>
        <div className="right-panel__eyebrow">
          {parentDomain && <span className="dot" style={{ background: domainColor(parentDomain.id) }}></span>}
          <span>{parentValue ? parentValue.name : 'Practice'}</span>
          {parentDomain && <React.Fragment><span style={{ color: 'var(--stone-3)' }}>·</span><span>{parentDomain.name}</span></React.Fragment>}
        </div>
        <h2 className="right-panel__title">{selectedPractice.name}</h2>
      </React.Fragment>
    );
  } else if (selectedValue) {
    const dom = domains.find(d => d.id === selectedValue.domain);
    head = (
      <React.Fragment>
        <div className="right-panel__eyebrow">
          {dom && <span className="dot" style={{ background: domainColor(dom.id) }}></span>}
          <span>{dom ? dom.name : 'Value'}</span>
        </div>
        <h2 className="right-panel__title">{selectedValue.name}</h2>
      </React.Fragment>
    );
  } else if (selectedDomain) {
    head = (
      <React.Fragment>
        <div className="right-panel__eyebrow">
          <span className="dot" style={{ background: domainColor(selectedDomain.id) }}></span>
          <span>Domain</span>
        </div>
        <h2 className="right-panel__title">{selectedDomain.name}</h2>
      </React.Fragment>
    );
  } else {
    head = (
      <React.Fragment>
        <div className="right-panel__eyebrow">Identity Architecture</div>
        <h2 className="right-panel__title">Overview</h2>
      </React.Fragment>
    );
  }

  return (
    <aside className="right-panel">
      <div className="right-panel__head">{head}</div>

      <div className="right-panel__body">
        {selectedPractice && (
          <PracticeBody p={selectedPractice} values={values} domains={domains}
            onSelectValue={onSelectValue} onSelectDomain={onSelectDomain} />
        )}
        {!selectedPractice && selectedValue && (
          <ValueBody v={selectedValue} domains={domains} practices={practices}
            editMode={editMode} pickedItem={pickedItem} onAssign={onAssign}
            onSelectPractice={onSelectPractice} onSelectDomain={onSelectDomain} />
        )}
        {!selectedPractice && !selectedValue && selectedDomain && (
          <DomainBody d={selectedDomain} values={values} practices={practices}
            onSelectValue={onSelectValue} onSelectPractice={onSelectPractice} />
        )}
        {!selectedPractice && !selectedValue && !selectedDomain && (
          <OverviewBody totalValues={totalValues} resonantValues={resonantValues}
            practices={practices} unmappedItems={unmappedItems}
            editMode={editMode} pickedUnmappedId={pickedUnmappedId}
            onPickUnmapped={onPickUnmapped} />
        )}
      </div>

      <div className="right-panel__edit" onClick={onToggleEdit}>
        <div className={`toggle-switch ${editMode ? 'toggle-switch--on' : ''}`}>
          <div className="toggle-switch__knob"></div>
        </div>
        <div className="label">
          <span className="k">Edit Mode · {editMode ? 'On' : 'Off'}</span>
          <span className="v">{editMode ? 'Structural assignments unlocked.' : 'Calm sanctuary. Read-only.'}</span>
        </div>
      </div>
    </aside>
  );
}

// ── Panel body components ─────────────────────────────────────────────────

function OverviewBody({ totalValues, resonantValues, practices, unmappedItems, editMode, pickedUnmappedId, onPickUnmapped }) {
  return (
    <React.Fragment>
      <div className="panel-section">
        <div className="panel-section__title">This Cycle</div>
        <div className="stats-grid">
          <div className="stat-row"><span className="k">Active Values</span><span className="v">{totalValues}</span></div>
          <div className="stat-row"><span className="k">Resonant · 7d</span><span className="v v--gold">{resonantValues} of {totalValues}</span></div>
          <div className="stat-row"><span className="k">Practices in Orbit</span><span className="v">{practices.length}</span></div>
        </div>
      </div>

      <div className="panel-section">
        <div className="panel-section__title">Unmapped · {unmappedItems.length}</div>
        {unmappedItems.length === 0 ? (
          <p className="unmapped-empty">All practices are anchored to values.</p>
        ) : (
          unmappedItems.map(item => (
            <div key={item.id}
              className={`unmapped-card ${pickedUnmappedId === item.id ? 'unmapped-card--selected' : ''}`}
              onClick={() => editMode && onPickUnmapped(pickedUnmappedId === item.id ? null : item.id)}>
              <div className="unmapped-card__name">{item.name || item.label}</div>
              <div className="unmapped-card__hint">{editMode ? 'Click · then select a value on the map' : 'Enable edit mode to assign'}</div>
            </div>
          ))
        )}
      </div>

      <p className="reading-hint"><em>Drag to pan · scroll to zoom. Click a star or domain to inspect it.</em></p>
    </React.Fragment>
  );
}

function ValueBody({ v, domains, practices, editMode, pickedItem, onAssign, onSelectPractice, onSelectDomain }) {
  const bridge = v.bridge ? domains.find(d => d.id === v.bridge) : null;
  const childPractices = practices.filter(p => p.value === v.id);

  return (
    <React.Fragment>
      {v.blurb && <p className="reading-hint"><em>{v.blurb}</em></p>}

      {bridge && (
        <div className="panel-section">
          <div className="panel-section__title">Secondary Domain</div>
          <button className="bridge-badge bridge-badge--btn"
            onClick={() => onSelectDomain(bridge)}>
            <span className="dot" style={{ background: domainColor(bridge.id) }}></span>
            <span>Bridges to <strong>{bridge.name}</strong></span>
            <span className="arrow">›</span>
          </button>
        </div>
      )}

      <div className="panel-section">
        <div className="panel-section__title">Practices in Orbit · {childPractices.length}</div>
        {childPractices.length === 0 ? (
          <p className="unmapped-empty">No practices anchored to this value yet.</p>
        ) : (
          <div className="practice-list">
            {childPractices.map(p => (
              <button key={p.id} className="practice-list__row practice-list__row--btn"
                onClick={() => onSelectPractice(p)}>
                <span className={`marker ${(p.velocity||0) > 0.6 ? 'marker--filled' : ''}`}></span>
                <div className="name">{p.name}</div>
                <span className="tag">{velocityState(p.velocity||0)}</span>
                <span className="arrow">›</span>
              </button>
            ))}
          </div>
        )}
      </div>

      {editMode && (
        <div className="panel-section">
          <div className="panel-section__title">Assign Unmapped Practice</div>
          <div className="assign-zone">
            {pickedItem ? (
              <React.Fragment>
                <p className="assign-zone__prompt">Anchor <em>{pickedItem.name || pickedItem.label}</em> to <strong>{v.name}</strong>?</p>
                <span className="assign-zone__chip">{pickedItem.name || pickedItem.label}</span>
                <button className="assign-zone__btn" onClick={() => onAssign(pickedItem, v)}>
                  Anchor to {v.name}
                </button>
              </React.Fragment>
            ) : (
              <p className="assign-zone__prompt">Select a practice from Unmapped (in Overview) to anchor it here.</p>
            )}
          </div>
        </div>
      )}
    </React.Fragment>
  );
}

function DomainBody({ d, values, practices, onSelectValue, onSelectPractice }) {
  const domainValues = values.filter(v => v.domain === d.id);
  const domainPractices = practices.filter(p => domainValues.some(v => v.id === p.value));

  const DOMAIN_NOTES = {
    inner:         'The world you build inside — beliefs, emotional landscape, spiritual orientation. Values here shape how you experience everything else.',
    physical:      'How you inhabit a body and a physical world — movement, health, environment, craft. The most tangible layer of identity.',
    creative:      'Expression, making, and the drive to bring new things into existence. Where inner vision meets outer form.',
    interpersonal: 'The relational field — how you connect, love, collaborate, and hold space. Identity shaped through others.',
    adventure:     'Growth through friction and the unknown — challenge, exploration, expanding the edge of comfort.',
    admin:         'The infrastructure of a life — resources, systems, obligations. The container that makes everything else possible.',
  };

  return (
    <React.Fragment>
      <div className="panel-section">
        <p className="reading-hint"><em>{DOMAIN_NOTES[d.id] || 'A sector of the identity architecture.'}</em></p>
      </div>

      {domainValues.length > 0 && (
        <div className="panel-section">
          <div className="panel-section__title">Values · {domainValues.length}</div>
          <div className="practice-list">
            {domainValues.map(v => (
              <button key={v.id} className="practice-list__row practice-list__row--btn"
                onClick={() => onSelectValue(v)}>
                <span className="marker marker--star" style={{ borderColor: domainColor(d.id) }}></span>
                <div className="name">{v.name}</div>
                <span className="tag">{velocityState(v.velocity||0)}</span>
                <span className="arrow">›</span>
              </button>
            ))}
          </div>
        </div>
      )}

      {domainPractices.length > 0 && (
        <div className="panel-section">
          <div className="panel-section__title">Practices · {domainPractices.length}</div>
          <div className="practice-list">
            {domainPractices.map(p => {
              const parentV = values.find(v => v.id === p.value);
              return (
                <button key={p.id} className="practice-list__row practice-list__row--btn"
                  onClick={() => onSelectPractice(p)}>
                  <span className={`marker ${(p.velocity||0) > 0.6 ? 'marker--filled' : ''}`}></span>
                  <div className="name">
                    {p.name}
                    {parentV && <span className="name__sub">{parentV.name}</span>}
                  </div>
                  <span className="tag">{velocityState(p.velocity||0)}</span>
                  <span className="arrow">›</span>
                </button>
              );
            })}
          </div>
        </div>
      )}

      {domainValues.length === 0 && (
        <p className="unmapped-empty">No values anchored to this domain yet.</p>
      )}
    </React.Fragment>
  );
}

function PracticeBody({ p, values, domains, onSelectValue, onSelectDomain }) {
  const parentValue  = values.find(v => v.id === p.value);
  const parentDomain = parentValue ? domains.find(d => d.id === parentValue.domain) : null;
  const vel = p.velocity || 0;
  const state = velocityState(vel);

  return (
    <React.Fragment>
      <div className="panel-section">
        <div className="panel-section__title">Activity · 7 days</div>
        <div className="stats-grid">
          <div className="stat-row">
            <span className="k">Status</span>
            <span className={`v ${vel > 0.7 ? 'v--gold' : ''}`}>{state}</span>
          </div>
          <div className="stat-row">
            <span className="k">Presence</span>
            <span className="v mono">{Math.round(vel * 100)}/100</span>
          </div>
        </div>
      </div>

      {parentValue && (
        <div className="panel-section">
          <div className="panel-section__title">Anchored to Value</div>
          <button className="bridge-badge bridge-badge--btn"
            onClick={() => onSelectValue(parentValue)}>
            <span className="dot" style={{ background: parentDomain ? domainColor(parentDomain.id) : 'var(--ink-3)' }}></span>
            <span>{parentValue.name}</span>
            <span className="arrow">›</span>
          </button>
        </div>
      )}

      {parentDomain && (
        <div className="panel-section">
          <div className="panel-section__title">Domain</div>
          <button className="bridge-badge bridge-badge--btn"
            onClick={() => onSelectDomain(parentDomain)}>
            <span className="dot" style={{ background: domainColor(parentDomain.id) }}></span>
            <span>{parentDomain.name}</span>
            <span className="arrow">›</span>
          </button>
        </div>
      )}
    </React.Fragment>
  );
}

// ── Icons ─────────────────────────────────────────────────────────────────
const ZoomInIcon  = () => <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="9" cy="9" r="6" stroke="currentColor" strokeWidth="1.5"/><line x1="13.2" y1="13.2" x2="17" y2="17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><line x1="9" y1="6.5" x2="9" y2="11.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><line x1="6.5" y1="9" x2="11.5" y2="9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>;
const ZoomOutIcon = () => <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="9" cy="9" r="6" stroke="currentColor" strokeWidth="1.5"/><line x1="13.2" y1="13.2" x2="17" y2="17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/><line x1="6.5" y1="9" x2="11.5" y2="9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>;
const ResetIcon   = () => <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M 4 10 A 6 6 0 1 1 6 14.5" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round"/><polyline points="3,7 4,10 7,9" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>;

// ── Root ──────────────────────────────────────────────────────────────────
function AlignmentMap() {
  const [loading,   setLoading]   = React.useState(true);
  const [error,     setError]     = React.useState(null);
  const [domains,   setDomains]   = React.useState([]);
  const [values,    setValues]    = React.useState([]);
  const [practices, setPractices] = React.useState([]);
  const [unmappedItems, setUnmappedItems] = React.useState([]);

  const [selectedValueId,    setSelectedValueId]    = React.useState(null);
  const [selectedDomainId,   setSelectedDomainId]   = React.useState(null);
  const [selectedPracticeId, setSelectedPracticeId] = React.useState(null);
  const [hoveredValueId,     setHoveredValueId]     = React.useState(null);
  const [zoom, setZoom] = React.useState(1);
  const [pan,  setPan]  = React.useState({ x: 0, y: 0 });
  const [editMode,         setEditMode]         = React.useState(false);
  const [pickedUnmappedId, setPickedUnmappedId] = React.useState(null);
  const [showValues,    setShowValues]    = React.useState(true);
  const [showPractices, setShowPractices] = React.useState(true);

  const fetchData = React.useCallback(() => {
    Promise.all([
      fetch('/api/navigation/map').then(r => r.json()),
      fetch('/api/navigation/landing-bay').then(r => r.json()),
    ]).then(([mapData, bayData]) => {
      const { domains: d, values: v, practices: p } = transformAPIData(mapData);
      setDomains(d); setValues(v); setPractices(p);
      setUnmappedItems(bayData);
      setLoading(false);
    }).catch(err => { setError(err.message); setLoading(false); });
  }, []);

  React.useEffect(() => { fetchData(); }, [fetchData]);

  const selectedValue    = React.useMemo(() => values.find(v => v.id === selectedValueId),       [values, selectedValueId]);
  const selectedDomain   = React.useMemo(() => domains.find(d => d.id === selectedDomainId),     [domains, selectedDomainId]);
  const selectedPractice = React.useMemo(() => practices.find(p => p.id === selectedPracticeId), [practices, selectedPracticeId]);

  const onSelectValue    = (v) => { setSelectedValueId(v.id); setSelectedDomainId(null); setSelectedPracticeId(null); };
  const onSelectDomain   = (d) => { setSelectedDomainId(d.id); setSelectedValueId(null); setSelectedPracticeId(null); };
  const onSelectPractice = (p) => { setSelectedPracticeId(p.id); setSelectedValueId(null); setSelectedDomainId(null); };
  const onClearSelection = () => { setSelectedValueId(null); setSelectedDomainId(null); setSelectedPracticeId(null); };

  const onAssign = (practice, targetValue) => {
    fetch('/api/navigation/assign', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ practice_id: practice.id, value_id: targetValue.id }),
    }).then(() => { fetchData(); setPickedUnmappedId(null); });
  };

  const onZoomIn    = () => setZoom(z => clampZoom(+(z * 1.2).toFixed(2)));
  const onZoomOut   = () => setZoom(z => clampZoom(+(z / 1.2).toFixed(2)));
  const onResetView = () => { setZoom(1); setPan({ x: 0, y: 0 }); };

  const totalValues    = values.length;
  const resonantValues = values.filter(v => (v.velocity || 0) >= 0.7).length;
  if (loading) return <div className="app--loading"><div className="loading-mark">Plotting structural coordinates…</div></div>;
  if (error)   return <div className="app--loading"><div className="loading-mark" style={{ color: 'var(--ink-1)' }}>Navigation offline · {error}</div></div>;

  return (
    <div className="app">
      <NavSidebar />

      <div className="legend-strip">
        <span className="legend-strip__label">Life Domains</span>
        <div className="legend-strip__pills">
          {domains.map(d => (
            <button key={d.id}
              className={`legend-pill ${selectedDomainId === d.id ? 'legend-pill--active' : ''}`}
              onClick={() => selectedDomainId === d.id ? onClearSelection() : onSelectDomain(d)}>
              <span className="legend-pill__dot" style={{ background: domainColor(d.id) }}></span>
              <span className="legend-pill__name">{d.name}</span>
            </button>
          ))}
        </div>
      </div>

      <main className="stage">
        <AlignmentGraph
          domains={domains} values={values} practices={practices}
          selectedValueId={selectedValueId} selectedDomainId={selectedDomainId}
          selectedPracticeId={selectedPracticeId} hoveredValueId={hoveredValueId}
          zoom={zoom} pan={pan} onZoomChange={setZoom} onPanChange={setPan}
          showValues={showValues} showPractices={showPractices}
          onSelectValue={onSelectValue} onSelectDomain={onSelectDomain}
          onHoverValue={setHoveredValueId} onClearSelection={onClearSelection}
          onSelectPractice={onSelectPractice}
        />
        <div className="canvas-filters">
          <button className={`filter-chip ${showValues ? 'filter-chip--on' : ''}`}
            aria-pressed={showValues} onClick={() => setShowValues(v => !v)}>Values</button>
          <button className={`filter-chip ${showPractices ? 'filter-chip--on' : ''}`}
            aria-pressed={showPractices} onClick={() => setShowPractices(v => !v)}>Practices</button>
        </div>
        <div className="controls">
          <button className="controls__btn controls__btn--accent" onClick={onZoomIn} title="Zoom in" aria-label="Zoom in"><ZoomInIcon /></button>
          <div className="controls__zoom-readout" aria-label={`Zoom level ${(zoom * 100).toFixed(0)}%`}>{(zoom * 100).toFixed(0)}%</div>
          <button className="controls__btn" onClick={onZoomOut} title="Zoom out" aria-label="Zoom out"><ZoomOutIcon /></button>
          <button className="controls__btn" onClick={onResetView} title="Reset view" aria-label="Reset view"><ResetIcon /></button>
        </div>
      </main>

      <RightPanel
        selectedValue={selectedValue} selectedDomain={selectedDomain} selectedPractice={selectedPractice}
        domains={domains} values={values} practices={practices} unmappedItems={unmappedItems}
        editMode={editMode} onToggleEdit={() => { setEditMode(e => !e); setPickedUnmappedId(null); }}
        onClose={onClearSelection}
        pickedUnmappedId={pickedUnmappedId} onPickUnmapped={setPickedUnmappedId}
        onAssign={onAssign}
        totalValues={totalValues} resonantValues={resonantValues}
        onSelectValue={onSelectValue} onSelectDomain={onSelectDomain} onSelectPractice={onSelectPractice}
      />
    </div>
  );
}


window.AlignmentMap = AlignmentMap;
