import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

// --------------------------------------------------
// Scene
// --------------------------------------------------

const canvasWrap = document.getElementById("canvas-wrap");
const canvas = document.getElementById("canvas");
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);

const camera = new THREE.PerspectiveCamera(
  60, canvasWrap.clientWidth / canvasWrap.clientHeight, 0.1, 10000
);
camera.position.set(4, 0, 56);

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, preserveDrawingBuffer: true });
renderer.setSize(canvasWrap.clientWidth, canvasWrap.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x06061a, 1);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.autoRotate    = false;
controls.enableRotate  = false;
controls.target.set(4, 0, 0);
controls.minDistance = 5;
controls.maxDistance = 280;
controls.zoomSpeed   = 0.15;

// --------------------------------------------------
// RDKit — lazy-loaded on first hover/click
// --------------------------------------------------

let rdkit = null;
let _rdkitLoading = false;

function ensureRDKit() {
  if (rdkit || _rdkitLoading) return;
  _rdkitLoading = true;
  const s = document.createElement("script");
  s.src = "https://cdn.jsdelivr.net/npm/@rdkit/rdkit/dist/RDKit_minimal.js";
  s.onload = () => window.initRDKitModule().then(rdk => { rdkit = rdk; });
  document.head.appendChild(s);
}

// --------------------------------------------------
// State
// --------------------------------------------------

let nodeData        = [];
let smilesToNode    = new Map();  // smiles → node object (for network graph clicks)
let pointsMesh      = null;
let propRanges      = {};
let colourMode      = "depth";
let baseColors      = null;
let adj             = new Map(); // directed assembly adjacency: lower-depth → higher-depth
let adjR            = new Map(); // reverse: higher-depth → lower-depth (parents)
let simAdj          = new Map(); // idx → [{idx, score}]  (Tanimoto k-NN)
let networkMode     = "none"; // "none" | "rxn" | "similarity" | "jas"
let pathwayLines    = null;  // BFS/similarity highlight lines (pinned)
let hoverLines      = null;  // direct-neighbour lines (hover only)
let selectedIndices = [];    // indices highlighted by current lasso drag
let subsetIdxSet    = null;  // Set<number> when in subset mode, null otherwise
let _savedAdj = null, _savedAdjR = null, _savedSimAdj = null;
let pinnedNode      = null;
let lastHoverIdx    = -1;

const raycaster = new THREE.Raycaster();
raycaster.params.Points.threshold = 0.5;
const mouse = new THREE.Vector2();

// --------------------------------------------------
// Selection beacons — Points sprites that match the point-cloud visual style
// --------------------------------------------------

function makeHoverBeaconTex(size = 256) {
  const c = document.createElement("canvas");
  c.width = c.height = size;
  const ctx = c.getContext("2d");
  const r = size / 2;
  const g = ctx.createRadialGradient(r, r, 0, r, r, r);
  g.addColorStop(0,    "rgba(255,255,255,1)");
  g.addColorStop(0.18, "rgba(255,255,255,0.85)");
  g.addColorStop(0.45, "rgba(255,255,255,0.35)");
  g.addColorStop(0.75, "rgba(255,255,255,0.06)");
  g.addColorStop(1,    "rgba(255,255,255,0)");
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, size, size);
  return new THREE.CanvasTexture(c);
}

function makePinBeaconTex(size = 256) {
  const c = document.createElement("canvas");
  c.width = c.height = size;
  const ctx = c.getContext("2d");
  const r = size / 2;
  // soft outer aura
  const aura = ctx.createRadialGradient(r, r, r * 0.28, r, r, r);
  aura.addColorStop(0,    "rgba(255,255,255,0.03)");
  aura.addColorStop(0.55, "rgba(255,255,255,0.10)");
  aura.addColorStop(0.72, "rgba(255,255,255,0.28)");
  aura.addColorStop(0.82, "rgba(255,255,255,0.10)");
  aura.addColorStop(1,    "rgba(255,255,255,0)");
  ctx.fillStyle = aura;
  ctx.fillRect(0, 0, size, size);
  // crisp white ring
  ctx.strokeStyle = "rgba(255,255,255,0.96)";
  ctx.lineWidth   = Math.max(3, size * 0.042);
  ctx.beginPath();
  ctx.arc(r, r, r * 0.63, 0, Math.PI * 2);
  ctx.stroke();
  return new THREE.CanvasTexture(c);
}

function _makeBeacon(tex, size, order) {
  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(3), 3));
  const m = new THREE.Points(geo, new THREE.PointsMaterial({
    size, map: tex, transparent: true, depthTest: false,
    sizeAttenuation: true, alphaTest: 0.008,
  }));
  m.renderOrder = order;
  m.visible = false;
  scene.add(m);
  return m;
}

const hoverBeacon = _makeBeacon(makeHoverBeaconTex(), 2.6, 4);
const pinBeacon   = _makeBeacon(makePinBeaconTex(),   5.2, 5);
const PIN_BASE_SIZE = 5.2;

// --------------------------------------------------
// Colour helpers
// --------------------------------------------------

// Plasma colormap — deep blue-purple → magenta → orange → bright yellow.
// Matches the vivid scientific palette of tools like ESM Atlas.
const PLASMA_STOPS = [
  [0.050, 0.030, 0.528],   // 0.00  deep indigo
  [0.298, 0.008, 0.653],   // 0.14  purple
  [0.494, 0.012, 0.657],   // 0.28  vivid purple
  [0.671, 0.050, 0.573],   // 0.43  magenta-purple
  [0.820, 0.129, 0.438],   // 0.57  hot pink
  [0.935, 0.275, 0.253],   // 0.71  salmon-red
  [0.980, 0.468, 0.091],   // 0.86  orange
  [0.940, 0.975, 0.131],   // 1.00  bright yellow
];

function plasmaAt(t) {
  const n = PLASMA_STOPS.length - 1;
  const i = Math.min(n - 1, Math.floor(t * n));
  const f = t * n - i;
  return PLASMA_STOPS[i].map((v, j) => v + f * (PLASMA_STOPS[i + 1][j] - v));
}

function valueToRGB(value, min, max) {
  if (value == null) return [0.3, 0.3, 0.45];
  const t = Math.max(0, Math.min(1, (value - min) / (max - min + 1e-9)));
  return plasmaAt(t);
}

function computePropRanges(nodes) {
  const fields = { depth: n => n.depth, mw: n => n.attributes?.mw,
                   logp:  n => n.attributes?.logp, tpsa: n => n.attributes?.tpsa };
  const ranges = {};
  for (const [key, getter] of Object.entries(fields)) {
    const vals = nodes.map(getter).filter(v => v != null);
    if (vals.length) ranges[key] = { min: Math.min(...vals), max: Math.max(...vals) };
  }
  return ranges;
}

function recolour(mode) {
  if (!pointsMesh || !propRanges[mode]) return;
  const arr    = pointsMesh.geometry.attributes.color.array;
  const range  = propRanges[mode];
  const getter = mode === "depth" ? n => n.depth : n => n.attributes?.[mode];

  nodeData.forEach((n, i) => {
    const [r, g, b] = valueToRGB(getter(n), range.min, range.max);
    arr[i * 3] = r; arr[i * 3 + 1] = g; arr[i * 3 + 2] = b;
  });
  pointsMesh.geometry.attributes.color.needsUpdate = true;

  // Keep baseColors in sync so pathway dimming can restore correctly
  baseColors.set(arr);

  colourMode = mode;
  updateToolbarActive(mode);
  updateLegend(mode, range);
  drawHistogram(mode);
}

// --------------------------------------------------
// Pathway highlight
// --------------------------------------------------

function bfsPathway(startIdx, maxNodes = 60) {
  const visited = new Set([startIdx]);
  let frontier  = [startIdx];
  while (frontier.length > 0 && visited.size < maxNodes) {
    const next = [];
    for (const idx of frontier) {
      // traverse both forward (descendants) and backward (ancestors)
      const fwd = adj.get(idx);
      const rev = adjR.get(idx);
      const nbrs = fwd && rev ? [...fwd, ...rev]
                 : fwd ? [...fwd] : rev ? [...rev] : [];
      for (const nb of nbrs) {
        if (!visited.has(nb)) {
          visited.add(nb);
          next.push(nb);
          if (visited.size >= maxNodes) break;
        }
      }
      if (visited.size >= maxNodes) break;
    }
    frontier = next;
  }
  return visited;
}

function applyPathwayHighlight(pathwaySet) {
  if (!pointsMesh) return;
  const arr       = pointsMesh.geometry.attributes.color.array;
  const pinnedIdx = pinnedNode?._idx ?? -1;
  for (let i = 0; i < nodeData.length; i++) {
    if (subsetIdxSet && !subsetIdxSet.has(i)) {
      arr[i*3] = baseColors[i*3]*0.35; arr[i*3+1] = baseColors[i*3+1]*0.35; arr[i*3+2] = baseColors[i*3+2]*0.35;
      continue;
    }
    if (i === pinnedIdx) {
      arr[i * 3] = 1; arr[i * 3 + 1] = 1; arr[i * 3 + 2] = 1;
    } else if (pathwaySet.has(i)) {
      const r = baseColors[i * 3], g = baseColors[i * 3 + 1], b = baseColors[i * 3 + 2];
      arr[i * 3]     = Math.min(1, r * 9.0 + 0.825);
      arr[i * 3 + 1] = Math.min(1, g * 9.0 + 0.825);
      arr[i * 3 + 2] = Math.min(1, b * 9.0 + 0.825);
    } else {
      arr[i * 3] = arr[i * 3 + 1] = arr[i * 3 + 2] = 0.04;
    }
  }
  pointsMesh.geometry.attributes.color.needsUpdate = true;

  if (pathwayLines) { scene.remove(pathwayLines); pathwayLines.geometry.dispose(); pathwayLines = null; }
  const positions = [], edgeColors = [];
  for (const si of pathwaySet) {
    const nbrs = adj.get(si); // directed: si → ti only
    if (!nbrs) continue;
    for (const ti of nbrs) {
      if (!pathwaySet.has(ti)) continue;
      const sn = nodeData[si], tn = nodeData[ti];
      positions.push(sn.x, sn.y, sn.z, tn.x, tn.y, tn.z);
      // source: blue (building block), target: teal (assembled)
      edgeColors.push(0.375, 0.675, 1.0,  0.0, 1.0, 1.0);
    }
  }
  if (positions.length > 0) {
    const geo = new THREE.BufferGeometry();
    geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
    geo.setAttribute("color",    new THREE.BufferAttribute(new Float32Array(edgeColors), 3));
    pathwayLines = new THREE.LineSegments(geo,
      new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.75, depthTest: false }));
    pathwayLines.renderOrder = 2;
    scene.add(pathwayLines);
  }
}

function clearPathwayHighlight() {
  if (!pointsMesh) return;
  const arr = pointsMesh.geometry.attributes.color.array;
  if (subsetIdxSet) {
    for (let i = 0; i < nodeData.length; i++) {
      if (subsetIdxSet.has(i)) {
        arr[i*3] = baseColors[i*3]; arr[i*3+1] = baseColors[i*3+1]; arr[i*3+2] = baseColors[i*3+2];
      } else {
        arr[i*3] = baseColors[i*3]*0.35; arr[i*3+1] = baseColors[i*3+1]*0.35; arr[i*3+2] = baseColors[i*3+2]*0.35;
      }
    }
  } else {
    arr.set(baseColors);
  }
  pointsMesh.geometry.attributes.color.needsUpdate = true;
  if (pathwayLines) { scene.remove(pathwayLines); pathwayLines.geometry.dispose(); pathwayLines = null; }
}

// Draws the full assembly subgraph (shared building blocks) for the hovered molecule.
// Uses BFS to find all related molecules, then draws every edge within that set —
// same web as the click view but without dimming the cloud.
function showHoverPathwayLines(idx) {
  if (hoverLines) { scene.remove(hoverLines); hoverLines.geometry.dispose(); hoverLines = null; }
  const pathwaySet = bfsPathway(idx);
  const positions = [], edgeColors = [];
  for (const si of pathwaySet) {
    const nbrs = adj.get(si); // directed forward edges only
    if (!nbrs) continue;
    for (const ti of nbrs) {
      if (!pathwaySet.has(ti)) continue;
      const sn = nodeData[si], tn = nodeData[ti];
      positions.push(sn.x, sn.y, sn.z, tn.x, tn.y, tn.z);
      // source: soft blue, target: bright white — direction visible at a glance
      edgeColors.push(0.675, 0.825, 1.0,  1.0, 1.0, 1.0);
    }
  }
  if (positions.length === 0) return;
  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
  geo.setAttribute("color",    new THREE.BufferAttribute(new Float32Array(edgeColors), 3));
  hoverLines = new THREE.LineSegments(geo,
    new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.9, depthTest: false }));
  hoverLines.renderOrder = 2;
  scene.add(hoverLines);
}

// Similarity highlight: center = white, neighbours coloured by Tanimoto score
function applySimilarityHighlight(centerIdx) {
  if (!pointsMesh) return;
  const neighbours = simAdj.get(centerIdx) || [];
  const scoreMap   = new Map(neighbours.map(({ idx, score }) => [idx, score]));

  const arr       = pointsMesh.geometry.attributes.color.array;
  const pinnedIdx = pinnedNode?._idx ?? -1;
  const isPinned  = pinnedIdx >= 0;

  for (let i = 0; i < nodeData.length; i++) {
    if (subsetIdxSet && !subsetIdxSet.has(i)) {
      arr[i*3] = baseColors[i*3]*0.35; arr[i*3+1] = baseColors[i*3+1]*0.35; arr[i*3+2] = baseColors[i*3+2]*0.35;
      continue;
    }
    if (i === centerIdx || i === pinnedIdx) {
      arr[i*3] = 1; arr[i*3+1] = 1; arr[i*3+2] = 1;
    } else if (scoreMap.has(i)) {
      const r = baseColors[i * 3], g = baseColors[i * 3 + 1], b = baseColors[i * 3 + 2];
      arr[i*3]   = Math.min(1, r * 9.0 + 0.825);
      arr[i*3+1] = Math.min(1, g * 9.0 + 0.825);
      arr[i*3+2] = Math.min(1, b * 9.0 + 0.825);
    } else {
      arr[i*3] = arr[i*3+1] = arr[i*3+2] = isPinned ? 0.04 : baseColors[i*3];
    }
  }
  pointsMesh.geometry.attributes.color.needsUpdate = true;

  if (pathwayLines) { scene.remove(pathwayLines); pathwayLines.geometry.dispose(); pathwayLines = null; }
  const positions = [], colors = [];
  for (const { idx: ti, score } of neighbours) {
    const sn = nodeData[centerIdx], tn = nodeData[ti];
    const t  = (score - 0.35) / 0.65;
    positions.push(sn.x, sn.y, sn.z, tn.x, tn.y, tn.z);
    const c = Math.min(1, 0.15 + (0.6 + t * 0.3) * 1.5);
    colors.push(0.15, c, 1.0,  0.15, c, 1.0);
  }
  if (positions.length) {
    const geo = new THREE.BufferGeometry();
    geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
    geo.setAttribute("color",    new THREE.BufferAttribute(new Float32Array(colors),    3));
    pathwayLines = new THREE.LineSegments(geo,
      new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.525, depthTest: false }));
    pathwayLines.renderOrder = 2;
    scene.add(pathwayLines);
  }
}

// --------------------------------------------------
// Canvas toolbar — colour-by buttons
// --------------------------------------------------

const COLOUR_MODES = [
  { key: "depth", label: "Depth" },
  { key: "mw",    label: "MW"    },
  { key: "logp",  label: "LogP"  },
  { key: "tpsa",  label: "TPSA"  },
];

const colourControls = document.getElementById("colour-controls");
const modeButtons    = {};

COLOUR_MODES.forEach(({ key, label }) => {
  const btn = document.createElement("button");
  btn.textContent  = label;
  btn.style.cssText = `
    background:transparent; border:1px solid #252538; color:#7878a0;
    padding:4px 12px; border-radius:4px; cursor:pointer;
    font-family:inherit; font-size:11px; font-weight:500;
    transition:color .12s, border-color .12s, background .12s;
  `;
  btn.addEventListener("click", () => { recolour(key); colourPopup.classList.remove("open"); tbColour.classList.remove("active"); });
  colourControls.appendChild(btn);
  modeButtons[key] = btn;
});

function updateToolbarActive(mode) {
  Object.entries(modeButtons).forEach(([k, btn]) => {
    btn.style.background  = k === mode ? "rgba(68,136,255,.15)" : "transparent";
    btn.style.borderColor = k === mode ? "#4488ff"              : "#252538";
    btn.style.color       = k === mode ? "#4488ff"              : "#7878a0";
  });
}

// Colour popup toggle
const tbColour    = document.getElementById("tb-colour");
const colourPopup = document.getElementById("colour-popup");
colourPopup.addEventListener("click", (e) => e.stopPropagation());
tbColour.addEventListener("click", (e) => {
  e.stopPropagation();
  colourPopup.classList.toggle("open");
  tbColour.classList.toggle("active", colourPopup.classList.contains("open"));
  searchOverlay.classList.remove("open");
});

// Search overlay
const tbSearch     = document.getElementById("tb-search");
const searchOverlay = document.getElementById("search-overlay");
const searchInput  = document.getElementById("search-input");
document.getElementById("search-close").addEventListener("click", () => searchOverlay.classList.remove("open"));
tbSearch.addEventListener("click", (e) => {
  e.stopPropagation();
  searchOverlay.classList.toggle("open");
  colourPopup.classList.remove("open");
  tbColour.classList.remove("active");
  if (searchOverlay.classList.contains("open")) searchInput.focus();
});
searchInput.addEventListener("keydown", (e) => {
  if (e.key !== "Enter") return;
  const smiles = searchInput.value.trim();
  const node   = smilesToNode.get(smiles);
  if (node) {
    pinNode(node);
    // fly camera to node
    cameraFlyTo = new THREE.Vector3(node.x, node.y, node.z);
    searchOverlay.classList.remove("open");
    searchInput.value = "";
  } else {
    searchInput.style.color = "#ff6666";
    setTimeout(() => { searchInput.style.color = ""; }, 800);
  }
});

// --------------------------------------------------
// Filter panel
// --------------------------------------------------

const filterPanel  = document.getElementById("filter-panel");
const tbFilter     = document.getElementById("tb-filter");
let   filterActive = false;

// Each dimension: { loSlider, hiSlider, loVal, hiVal, getter, dataMin, dataMax }
const FILTERS = [
  { id: "mw", loS: "fp-mw-lo", loV: "fv-mw-lo", hiV: "fv-mw-hi", getter: n => n.attributes?.mw       ?? 0 },
  { id: "bn", loS: "fp-bn-lo", loV: "fv-bn-lo", hiV: "fv-bn-hi", getter: n => n.attributes?.bond_count ?? 0 },
  { id: "rn", loS: "fp-rn-lo", loV: "fv-rn-lo", hiV: "fv-rn-hi", getter: n => n.attributes?.ring_count ?? 0 },
  { id: "ai", loS: "fp-ai-lo", loV: "fv-ai-lo", hiV: "fv-ai-hi", getter: n => n.depth                  ?? 0 },
];
const fpState = {};

function initFilterPanel() {
  FILTERS.forEach(f => {
    const vals     = nodeData.map(f.getter).filter(v => v != null);
    const dMin     = Math.floor(Math.min(...vals));
    const dMax     = Math.ceil(Math.max(...vals));
    fpState[f.id]  = { lo: dMin, hi: dMax, dMin, dMax };

    const loSlider = document.getElementById(f.loS);
    const loInput  = document.getElementById(f.loV);
    const hiInput  = document.getElementById(f.hiV);

    loSlider.min = dMin; loSlider.max = dMax; loSlider.value = dMin;
    loInput.min  = dMin; loInput.max  = dMax; loInput.value  = dMin;
    hiInput.min  = dMin; hiInput.max  = dMax; hiInput.value  = dMax;

    loSlider.addEventListener("input", () => {
      fpState[f.id].lo = parseFloat(loSlider.value);
      loInput.value = loSlider.value;
    });
    loInput.addEventListener("change", () => {
      fpState[f.id].lo = parseFloat(loInput.value);
      loSlider.value   = loInput.value;
    });
    hiInput.addEventListener("change", () => {
      fpState[f.id].hi = parseFloat(hiInput.value);
    });
  });

  document.getElementById("fp-apply").addEventListener("click", applyFilter);
  document.getElementById("fp-reset").addEventListener("click", resetFilter);
}

function applyFilter() {
  filterPanel.classList.remove("open");
  tbFilter.classList.remove("active");

  const arr     = pointsMesh.geometry.attributes.color.array;
  let   anyHide = false;

  nodeData.forEach((n, i) => {
    const pass = FILTERS.every(f => {
      const v = f.getter(n) ?? 0;
      return v >= fpState[f.id].lo && v <= fpState[f.id].hi;
    });
    if (!pass) {
      arr[i*3] = arr[i*3+1] = arr[i*3+2] = 0.06; // near-black on dark bg
      anyHide = true;
    } else {
      arr[i*3]   = baseColors[i*3];
      arr[i*3+1] = baseColors[i*3+1];
      arr[i*3+2] = baseColors[i*3+2];
    }
  });

  pointsMesh.geometry.attributes.color.needsUpdate = true;
  filterActive = anyHide;
  tbFilter.classList.toggle("filtered", filterActive);

  // count visible
  const visible = nodeData.filter((n) => FILTERS.every(f => {
    const v = f.getter(n) ?? 0;
    return v >= fpState[f.id].lo && v <= fpState[f.id].hi;
  })).length;
  showStatus(`${visible.toLocaleString()} molecules match filter`, false);
  setTimeout(hideStatus, 2000);
}

function resetFilter() {
  FILTERS.forEach(f => {
    fpState[f.id].lo = fpState[f.id].dMin;
    fpState[f.id].hi = fpState[f.id].dMax;
    document.getElementById(f.loS).value = fpState[f.id].dMin;
    document.getElementById(f.loV).value = fpState[f.id].dMin;
    document.getElementById(f.hiV).value = fpState[f.id].dMax;
  });
  filterActive = false;
  tbFilter.classList.remove("filtered");
  const arr = pointsMesh.geometry.attributes.color.array;
  arr.set(baseColors);
  pointsMesh.geometry.attributes.color.needsUpdate = true;
  filterPanel.classList.remove("open");
  tbFilter.classList.remove("active");
}

filterPanel.addEventListener("click", (e) => e.stopPropagation());

tbFilter.addEventListener("click", (e) => {
  e.stopPropagation();
  filterPanel.classList.toggle("open");
  colourPopup.classList.remove("open");
  tbColour.classList.remove("active");
  searchOverlay.classList.remove("open");
  tbFilter.classList.toggle("active", filterPanel.classList.contains("open"));
});

// Mobile sidebar: move element into #left-col so it sits inline below the canvas
(function syncSidebarPosition() {
  const mq         = window.matchMedia("(max-width: 600px)");
  const leftCol    = document.getElementById("left-col");
  const mainRow    = document.getElementById("main-row");
  const sidebarCol = document.getElementById("sidebar-col");
  const pathway    = document.getElementById("pathway");
  if (!leftCol || !mainRow || !sidebarCol) return;

  function reposition(e) {
    if (e.matches) {
      if (sidebarCol.parentElement !== leftCol)
        leftCol.insertBefore(sidebarCol, pathway || null);
    } else {
      if (sidebarCol.parentElement !== mainRow)
        mainRow.appendChild(sidebarCol);
    }
  }
  mq.addEventListener("change", reposition);
  reposition(mq);
})();

// Hamburger menu toggle
const navHamburger = document.getElementById("nav-hamburger");
const navMenu      = document.getElementById("nav-menu");
if (navHamburger && navMenu) {
  navHamburger.addEventListener("click", (e) => {
    e.stopPropagation();
    const open = navMenu.classList.toggle("open");
    navHamburger.setAttribute("aria-expanded", open);
  });
  navMenu.querySelectorAll(".nav-btn").forEach(btn =>
    btn.addEventListener("click", () => {
      navMenu.classList.remove("open");
      navHamburger.setAttribute("aria-expanded", "false");
    })
  );
}

// Close popups on outside click
document.addEventListener("click", () => {
  colourPopup.classList.remove("open");
  tbColour.classList.remove("active");
  filterPanel.classList.remove("open");
  tbFilter.classList.remove("active");
  navMenu?.classList.remove("open");
  navHamburger?.setAttribute("aria-expanded", "false");
  const so = document.getElementById("smarts-overlay");
  if (so) { so.classList.remove("open"); document.getElementById("tb-smarts")?.classList.remove("active"); }
});

// Zoom slider — controls camera distance from target
document.getElementById("tb-size").addEventListener("input", (e) => {
  const z = parseFloat(e.target.value); // 0.5 → 5
  camera.zoom = z;
  camera.updateProjectionMatrix();
});

// Camera fly-to target (set by search)
let cameraFlyTo = null;

// --------------------------------------------------
// Properties sidebar (slides open on molecule click)
// --------------------------------------------------

const sidebarCol     = document.getElementById("sidebar-col");
const sidebarContent = document.getElementById("sidebar-content");
document.getElementById("sidebar-close").addEventListener("click", () => {
  sidebarCol.classList.remove("open");
  // leave pinned node, beacon, and pathway highlight intact
});

function getInChI(smiles) {
  if (!rdkit || !smiles) return null;
  try {
    const mol = rdkit.get_mol(smiles);
    if (!mol.is_valid()) { mol.delete(); return null; }
    const inchi = typeof mol.get_inchi === 'function' ? mol.get_inchi() : null;
    mol.delete();
    return inchi;
  } catch { return null; }
}

function getInChIKey(smiles) {
  if (!rdkit || !smiles) return null;
  try {
    const inchi = getInChI(smiles);
    if (!inchi) return null;
    return typeof rdkit.get_inchikey_for_inchi === 'function'
      ? rdkit.get_inchikey_for_inchi(inchi)
      : null;
  } catch { return null; }
}

function showSidebar(node) {
  const svg      = getMoleculeSVGFluid(node.id, 220, 165);
  const desc     = getDescriptors(node.id);
  const inchi    = getInChI(node.id);
  const inchIKey = getInChIKey(node.id);

  const fmt = (val) => {
    if (typeof val !== "number") return val;
    return Number.isInteger(val) ? val : val.toFixed(2);
  };

  const idHtml = [
    ["SMILES",    node.id],
    ["InChI",     inchi],
    ["InChI Key", inchIKey],
  ].map(([label, val]) => {
    const display = val != null
      ? `<code class="sb-id-val">${_esc(val)}</code>`
      : `<code class="sb-id-val sb-na">N/A</code>`;
    return `<div class="sb-id-row">
      <span class="sb-id-label">${label}</span>${display}
    </div>`;
  }).join('');

  const PROPS = [
    ["MW",             desc?.amw               ?? node.attributes?.mw,        "g/mol"],
    ["Atoms",          desc?.numHeavyAtoms     ?? null,                        ""],
    ["Bonds",          node.attributes?.bond_count ?? null,                    ""],
    ["Rings",          node.attributes?.ring_count ?? null,                    ""],
    ["LogP",           desc?.alogp             ?? node.attributes?.logp,       ""],
    ["TPSA",           desc?.tpsa              ?? node.attributes?.tpsa,       "Å²"],
    ["HBA",            desc?.lipinskiHBA       ?? node.attributes?.hba,        ""],
    ["HBD",            desc?.lipinskiHBD       ?? node.attributes?.hbd,        ""],
    ["Rot Bonds",      desc?.numRotatableBonds ?? node.attributes?.rotbonds,   ""],
    ["Assembly Depth", node.depth ?? null,                                     ""],
  ];

  const propCards = PROPS
    .filter(([, v]) => v != null)
    .map(([label, val, unit]) =>
      `<div class="sb-prop-card">
        <div class="sb-prop-label">${label}</div>
        <div class="sb-prop-val">${fmt(val)}${unit ? `<span class="sb-prop-unit">${unit}</span>` : ""}</div>
      </div>`
    ).join("");

  sidebarContent.innerHTML = `
    ${svg ? `<div class="sb-svg-wrap">${svg}</div>` : ""}
    <div class="sb-section">
      <div class="sb-section-title">Identifiers</div>
      ${idHtml}
    </div>
    ${propCards ? `<div class="sb-section">
      <div class="sb-section-title">Properties</div>
      <div class="sb-props-grid">${propCards}</div>
    </div>` : ""}
  `;
  sidebarCol.classList.add("open");
}

function _setBeaconPos(beacon, x, y, z) {
  const a = beacon.geometry.attributes.position.array;
  a[0] = x; a[1] = y; a[2] = z;
  beacon.geometry.attributes.position.needsUpdate = true;
}

function pinNode(node) {
  lastHoverIdx = -1;  // force hover-line redraw on next animate frame
  pinnedNode = node;
  _setBeaconPos(pinBeacon, node.x, node.y, node.z);
  pinBeacon.visible = true;
  showSidebar(node);
  if (networkMode === "similarity") {
    applySimilarityHighlight(node._idx);
    renderSimilarityPanel(node);
  } else {
    // Always show reaction network on click
    applyPathwayHighlight(bfsPathway(node._idx));
    renderNetworkPanel(node);
  }
}

function unpinNode() {
  pinnedNode = null;
  pinBeacon.visible = false;
  sidebarCol.classList.remove("open");
  clearPathwayHighlight();
  hideNetPanel();
}

// --------------------------------------------------
// Hover tooltip
// --------------------------------------------------

const tooltip = document.createElement("div");
tooltip.id = "hover-tooltip";
tooltip.style.cssText = `
  position: fixed; background: var(--tooltip-bg); color: var(--text);
  padding: 8px 10px; border-radius: 6px; border: 1px solid var(--border);
  pointer-events: none; max-width: 200px; font-family: monospace;
  font-size: 11px; display: none; z-index: 40;
  box-shadow: 0 4px 16px rgba(0,0,0,.3);
`;
document.body.appendChild(tooltip);

// --------------------------------------------------
// Theme toggle (dark ↔ light)
// --------------------------------------------------

function _netBg() {
  return "#06061a"; // canvases always dark regardless of theme
}

document.getElementById("theme-toggle").addEventListener("click", () => {
  const isLight = document.documentElement.classList.toggle("light");
  document.getElementById("icon-moon").style.display = isLight ? "none"  : "";
  document.getElementById("icon-sun").style.display  = isLight ? ""      : "none";
});

// --------------------------------------------------
// Network graph — bottom panel
// --------------------------------------------------

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

const pathwaySection = document.getElementById("pathway");
const netCanvas = document.getElementById("net-panel");

document.getElementById("pathway-close").addEventListener("click", hideNetPanel);

document.getElementById("net-download").addEventListener("click", () => {
  const svgEl = netCanvas.querySelector("svg");
  if (!svgEl) return;
  const title = document.getElementById("section-panel-title").textContent.trim()
    .replace(/[·\s]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toLowerCase();
  const svgStr = new XMLSerializer().serializeToString(svgEl);
  const blob = new Blob(
    [`<?xml version="1.0" encoding="utf-8"?>\n` + svgStr],
    { type: "image/svg+xml" }
  );
  const url = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement("a"), {
    href: url,
    download: `${title || "reaction_network"}.svg`,
  });
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
});

document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") {
    colourPopup.classList.remove("open");
    tbColour.classList.remove("active");
    searchOverlay.classList.remove("open");
    filterPanel.classList.remove("open");
    tbFilter.classList.remove("active");
    if (subsetIdxSet) exitSubsetMode();
  }
});

// Nav buttons — declare both up front to avoid TDZ cross-references
const navRxnBtn = document.getElementById("nav-rxn");
const navSimBtn = document.getElementById("nav-sim");

// RXN Network button — show reaction network for pinned node and scroll to it
navRxnBtn.addEventListener("click", () => {
  navRxnBtn.classList.add("active");
  navSimBtn.classList.remove("active");
  if (networkMode === "similarity") {
    networkMode = "none";
    clearPathwayHighlight();
  }
  if (pinnedNode) {
    applyPathwayHighlight(bfsPathway(pinnedNode._idx));
    renderNetworkPanel(pinnedNode);
  }
  // Remove active state after brief highlight
  setTimeout(() => navRxnBtn.classList.remove("active"), 600);
});

// Retrosynthesis — placeholder
document.getElementById("nav-retro").addEventListener("click", () => {
  showStatus("Retrosynthesis — coming soon", false);
  setTimeout(hideStatus, 2000);
});

// Similarity Network toggle
navSimBtn.addEventListener("click", async () => {
  if (networkMode === "similarity") {
    networkMode = "none";
    navSimBtn.classList.remove("active");
    clearPathwayHighlight();
    if (pinnedNode) {
      applyPathwayHighlight(bfsPathway(pinnedNode._idx));
      renderNetworkPanel(pinnedNode);
    } else {
      hideNetPanel();
    }
    return;
  }

  // Lazy-load similarity data on first use
  if (simAdj.size === 0) {
    navSimBtn.classList.add("active");
    showStatus("Loading similarity data…");
    try {
      const res = await fetch("/data/processed/similarity_edges.json");
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const simEdges = await res.json();
      for (const [si, ti, score] of simEdges) {
        if (!simAdj.has(si)) simAdj.set(si, []);
        simAdj.get(si).push({ idx: ti, score });
      }
      console.log(`Loaded ${simEdges.length} similarity edges`);
      hideStatus();
    } catch (err) {
      showStatus("Similarity data not available", true);
      navSimBtn.classList.remove("active");
      setTimeout(hideStatus, 2000);
      return;
    }
  }

  networkMode = "similarity";
  navSimBtn.classList.add("active");
  navRxnBtn.classList.remove("active");
  clearPathwayHighlight();
  if (pinnedNode) {
    applySimilarityHighlight(pinnedNode._idx);
    renderSimilarityPanel(pinnedNode);
  } else if (lastHoverIdx !== -1) {
    applySimilarityHighlight(lastHoverIdx);
    hideNetPanel();
  } else {
    hideNetPanel();
  }
});

function showNetPanel(smiles, title = "Reaction Network") {
  pathwaySection.classList.add("open");
  document.getElementById("section-panel-title").textContent = title;
}

function hideNetPanel() {
  pathwaySection.classList.remove("open");
  document.getElementById("section-panel-title").textContent = "Reaction Network";
  netCanvas.innerHTML =
    `<div id="pathway-placeholder-msg">Click a molecule to explore its reaction network</div>`;
}

// Return RDKit molecule SVG as bare inner content (no <svg> wrapper, no white bg)
// so it can be inlined directly into the parent network SVG via a <g> transform.
// Using <image data:image/svg+xml> inside innerHTML-injected SVG is unreliable in Chrome.
function getMolSVGContent(smiles, w, h) {
  const raw = getMoleculeSVG(smiles, w, h);
  if (!raw) return null;
  return raw
    .replace(/<\?xml\b[^?]*\?>/gi, '')           // strip XML declaration
    .replace(/<svg\b[^>]*>/i,       '')           // strip opening <svg>
    .replace(/<\/svg\s*>/i,         '')           // strip closing </svg>
    .replace(/fill\s*:\s*#FFFFFF\s*;?/g, 'fill:none;')   // transparent background
    .replace(/stroke\s*:\s*#000000\b/g,  'stroke:#c8c8c8') // light grey C-bonds
    .replace(/fill\s*:\s*#000000\b/g,    'fill:#c8c8c8');  // light grey C-atoms
}

let _netUid = 0;

function buildNetworkSVG(graphData, targetSmiles) {
  const TILE_W = 110, TILE_H = 76, STEP_X = 150, STEP_Y = 108, PAD = 20;
  const uid = `n${++_netUid}`;

  // Depth colour via plasma scale
  const maxDepth = Math.max(1, ...graphData.nodes.map(n => n.level ?? 0));
  function depthHex(lvl) {
    const [r, g, b] = plasmaAt(Math.min(1, lvl / maxDepth));
    return '#' + [r, g, b].map(v => Math.round(v * 255).toString(16).padStart(2, '0')).join('');
  }

  // Group nodes by level
  const byLevel = new Map();
  for (const n of graphData.nodes) {
    const lvl = n.level ?? 0;
    if (!byLevel.has(lvl)) byLevel.set(lvl, []);
    byLevel.get(lvl).push(n);
  }
  const maxLevel   = Math.max(...[...byLevel.keys()]);
  const maxInLevel = Math.max(...[...byLevel.values()].map(a => a.length));

  const W = (maxLevel + 1) * STEP_X + 2 * PAD;
  const H = maxInLevel * STEP_Y + 2 * PAD;

  // Centre each column vertically
  const pos = new Map();
  for (const [lvl, nodes] of byLevel) {
    const totalH = nodes.length * STEP_Y;
    const startY  = (H - totalH) / 2 + STEP_Y / 2;
    nodes.forEach((n, i) => {
      pos.set(n.id, {
        cx: PAD + Number(lvl) * STEP_X + TILE_W / 2,
        cy: startY + i * STEP_Y,
      });
    });
  }

  // Deduplicate edges (MultiDiGraph may have parallel edges)
  const seen = new Set();
  const edges = graphData.edges.filter(({ source, target }) => {
    const k = `${source}||${target}`;
    return seen.has(k) ? false : (seen.add(k), true);
  });

  const defs = `<defs>
    <linearGradient id="eg_${uid}" x1="0" y1="0" x2="1" y2="0">
      <stop offset="0%"   stop-color="#00c8ff" stop-opacity="0.3"/>
      <stop offset="100%" stop-color="#00c8ff" stop-opacity="0.8"/>
    </linearGradient>
    <marker id="arr_${uid}" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto">
      <path d="M0,0 L7,3.5 L0,7 z" fill="#00c8ff" opacity="0.75"/>
    </marker>
    <filter id="glow_${uid}" x="-55%" y="-55%" width="210%" height="210%">
      <feGaussianBlur stdDeviation="5" result="blur"/>
      <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
    </filter>
  </defs>`;

  // Curved edges — connect right-centre of source to left-centre of target
  let edgesHtml = '';
  for (const { source, target } of edges) {
    const s = pos.get(source), t = pos.get(target);
    if (!s || !t) continue;
    const sx = s.cx + TILE_W / 2, sy = s.cy;
    const tx = t.cx - TILE_W / 2 - 3, ty = t.cy;
    const dx = tx - sx;
    edgesHtml += `<path d="M${sx},${sy} C${sx + dx * 0.45},${sy} ${tx - dx * 0.45},${ty} ${tx},${ty}"
      fill="none" stroke="url(#eg_${uid})" stroke-width="1.5" marker-end="url(#arr_${uid})"/>`;
  }

  // Tiles
  let nodesHtml = '';
  for (const n of graphData.nodes) {
    const p = pos.get(n.id);
    if (!p) continue;
    const isTarget = n.id === targetSmiles;
    const inViz    = smilesToNode.has(n.id);
    const lvl      = n.level ?? 0;
    const col      = depthHex(lvl);
    const bx       = p.cx - TILE_W / 2;
    const by       = p.cy - TILE_H / 2;

    const molContent = getMolSVGContent(n.id, TILE_W - 10, TILE_H - 10);
    const imgEl  = molContent
      ? `<g transform="translate(${bx + 5},${by + 5})">${molContent}</g>`
      : `<text x="${p.cx}" y="${p.cy + 4}" text-anchor="middle"
           font-size="7" fill="rgba(255,255,255,.45)" font-family="monospace">${_esc(n.id.slice(0, 16))}</text>`;

    const glowEl = isTarget ? `
      <rect x="${bx - 12}" y="${by - 12}" width="${TILE_W + 24}" height="${TILE_H + 24}" rx="12"
            fill="none" stroke="${col}" stroke-width="5" opacity="0.18" filter="url(#glow_${uid})"/>
      <rect x="${bx - 6}"  y="${by - 6}"  width="${TILE_W + 12}" height="${TILE_H + 12}" rx="9"
            fill="none" stroke="${col}" stroke-width="1.5" opacity="0.38"/>` : '';

    nodesHtml += `<g class="net-node" data-smiles="${_esc(n.id)}"
        style="${inViz ? 'cursor:pointer' : ''}">
      ${glowEl}
      <rect x="${bx}" y="${by}" width="${TILE_W}" height="${TILE_H}"
        rx="7" fill="${isTarget ? '#1c1008' : '#0d0d25'}"
        stroke="${col}" stroke-width="${isTarget ? 2.5 : 2}"/>
      ${imgEl}
      <rect x="${bx}" y="${by}" width="22" height="14" rx="4" fill="${col}"/>
      <text x="${bx + 11}" y="${by + 10}" text-anchor="middle" font-size="8" font-weight="800"
            fill="white" font-family="monospace">d${lvl}</text>
      <title>${_esc(n.id)}</title>
    </g>`;
  }

  return `<svg viewBox="0 0 ${W} ${H}" width="100%" xmlns="http://www.w3.org/2000/svg"
      style="display:block;border-radius:8px;background:${_netBg()}">
    ${defs}
    <g>${edgesHtml}${nodesHtml}</g>
  </svg>`;
}

let _netZoomCleanup = null;

function attachNetZoom(svg) {
  if (_netZoomCleanup) { _netZoomCleanup(); _netZoomCleanup = null; }
  if (!svg) return;
  const g = svg.querySelector('.nz');
  if (!g) return;
  let s = 1, tx = 0, ty = 0, down = false, lx = 0, ly = 0;

  function applyT() {
    g.setAttribute('transform', `translate(${tx.toFixed(1)},${ty.toFixed(1)}) scale(${s.toFixed(3)})`);
  }

  svg.addEventListener('wheel', e => {
    e.preventDefault();
    e.stopPropagation();
    const r   = svg.getBoundingClientRect();
    const vb  = svg.viewBox.baseVal;
    const scx = vb.width  / r.width;
    const scy = vb.height / r.height;
    const mx  = (e.clientX - r.left) * scx;
    const my  = (e.clientY - r.top)  * scy;
    const f   = e.deltaY > 0 ? 0.88 : 1.14;
    const ns  = Math.max(0.2, Math.min(8, s * f));
    tx = mx - (mx - tx) * (ns / s);
    ty = my - (my - ty) * (ns / s);
    s  = ns;
    applyT();
  }, { passive: false });

  svg.addEventListener('mousedown', e => {
    if (e.button !== 0 || e.target.closest('.net-node')) return;
    down = true; lx = e.clientX; ly = e.clientY;
    svg.style.cursor = 'grabbing';
    e.preventDefault();
  });

  const onMove = e => {
    if (!down) return;
    const r   = svg.getBoundingClientRect();
    const vb  = svg.viewBox.baseVal;
    tx += (e.clientX - lx) * (vb.width  / r.width);
    ty += (e.clientY - ly) * (vb.height / r.height);
    lx = e.clientX; ly = e.clientY;
    applyT();
  };

  const onUp = () => { down = false; svg.style.cursor = 'grab'; };

  window.addEventListener('mousemove', onMove);
  window.addEventListener('mouseup',   onUp);

  _netZoomCleanup = () => {
    window.removeEventListener('mousemove', onMove);
    window.removeEventListener('mouseup',   onUp);
  };
}

async function renderNetworkPanel(node) {
  netCanvas.innerHTML = `<div style="padding:20px;color:#55556a;font-size:12px;font-family:monospace">Loading…</div>`;
  showNetPanel(node.id);
  try {
    const res = await fetch(`/api/network?smiles=${encodeURIComponent(node.id)}`);
    if (!res.ok) {
      netCanvas.innerHTML = `<div style="padding:20px;color:#55556a;font-size:12px;font-family:monospace">No pathway data for this molecule.</div>`;
      return;
    }
    const graphData = await res.json();
    netCanvas.innerHTML = buildNetworkSVG(graphData, node.id);

    netCanvas.querySelectorAll(".net-node[data-smiles]").forEach(el => {
      el.addEventListener("click", () => {
        const nd = smilesToNode.get(el.dataset.smiles);
        if (nd) pinNode(nd);
      });

      el.addEventListener("mouseenter", (e) => {
        const smiles = el.dataset.smiles;
        const nd     = smilesToNode.get(smiles);
        const desc   = getDescriptors(smiles);
        const fmt    = v => typeof v === "number" ? (Number.isInteger(v) ? v : v.toFixed(2)) : (v ?? "—");
        const rows   = [
          ["MW",             fmt(desc?.amw              ?? nd?.attributes?.mw),    "g/mol"],
          ["LogP",           fmt(desc?.alogp            ?? nd?.attributes?.logp),  ""],
          ["TPSA",           fmt(desc?.tpsa             ?? nd?.attributes?.tpsa),  "Å²"],
          ["HBA",            fmt(desc?.lipinskiHBA      ?? nd?.attributes?.hba),   ""],
          ["HBD",            fmt(desc?.lipinskiHBD      ?? nd?.attributes?.hbd),   ""],
          ["Atoms",          fmt(desc?.numHeavyAtoms    ?? null),                   ""],
          ["Assembly Depth", fmt(nd?.depth              ?? null),                   ""],
        ].filter(([, v]) => v !== "—");
        tooltip.innerHTML =
          `<div style="font-size:9px;color:#aaa;word-break:break-all;margin-bottom:6px;max-width:180px">${_esc(smiles)}</div>` +
          rows.map(([l, v, u]) =>
            `<div style="display:flex;justify-content:space-between;gap:12px">
              <span style="color:#8888aa">${l}</span>
              <span style="color:#fff;font-weight:600">${v}${u ? " "+u : ""}</span>
            </div>`
          ).join("");
        tooltip.style.display = "block";
        tooltip.style.left = (e.clientX + 14) + "px";
        tooltip.style.top  = (e.clientY + 14) + "px";
      });

      el.addEventListener("mousemove", (e) => {
        tooltip.style.left = (e.clientX + 14) + "px";
        tooltip.style.top  = (e.clientY + 14) + "px";
      });

      el.addEventListener("mouseleave", () => { tooltip.style.display = "none"; });
    });
  } catch {
    netCanvas.innerHTML = `<div style="padding:20px;color:#55556a;font-size:12px;font-family:monospace">No pathway data for this molecule.</div>`;
  }
}

// --------------------------------------------------
// Similarity panel
// --------------------------------------------------

function buildSimilarityPanelHTML(neighbours) {
  const sorted = [...neighbours].sort((a, b) => b.score - a.score).slice(0, 15);
  const cards  = sorted.map(({ idx, score }) => {
    const n    = nodeData[idx];
    const svg  = getMoleculeSVGFluid(n.id, 96, 72);
    const pct  = Math.round(score * 100);
    const col  = score >= 0.7 ? "#44dd88" : score >= 0.5 ? "#4499ff" : "#aaaacc";
    const inViz = smilesToNode.has(n.id);
    return `
      <div class="sim-card" data-smiles="${_esc(n.id)}" data-idx="${idx}"
           style="flex-shrink:0;width:140px;background:#0e0e1e;border:1px solid #1c1c30;
                  border-radius:8px;padding:6px;cursor:${inViz ? "pointer" : "default"}">
        ${svg
          ? `<div style="background:#fff;border-radius:8px;padding:6px;line-height:0;margin-bottom:6px">${svg}</div>`
          : `<div style="height:72px;display:flex;align-items:center;justify-content:center;color:#444;font-size:9px;margin-bottom:6px">no structure</div>`}
        <div style="font-size:9px;color:#ffffff;word-break:break-all;
                    margin-bottom:4px;font-family:monospace;line-height:1.3">${_esc(n.id)}</div>
        <div style="font-size:11px;font-weight:700;color:${col}">${pct}% similar</div>
      </div>`;
  }).join("");
  return `<div style="display:flex;flex-wrap:wrap;gap:12px;padding:12px 20px;
    justify-content:center;align-items:flex-start;
    width:100%;box-sizing:border-box">${cards}</div>`;
}

function renderSimilarityPanel(node) {
  const neighbours = simAdj.get(node._idx) || [];
  showNetPanel(node.id, "Similarity Network");
  if (!neighbours.length) {
    netCanvas.innerHTML = `<div style="padding:20px;color:#55556a;font-size:12px;font-family:monospace">No similarity data for this molecule.</div>`;
    return;
  }
  netCanvas.innerHTML = buildSimilarityPanelHTML(neighbours);

  netCanvas.querySelectorAll(".sim-card[data-smiles]").forEach(el => {
    el.addEventListener("click", () => {
      const nd = smilesToNode.get(el.dataset.smiles);
      if (nd) pinNode(nd);
    });

    el.addEventListener("mouseenter", (e) => {
      const smiles = el.dataset.smiles;
      const nd  = nodeData[parseInt(el.dataset.idx)];
      const desc = getDescriptors(smiles);
      const fmt  = v => (typeof v === "number" ? (Number.isInteger(v) ? v : v.toFixed(2)) : (v ?? "—"));
      tooltip.innerHTML = [
        ["MW",             fmt(desc?.amw              ?? nd?.attributes?.mw)     , "g/mol"],
        ["Atoms",          fmt(desc?.numHeavyAtoms    ?? null)                   , ""],
        ["Bonds",          fmt(nd?.attributes?.bond_count ?? null)               , ""],
        ["Rings",          fmt(nd?.attributes?.ring_count ?? null)               , ""],
        ["LogP",           fmt(desc?.alogp            ?? nd?.attributes?.logp)   , ""],
        ["Assembly Depth", fmt(nd?.depth ?? null)                                , ""],
      ].filter(([, v]) => v !== "—").map(([l, v, u]) =>
        `<div style="display:flex;justify-content:space-between;gap:10px">
          <span style="color:#8888aa">${l}</span>
          <span style="color:#fff;font-weight:600">${v}${u ? " " + u : ""}</span>
        </div>`
      ).join("");
      tooltip.style.display = "block";
      tooltip.style.left = (e.clientX + 14) + "px";
      tooltip.style.top  = (e.clientY + 14) + "px";
    });

    el.addEventListener("mousemove", (e) => {
      tooltip.style.left = (e.clientX + 14) + "px";
      tooltip.style.top  = (e.clientY + 14) + "px";
    });

    el.addEventListener("mouseleave", () => {
      tooltip.style.display = "none";
    });
  });
}

// --------------------------------------------------
// RDKit helpers
// --------------------------------------------------

function getMoleculeSVG(smiles, w = 200, h = 150) {
  if (!rdkit || !smiles) return null;
  try {
    const mol = rdkit.get_mol(smiles);
    if (!mol.is_valid()) { mol.delete(); return null; }
    const svg = mol.get_svg(w, h);
    mol.delete();
    return svg;
  } catch { return null; }
}

// Returns a fluid SVG that fills its container width, preserving aspect ratio.
function getMoleculeSVGFluid(smiles, w = 200, h = 150) {
  const raw = getMoleculeSVG(smiles, w, h);
  if (!raw) return null;
  let svg = raw;
  if (!svg.includes("viewBox")) {
    svg = svg.replace("<svg ", `<svg viewBox="0 0 ${w} ${h}" `);
  }
  svg = svg.replace(/\bwidth="[^"]*"/, 'width="100%"');
  svg = svg.replace(/\bheight="[^"]*"/, 'height="auto"');
  svg = svg.replace("<svg ", '<svg style="display:block" ');
  return svg;
}

function getDescriptors(smiles) {
  if (!rdkit || !smiles) return null;
  try {
    const mol = rdkit.get_mol(smiles);
    if (!mol.is_valid()) { mol.delete(); return null; }
    const desc = JSON.parse(mol.get_descriptors());
    mol.delete();
    return desc;
  } catch { return null; }
}

// --------------------------------------------------
// Joint Assembly Space (JAS)
// --------------------------------------------------

async function buildJASGraph(selectedIndices) {
  const smilesToIdx = new Map(nodeData.map((n, i) => [n.id, i]));
  const jasNodes = new Map(); // smiles → {id, level, idx3d}
  const edgeSet  = new Set(); // "src→tgt" dedup key
  const edgeList = [];        // [srcSmiles, tgtSmiles]

  await Promise.all(selectedIndices.map(async idx3d => {
    const smiles = nodeData[idx3d].id;
    let data;
    try {
      const r = await fetch(`/api/network?smiles=${encodeURIComponent(smiles)}`);
      if (!r.ok) return;
      data = await r.json();
    } catch { return; }
    for (const n of data.nodes) {
      if (!jasNodes.has(n.id)) {
        jasNodes.set(n.id, {
          id: n.id,
          level: n.level ?? 0,
          idx3d: smilesToIdx.has(n.id) ? smilesToIdx.get(n.id) : -1
        });
      }
    }
    for (const e of data.edges) {
      const key = `${e.source}→${e.target}`;
      if (!edgeSet.has(key)) { edgeSet.add(key); edgeList.push([e.source, e.target]); }
    }
  }));

  return {
    nodes: [...jasNodes.values()],
    edges: edgeList,
    targetSmiles: new Set(selectedIndices.map(i => nodeData[i].id))
  };
}

function applyJASHighlight(jasIdx3dSet, targetIdxSet) {
  if (!pointsMesh) return;
  const arr = pointsMesh.geometry.attributes.color.array;
  for (let i = 0; i < nodeData.length; i++) {
    if (!jasIdx3dSet.has(i)) {
      arr[i*3] = arr[i*3+1] = arr[i*3+2] = 0.04;
    } else if (targetIdxSet.has(i)) {
      arr[i*3] = 1; arr[i*3+1] = 1; arr[i*3+2] = 1;
    } else {
      const r = baseColors[i*3], g = baseColors[i*3+1], b = baseColors[i*3+2];
      arr[i*3]   = Math.min(1, r * 9.0 + 0.825);
      arr[i*3+1] = Math.min(1, g * 9.0 + 0.825);
      arr[i*3+2] = Math.min(1, b * 9.0 + 0.825);
    }
  }
  pointsMesh.geometry.attributes.color.needsUpdate = true;
}

async function renderJASPanel(selectedIndices) {
  if (!selectedIndices.length) return;

  networkMode = "jas";
  navRxnBtn.classList.remove("active");
  navSimBtn.classList.remove("active");
  showNetPanel("", "Reaction Network");
  netCanvas.innerHTML = `<div style="padding:20px;color:#6688bb;font-size:12px">Computing JAS…</div>`;

  const { nodes: allNodes, edges: allEdges, targetSmiles } = await buildJASGraph(selectedIndices);

  // Drop isolated nodes
  const connectedSmiles = new Set();
  for (const [s, t] of allEdges) { connectedSmiles.add(s); connectedSmiles.add(t); }
  const nodes = allNodes.filter(n => connectedSmiles.has(n.id));
  const edges = allEdges;

  if (!nodes.length) {
    netCanvas.innerHTML = `<div style="padding:20px;color:#c06060;font-size:12px">No pathway data found for the selected molecules.</div>`;
    return;
  }

  document.getElementById("section-panel-title").textContent =
    `Reaction Network  ·  ${selectedIndices.length} Targets  ·  ${nodes.length} Nodes`;

  // ── Layout ──────────────────────────────────────────────────
  const smiToNi = new Map(nodes.map((n, ni) => [n.id, ni]));

  const levelMap = new Map();
  for (const n of nodes) {
    if (!levelMap.has(n.level)) levelMap.set(n.level, []);
    levelMap.get(n.level).push(n);
  }
  const levels    = [...levelMap.keys()].sort((a, b) => a - b);
  const numLevels = levels.length;
  const maxNodesInRow = Math.max(...levels.map(lv => levelMap.get(lv).length));
  const NODE_SPACING  = Math.max(18, Math.min(50, 700 / Math.max(maxNodesInRow, 1)));
  const W         = Math.max(500, maxNodesInRow * NODE_SPACING + 60);
  const LEVEL_H   = Math.max(70, Math.min(110, 500 / Math.max(numLevels, 1)));
  const H         = numLevels * LEVEL_H + 60;

  const pos = new Map(); // smiles → {x, y}
  levels.forEach((lv, li) => {
    const row = levelMap.get(lv);
    const y   = 30 + li * LEVEL_H;
    row.forEach((n, ni) => {
      const x = 30 + ((ni + 0.5) / row.length) * (W - 60);
      pos.set(n.id, { x, y });
    });
  });

  // ── Node color helper ─────────────────────────────────────────
  const col = idx3d => {
    if (idx3d < 0) return '#7799cc';
    const r = Math.round(Math.min(1, baseColors[idx3d*3]   * 9.0 + 0.825) * 255);
    const g = Math.round(Math.min(1, baseColors[idx3d*3+1] * 9.0 + 0.825) * 255);
    const b = Math.round(Math.min(1, baseColors[idx3d*3+2] * 9.0 + 0.825) * 255);
    return `rgb(${r},${g},${b})`;
  };

  // ── Build SVG ───────────────────────────────────────────────
  const edgesSVG = edges.map(([s, t]) => {
    const sp = pos.get(s), tp = pos.get(t);
    if (!sp || !tp) return '';
    const sni = smiToNi.get(s), tni = smiToNi.get(t);
    if (sni == null || tni == null) return '';
    return `<path class="jas-edge" data-sni="${sni}" data-tni="${tni}"
      d="M${sp.x},${sp.y} C${sp.x},${sp.y+LEVEL_H*0.45} ${tp.x},${tp.y-LEVEL_H*0.45} ${tp.x},${tp.y}"
      fill="none" stroke="rgba(100,150,255,0.25)" stroke-width="1"/>`;
  }).join('');

  const nodesSVG = nodes.map((n, ni) => {
    const p = pos.get(n.id);
    if (!p) return '';
    const isTarget = targetSmiles.has(n.id);
    const r    = isTarget ? 2.5 : 1.5;
    const fill = isTarget ? '#ffffff' : col(n.idx3d);
    const hr   = r * 0.38;
    return `<g class="jas-node" data-ni="${ni}" opacity="1" style="cursor:pointer">
      <circle cx="${p.x}" cy="${p.y}" r="${r}" fill="${fill}" filter="url(#jas-glow)"/>
      <circle cx="${p.x - r*0.22}" cy="${p.y - r*0.28}" r="${hr}" fill="rgba(255,255,255,0.82)" pointer-events="none"/>
    </g>`;
  }).join('');

  netCanvas.innerHTML = `
    <div style="overflow-x:auto;overflow-y:auto;max-height:624px;border-radius:6px">
      <svg viewBox="0 0 ${W} ${H}" style="display:block;width:100%;height:auto;cursor:grab;background:#05070f" xmlns="http://www.w3.org/2000/svg">
        <defs>
          <filter id="jas-glow" x="-120%" y="-120%" width="340%" height="340%">
            <feGaussianBlur in="SourceGraphic" stdDeviation="2.2" result="blur"/>
            <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
          </filter>
        </defs>
        <g class="nz">
          <g id="jas-edges-g">${edgesSVG}</g>
          <g id="jas-nodes-g">${nodesSVG}</g>
        </g>
      </svg>
    </div>`;

  attachNetZoom(netCanvas.querySelector("svg"));

  // ── Hover interaction ─────────────────────────────────────────
  const jasAdj  = new Map(); // smiles → Set<smiles>
  const jasAdjR = new Map();
  for (const [s, t] of edges) {
    if (!jasAdj.has(s))  jasAdj.set(s,  new Set());
    if (!jasAdjR.has(t)) jasAdjR.set(t, new Set());
    jasAdj.get(s).add(t);
    jasAdjR.get(t).add(s);
  }

  function ancestorsOf(smiles) {
    const seen = new Set([smiles]);
    let frontier = [smiles];
    while (frontier.length) {
      const next = [];
      for (const x of frontier) {
        for (const p of (jasAdjR.get(x) || [])) if (!seen.has(p)) { seen.add(p); next.push(p); }
      }
      frontier = next;
    }
    return seen;
  }

  const nodeEls = netCanvas.querySelectorAll('.jas-node');
  const edgeEls = netCanvas.querySelectorAll('.jas-edge');

  nodeEls.forEach(el => {
    el.addEventListener('mouseenter', e => {
      const n         = nodes[+el.dataset.ni];
      const ancestors = ancestorsOf(n.id);

      nodeEls.forEach(nel => {
        nel.setAttribute('opacity', ancestors.has(nodes[+nel.dataset.ni].id) ? '1' : '0.06');
      });
      edgeEls.forEach(eel => {
        const sId = nodes[+eel.dataset.sni]?.id, tId = nodes[+eel.dataset.tni]?.id;
        const on  = sId && tId && ancestors.has(sId) && ancestors.has(tId);
        eel.setAttribute('stroke', on ? 'rgba(150,200,255,0.7)' : 'rgba(100,150,255,0.04)');
        eel.setAttribute('stroke-width', on ? '1.5' : '1');
      });

      const svg = getMoleculeSVG(n.id, 160, 120);
      tooltip.innerHTML =
        `<div style="font-size:9px;color:#aaa;word-break:break-all;margin-bottom:5px;max-width:180px">${n.id}</div>` +
        (svg ? `<div style="background:#fff;border-radius:3px;padding:2px;line-height:0">${svg}</div>` : '');
      tooltip.style.display = 'block';
      tooltip.style.left = (e.clientX + 14) + 'px';
      tooltip.style.top  = (e.clientY + 14) + 'px';
    });
    el.addEventListener('mousemove', e => {
      tooltip.style.left = (e.clientX + 14) + 'px';
      tooltip.style.top  = (e.clientY + 14) + 'px';
    });
    el.addEventListener('mouseleave', () => {
      nodeEls.forEach(nel => nel.setAttribute('opacity', '1'));
      edgeEls.forEach(eel => {
        eel.setAttribute('stroke', 'rgba(100,150,255,0.25)');
        eel.setAttribute('stroke-width', '1');
      });
      tooltip.style.display = 'none';
    });
    el.addEventListener('click', () => {
      const n = nodes[+el.dataset.ni];
      if (n.idx3d >= 0) pinNode(nodeData[n.idx3d]);
    });
  });
}


// JAS button inside the subset banner
document.getElementById("subset-jas").addEventListener("click", async () => {
  if (!subsetIdxSet || subsetIdxSet.size === 0) return;
  if (networkMode === "jas") {
    networkMode = "none";
    hideNetPanel();
    return;
  }
  navRxnBtn.classList.remove("active");
  navSimBtn.classList.remove("active");
  hideStatus();
  await renderJASPanel([...subsetIdxSet]);
});

// --------------------------------------------------
// Graph + edge loading
// --------------------------------------------------

async function loadGraph() {
  showStatus("Loading molecules...");
  try {
    const [nodesRes, edgesRes] = await Promise.all([
      fetch("/data/processed/nodes_compact.json"),
      fetch("/data/processed/edges.json"),
    ]);
    if (!nodesRes.ok) throw new Error(`nodes HTTP ${nodesRes.status}`);
    if (!edgesRes.ok) throw new Error(`edges HTTP ${edgesRes.status}`);

    const compact = await nodesRes.json();
    const attrKeys = compact.attrKeys;
    nodeData = compact.ids.map((id, i) => {
      const attrs = {};
      attrKeys.forEach((k, j) => { attrs[k] = compact.attrs[i][j]; });
      return { id, x: compact.x[i], y: compact.y[i], z: compact.z[i],
               depth: compact.depth[i], attributes: attrs, _idx: i };
    });
    nodeData.forEach(n => smilesToNode.set(n.id, n));
    const edges = await edgesRes.json();

    // Assembly adjacency (directed: lower-depth → higher-depth)
    for (const [si, ti] of edges) {
      if (!adj.has(si))  adj.set(si,  new Set());
      if (!adjR.has(ti)) adjR.set(ti, new Set());
      adj.get(si).add(ti);
      adjR.get(ti).add(si);
    }

    propRanges = computePropRanges(nodeData);
    buildPointCloud(nodeData);
    recolour("depth");
    document.getElementById("pts-count").textContent = `${nodeData.length.toLocaleString()} pts`;
    initFilterPanel();
    hideStatus();
    console.log(`Loaded ${nodeData.length} nodes, ${edges.length} assembly edges`);
  } catch (err) {
    showStatus(`Failed to load: ${err.message}`, true);
  }
}

// Bright solid core + wide soft halo — ESM-Atlas-style glowing disc.
function makeGlowTexture(size = 256) {
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext("2d");
  const r = size / 2;
  const grad = ctx.createRadialGradient(r, r, 0, r, r, r);
  grad.addColorStop(0,    "rgba(255,255,255,1)");    // pure white centre
  grad.addColorStop(0.12, "rgba(255,255,255,1)");    // solid bright core
  grad.addColorStop(0.30, "rgba(255,255,255,0.85)"); // inner halo
  grad.addColorStop(0.55, "rgba(255,255,255,0.40)"); // mid glow
  grad.addColorStop(0.80, "rgba(255,255,255,0.10)"); // outer haze
  grad.addColorStop(1,    "rgba(255,255,255,0)");    // transparent edge
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, size, size);
  return new THREE.CanvasTexture(canvas);
}

function buildPointCloud(nodes) {
  const geometry  = new THREE.BufferGeometry();
  const positions = new Float32Array(nodes.length * 3);
  const colors    = new Float32Array(nodes.length * 3);

  nodes.forEach((n, i) => {
    positions[i * 3]     = n.x || 0;
    positions[i * 3 + 1] = n.y || 0;
    positions[i * 3 + 2] = n.z || 0;
    colors[i * 3] = colors[i * 3 + 1] = colors[i * 3 + 2] = 0.4;
  });

  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color",    new THREE.BufferAttribute(colors, 3));

  pointsMesh = new THREE.Points(
    geometry,
    new THREE.PointsMaterial({
      size: 0.7,
      vertexColors: true,
      map: makeGlowTexture(),
      transparent: true,
      opacity: 0.92,
      alphaTest: 0.004,
      depthWrite: false,
      sizeAttenuation: true,
    })
  );
  scene.add(pointsMesh);

  baseColors = new Float32Array(colors);
}

// --------------------------------------------------
// Mouse / click handlers
// --------------------------------------------------

window.addEventListener("mousemove", (e) => {
  const r = canvasWrap.getBoundingClientRect();
  mouse.x =  ((e.clientX - r.left) / r.width)  * 2 - 1;
  mouse.y = -((e.clientY - r.top)  / r.height) * 2 + 1;
  tooltip.style.left = (e.clientX + 14) + "px";
  tooltip.style.top  = (e.clientY + 14) + "px";
});

let _suppressNextClick = false;

window.addEventListener("click", (e) => {
  if (_suppressNextClick) { _suppressNextClick = false; return; }
  if (!pointsMesh) return;
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObject(pointsMesh);
  if (hits.length > 0) {
    const clicked = nodeData[hits[0].index];
    if (e.shiftKey && pinnedNode && clicked !== pinnedNode) {
      highlightShortestPath(pinnedNode._idx, clicked._idx);
    } else {
      pinNode(clicked);
    }
  }
});

// --------------------------------------------------
// Animation loop
// --------------------------------------------------

function animate() {
  requestAnimationFrame(animate);
  controls.update();

  if (cameraFlyTo) {
    camera.position.lerp(
      new THREE.Vector3(cameraFlyTo.x, cameraFlyTo.y, cameraFlyTo.z + 30),
      0.08
    );
    controls.target.lerp(cameraFlyTo, 0.08);
    if (camera.position.distanceTo(
          new THREE.Vector3(cameraFlyTo.x, cameraFlyTo.y, cameraFlyTo.z + 30)
        ) < 0.5) cameraFlyTo = null;
  }

  if (pointsMesh) {
    raycaster.setFromCamera(mouse, camera);
    const hits = raycaster.intersectObject(pointsMesh);

    if (hits.length > 0) {
      const idx  = hits[0].index;
      const node = nodeData[idx];
      if (node) {
        ensureRDKit();
        _setBeaconPos(hoverBeacon, node.x, node.y, node.z);
        hoverBeacon.visible   = true;
        tooltip.style.display = "block";

        if (idx !== lastHoverIdx) {
          lastHoverIdx = idx;
          showHoverPathwayLines(idx);

          // Tooltip: SMILES + structure
          const svg = getMoleculeSVG(node.id, 160, 120);
          tooltip.innerHTML =
            `<div style="margin-bottom:5px;word-break:break-all">${node.id}</div>` +
            (svg ? `<div style="background:#fff;border-radius:3px;padding:2px;line-height:0">${svg}</div>` : "");
        }
      }
    } else {
      if (lastHoverIdx !== -1) {
        lastHoverIdx          = -1;
        hoverBeacon.visible   = false;
        tooltip.style.display = "none";
        if (hoverLines) {
          scene.remove(hoverLines);
          hoverLines.geometry.dispose();
          hoverLines = null;
        }
      }
    }

    // Gentle pulse on the selection ring
    if (pinBeacon.visible) {
      const t = performance.now() * 0.001;
      pinBeacon.material.size = PIN_BASE_SIZE * (1.0 + 0.10 * Math.sin(t * 2.8));
    }
  }

  renderer.render(scene, camera);
}

// --------------------------------------------------
// Colour legend
// --------------------------------------------------

function updateLegend(mode, range) {
  const NAMES = { depth: 'Asm. Depth', mw: 'MW', logp: 'LogP', tpsa: 'TPSA' };
  const fmt   = v => Math.abs(v) >= 100 ? Math.round(v).toString() : v.toFixed(1);
  document.getElementById('legend-prop-name').textContent = NAMES[mode] || mode;
  document.getElementById('legend-min-val').textContent   = fmt(range.min);
  document.getElementById('legend-max-val').textContent   = fmt(range.max);
}

// --------------------------------------------------
// Property histogram (inside colour popup)
// --------------------------------------------------

function drawHistogram(mode) {
  const hist  = document.getElementById('legend-hist');
  const range = propRanges[mode];
  if (!hist || !range || !nodeData.length) return;
  const ctx2   = hist.getContext('2d');
  const BINS   = hist.width;
  const getter = mode === 'depth' ? n => n.depth : n => n.attributes?.[mode];
  const { min, max } = range;
  const bins = new Array(BINS).fill(0);
  for (const n of nodeData) {
    const v = getter(n);
    if (v == null) continue;
    bins[Math.min(BINS - 1, Math.floor(((v - min) / (max - min + 1e-9)) * BINS))]++;
  }
  const maxBin = Math.max(...bins, 1);
  const H = hist.height;
  ctx2.clearRect(0, 0, BINS, H);
  for (let i = 0; i < BINS; i++) {
    const [r, g, b] = plasmaAt((i + 0.5) / BINS);
    ctx2.fillStyle = `rgb(${(r*255)|0},${(g*255)|0},${(b*255)|0})`;
    const bh = Math.max(1, Math.round((bins[i] / maxBin) * H));
    ctx2.fillRect(i, H - bh, 1, bh);
  }
}

// --------------------------------------------------
// Screenshot
// --------------------------------------------------

document.getElementById('tb-screenshot').addEventListener('click', () => {
  renderer.render(scene, camera);
  const url = renderer.domElement.toDataURL('image/png');
  const a   = document.createElement('a');
  a.href     = url;
  a.download = 'chemical-space.png';
  a.click();
});

// --------------------------------------------------
// SMARTS substructure search
// --------------------------------------------------

const tbSmarts       = document.getElementById('tb-smarts');
const smartsOverlay  = document.getElementById('smarts-overlay');
const smartsInput    = document.getElementById('smarts-input');
const smartsProgress = document.getElementById('smarts-progress');
let   smartsCancelToken = { cancelled: false };

tbSmarts.addEventListener('click', (e) => {
  e.stopPropagation();
  smartsOverlay.classList.toggle('open');
  tbSmarts.classList.toggle('active', smartsOverlay.classList.contains('open'));
  // close other overlays
  searchOverlay.classList.remove('open');
  colourPopup.classList.remove('open');
  tbColour.classList.remove('active');
  filterPanel.classList.remove('open');
  tbFilter.classList.remove('active');
  if (smartsOverlay.classList.contains('open')) smartsInput.focus();
});

document.getElementById('smarts-close-btn').addEventListener('click', () => {
  smartsOverlay.classList.remove('open');
  tbSmarts.classList.remove('active');
});

smartsInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter')  runSmartsSearch(smartsInput.value.trim());
  if (e.key === 'Escape') { smartsOverlay.classList.remove('open'); tbSmarts.classList.remove('active'); }
});

document.getElementById('smarts-run-btn').addEventListener('click', () => {
  runSmartsSearch(smartsInput.value.trim());
});

document.getElementById('smarts-clear-btn').addEventListener('click', () => {
  smartsCancelToken.cancelled = true;
  if (pointsMesh) {
    pointsMesh.geometry.attributes.color.array.set(baseColors);
    pointsMesh.geometry.attributes.color.needsUpdate = true;
  }
  smartsProgress.textContent = '';
  smartsInput.value = '';
});

smartsOverlay.addEventListener('click', (e) => e.stopPropagation());

async function runSmartsSearch(smarts) {
  if (!rdkit || !pointsMesh || !smarts) return;

  smartsCancelToken.cancelled = true;
  const token = { cancelled: false };
  smartsCancelToken = token;

  let qmol;
  try { qmol = rdkit.get_qmol(smarts); }
  catch { smartsProgress.textContent = 'Invalid SMARTS pattern'; return; }
  if (!qmol.is_valid()) { qmol.delete(); smartsProgress.textContent = 'Invalid SMARTS pattern'; return; }

  const arr   = pointsMesh.geometry.attributes.color.array;
  const CHUNK = 500;
  let matchCount = 0;

  for (let start = 0; start < nodeData.length; start += CHUNK) {
    if (token.cancelled) break;
    const end = Math.min(start + CHUNK, nodeData.length);

    for (let i = start; i < end; i++) {
      const mol = rdkit.get_mol(nodeData[i].id);
      let hasMatch = false;
      if (mol.is_valid()) {
        try {
          const m = JSON.parse(mol.get_substruct_match(qmol));
          hasMatch = m.atoms && m.atoms.length > 0;
        } catch { /* malformed result — treat as no match */ }
        if (hasMatch) matchCount++;
      }
      mol.delete();

      if (hasMatch) {
        arr[i*3] = 0.15; arr[i*3+1] = 1.0; arr[i*3+2] = 0.45; // bright green
      } else {
        arr[i*3]   = baseColors[i*3]   * 0.05;
        arr[i*3+1] = baseColors[i*3+1] * 0.05;
        arr[i*3+2] = baseColors[i*3+2] * 0.05;
      }
    }

    pointsMesh.geometry.attributes.color.needsUpdate = true;
    smartsProgress.textContent = `Searching… ${Math.round(end / nodeData.length * 100)}%`;
    await new Promise(r => setTimeout(r, 0));
  }

  qmol.delete();
  if (!token.cancelled) {
    smartsProgress.textContent = `${matchCount.toLocaleString()} match${matchCount !== 1 ? 'es' : ''}`;
  }
}

// --------------------------------------------------
// Shortest assembly path  (Shift + click second molecule)
// --------------------------------------------------

function shortestPath(startIdx, endIdx) {
  if (startIdx === endIdx) return [startIdx];
  const parent = new Map([[startIdx, -1]]);
  const queue  = [startIdx];
  while (queue.length > 0) {
    const curr = queue.shift();
    if (curr === endIdx) {
      const path = [];
      let n = endIdx;
      while (n !== -1) { path.unshift(n); n = parent.get(n); }
      return path;
    }
    for (const nb of [...(adj.get(curr) || []), ...(adjR.get(curr) || [])]) {
      if (!parent.has(nb)) { parent.set(nb, curr); queue.push(nb); }
    }
  }
  return null;
}

function highlightShortestPath(startIdx, endIdx) {
  const path = shortestPath(startIdx, endIdx);
  if (!path) {
    showStatus('No assembly path connects these two molecules', true);
    setTimeout(hideStatus, 2500);
    return;
  }
  applyPathwayHighlight(new Set(path));
  _setBeaconPos(pinBeacon, nodeData[endIdx].x, nodeData[endIdx].y, nodeData[endIdx].z);
  showStatus(`Assembly path: ${path.length - 1} step${path.length !== 2 ? 's' : ''}`);
}

// --------------------------------------------------
// --------------------------------------------------
// Subset mode — Enter on lasso selection
// --------------------------------------------------

const subsetBanner = document.getElementById('subset-banner');
const subsetLabel  = document.getElementById('subset-label');


function exitSubsetMode() {
  if (!subsetIdxSet) return;
  subsetIdxSet = null;
  adj     = _savedAdj;
  adjR    = _savedAdjR;
  simAdj  = _savedSimAdj;
  _savedAdj = _savedAdjR = _savedSimAdj = null;

  const arr = pointsMesh.geometry.attributes.color.array;
  arr.set(baseColors);
  pointsMesh.geometry.attributes.color.needsUpdate = true;

  // Re-apply existing highlight if a node is still pinned
  if (pinnedNode) {
    if (networkMode === "similarity") applySimilarityHighlight(pinnedNode._idx);
    else applyPathwayHighlight(bfsPathway(pinnedNode._idx));
  }

  document.getElementById('pts-count').textContent = `${nodeData.length.toLocaleString()} pts`;
  subsetBanner.style.display = 'none';
  hideStatus();
  if (networkMode === "jas") { networkMode = "none"; hideNetPanel(); }
}

document.getElementById('subset-exit').addEventListener('click', exitSubsetMode);

// --------------------------------------------------
// Box selection  (toggle via toolbar, then drag)
// --------------------------------------------------

const selectOverlay = document.getElementById('select-overlay');
const selectCtx     = selectOverlay.getContext('2d');
let   selectMode    = false;
let   selDragStart  = null;

const tbSelect = document.getElementById('tb-select');

tbSelect.addEventListener('click', (e) => {
  e.stopPropagation();
  if (networkMode !== "none" && networkMode !== "jas") {
    showStatus("Turn off the active network view before selecting", true);
    setTimeout(hideStatus, 2000);
    return;
  }
  selectMode = !selectMode;
  tbSelect.classList.toggle('active', selectMode);
  selectOverlay.classList.toggle('active', selectMode);
  controls.enabled = !selectMode;
  if (!selectMode) {
    selectCtx.clearRect(0, 0, selectOverlay.width, selectOverlay.height);
    selectedIndices = [];
    if (!subsetIdxSet) {
      const arr = pointsMesh.geometry.attributes.color.array;
      arr.set(baseColors);
      pointsMesh.geometry.attributes.color.needsUpdate = true;
      document.getElementById('pts-count').textContent = `${nodeData.length.toLocaleString()} pts`;
    }
    hideStatus();
  }
});

selectOverlay.addEventListener('mousedown', (e) => {
  const r = canvasWrap.getBoundingClientRect();
  selDragStart = { x: e.clientX - r.left, y: e.clientY - r.top };
});

selectOverlay.addEventListener('mousemove', (e) => {
  if (!selDragStart) return;
  const r  = canvasWrap.getBoundingClientRect();
  const x  = e.clientX - r.left, y = e.clientY - r.top;
  const rx = Math.min(selDragStart.x, x), ry = Math.min(selDragStart.y, y);
  const rw = Math.abs(x - selDragStart.x), rh = Math.abs(y - selDragStart.y);
  selectCtx.clearRect(0, 0, selectOverlay.width, selectOverlay.height);
  selectCtx.fillStyle   = 'rgba(100,160,255,.06)';
  selectCtx.fillRect(rx, ry, rw, rh);
  selectCtx.strokeStyle = 'rgba(255,255,255,.75)';
  selectCtx.lineWidth   = 1;
  selectCtx.setLineDash([4, 3]);
  selectCtx.strokeRect(rx + .5, ry + .5, rw, rh);
});

selectOverlay.addEventListener('mouseup', (e) => {
  if (!selDragStart) return;
  const r   = canvasWrap.getBoundingClientRect();
  const x   = e.clientX - r.left, y = e.clientY - r.top;
  selectCtx.clearRect(0, 0, selectOverlay.width, selectOverlay.height);
  const ndcX0 = selDragStart.x / r.width  *  2 - 1;
  const ndcY0 = selDragStart.y / r.height * -2 + 1;
  const ndcX1 = x / r.width  *  2 - 1;
  const ndcY1 = y / r.height * -2 + 1;
  selDragStart = null;
  _suppressNextClick = true;
  applyBoxSelection(ndcX0, ndcY0, ndcX1, ndcY1);
});

selectOverlay.addEventListener('mouseleave', () => {
  if (selDragStart) { selectCtx.clearRect(0, 0, selectOverlay.width, selectOverlay.height); selDragStart = null; }
});

function applyBoxSelection(ndcX0, ndcY0, ndcX1, ndcY1) {
  if (!pointsMesh) return;
  const minX = Math.min(ndcX0, ndcX1), maxX = Math.max(ndcX0, ndcX1);
  const minY = Math.min(ndcY0, ndcY1), maxY = Math.max(ndcY0, ndcY1);
  if (maxX - minX < 0.01 && maxY - minY < 0.01) return;

  camera.updateMatrixWorld();
  const proj = new THREE.Matrix4()
    .multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
  const pos = pointsMesh.geometry.attributes.position.array;
  const arr = pointsMesh.geometry.attributes.color.array;
  const v   = new THREE.Vector3();
  const pool = subsetIdxSet ? [...subsetIdxSet] : null;
  selectedIndices = [];

  const total = pool ? pool.length : nodeData.length;
  for (let ii = 0; ii < total; ii++) {
    const i = pool ? pool[ii] : ii;
    v.set(pos[i*3], pos[i*3+1], pos[i*3+2]).applyMatrix4(proj);
    if (v.x >= minX && v.x <= maxX && v.y >= minY && v.y <= maxY && v.z < 1) {
      arr[i*3] = baseColors[i*3]; arr[i*3+1] = baseColors[i*3+1]; arr[i*3+2] = baseColors[i*3+2];
      selectedIndices.push(i);
    } else {
      arr[i*3] = baseColors[i*3] * 0.35; arr[i*3+1] = baseColors[i*3+1] * 0.35; arr[i*3+2] = baseColors[i*3+2] * 0.35;
    }
  }
  // Keep non-pool nodes fully hidden
  if (pool) {
    const poolSet = subsetIdxSet;
    for (let i = 0; i < nodeData.length; i++) {
      if (!poolSet.has(i)) { arr[i*3] = arr[i*3+1] = arr[i*3+2] = 0; }
    }
  }
  pointsMesh.geometry.attributes.color.needsUpdate = true;

  if (selectedIndices.length === 0) return;
  document.getElementById('pts-count').textContent = `${selectedIndices.length.toLocaleString()} sel`;

  // Immediately commit — enter subset mode without resetting the lasso colours
  _commitSubset(selectedIndices);

  // Exit lasso mode so the cloud is interactive again
  selectMode = false;
  tbSelect.classList.remove("active");
  selectOverlay.classList.remove("active");
  controls.enabled = true;

}

function _commitSubset(indices) {
  // Restore saved maps if we're refining an existing subset
  if (_savedAdj) { adj = _savedAdj; adjR = _savedAdjR; simAdj = _savedSimAdj; }

  subsetIdxSet = new Set(indices);
  _savedAdj = adj; _savedAdjR = adjR; _savedSimAdj = simAdj;

  adj = new Map(); adjR = new Map();
  for (const [si, targets] of _savedAdj) {
    if (!subsetIdxSet.has(si)) continue;
    for (const ti of targets) {
      if (!subsetIdxSet.has(ti)) continue;
      if (!adj.has(si))  adj.set(si,  new Set());
      if (!adjR.has(ti)) adjR.set(ti, new Set());
      adj.get(si).add(ti); adjR.get(ti).add(si);
    }
  }
  simAdj = new Map();
  for (const [si, nbrs] of _savedSimAdj) {
    if (!subsetIdxSet.has(si)) continue;
    const f = nbrs.filter(({ idx }) => subsetIdxSet.has(idx));
    if (f.length) simAdj.set(si, f);
  }

  if (pinnedNode && !subsetIdxSet.has(pinnedNode._idx)) unpinNode();
  else if (pinnedNode) {
    if (networkMode === "similarity") applySimilarityHighlight(pinnedNode._idx);
    else applyPathwayHighlight(bfsPathway(pinnedNode._idx));
  }

  subsetLabel.textContent = `Subset: ${indices.length.toLocaleString()} molecules`;
  subsetBanner.style.display = 'flex';
  selectedIndices = [];
}

// --------------------------------------------------
// Resize
// --------------------------------------------------

new ResizeObserver(() => {
  const w = canvasWrap.clientWidth, h = canvasWrap.clientHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
  selectOverlay.width  = w;
  selectOverlay.height = h;
}).observe(canvasWrap);

// --------------------------------------------------
// Status — footer bar
// --------------------------------------------------

const statusEl = document.getElementById("footer-status");

function showStatus(msg, isError = false) {
  statusEl.textContent = msg;
  statusEl.style.color  = isError ? "#ff6666" : "#aaa";
}
function hideStatus() { statusEl.textContent = ""; }

// --------------------------------------------------
// Boot
// --------------------------------------------------

updateToolbarActive("depth");
loadGraph();
animate();
