// Knowledge graph renderer: interactive 2D force graph with
// zoom/pan, search, neighbor highlighting, curved links with arrows,
// responsive resize, and type/relationship legends.
(function () {
  const D3_SRC = '/assets/d3.min.js';

  let d3Loading = null;

  function ensureD3() {
    if (window.d3) return Promise.resolve();
    if (d3Loading) return d3Loading;
    d3Loading = new Promise((resolve, reject) => {
      const s = document.createElement('script');
      s.src = D3_SRC;
      s.async = true;
      s.onload = () => resolve();
      s.onerror = () => reject(new Error('Failed to load D3'));
      document.head.appendChild(s);
    });
    return d3Loading;
  }

  // Simple palettes (kept deterministic across renders)
  const PALETTE_A = ['#60A5FA', '#34D399', '#F59E0B', '#A78BFA', '#F472B6', '#F87171', '#22D3EE', '#84CC16', '#FB7185'];
  const PALETTE_B = ['#94A3B8', '#A3A3A3', '#9CA3AF', '#C084FC', '#FDA4AF', '#FCA5A5', '#67E8F9', '#A3E635', '#FDBA74'];

  function buildMap(values) {
    const unique = Array.from(new Set(values.filter(Boolean)));
    const map = new Map();
    unique.forEach((v, i) => map.set(v, PALETTE_A[i % PALETTE_A.length]));
    return map;
  }

  function linkColorMap(values) {
    const unique = Array.from(new Set(values.filter(Boolean)));
    const map = new Map();
    unique.forEach((v, i) => map.set(v, PALETTE_B[i % PALETTE_B.length]));
    return map;
  }

  function radiusForDegree(deg) {
    const d = Math.max(0, +deg || 0);
    const r = 6 + Math.sqrt(d) * 3; // gentle growth
    return Math.max(6, Math.min(r, 24));
  }

  function curvedPath(d) {
    const sx = d.source.x, sy = d.source.y, tx = d.target.x, ty = d.target.y;
    const dx = tx - sx, dy = ty - sy;
    const dr = Math.hypot(dx, dy) * 0.7; // curve radius
    const mx = (sx + tx) / 2;
    const my = (sy + ty) / 2;
    // Offset normal to create a consistent arc
    const nx = -dy / (Math.hypot(dx, dy) || 1);
    const ny = dx / (Math.hypot(dx, dy) || 1);
    const cx = mx + nx * 20;
    const cy = my + ny * 20;
    return `M ${sx},${sy} Q ${cx},${cy} ${tx},${ty}`;
  }

  function buildAdjacency(nodes, links) {
    const idToNode = new Map(nodes.map(n => [n.id, n]));
    const neighbors = new Map();
    nodes.forEach(n => neighbors.set(n.id, new Set()));
    links.forEach(l => {
      const s = typeof l.source === 'object' ? l.source.id : l.source;
      const t = typeof l.target === 'object' ? l.target.id : l.target;
      if (neighbors.has(s)) neighbors.get(s).add(t);
      if (neighbors.has(t)) neighbors.get(t).add(s);
    });
    return { idToNode, neighbors };
  }

  function attachOverlay(container, { onSearch, onToggleNames, onToggleEdgeLabels, onCenter }) {
    const overlay = document.createElement('div');
    overlay.className = 'kg-overlay';

    const primaryRow = document.createElement('div');
    primaryRow.className = 'kg-control-row kg-control-row-primary';

    const secondaryRow = document.createElement('div');
    secondaryRow.className = 'kg-control-row kg-control-row-secondary';

    // search box
    const input = document.createElement('input');
    input.type = 'text';
    input.placeholder = 'Search nodes…';
    input.className = 'nb-input kg-search-input';
    input.addEventListener('keydown', (e) => {
      if (e.key === 'Enter') onSearch && onSearch(input.value.trim());
    });

    const searchBtn = document.createElement('button');
    searchBtn.className = 'nb-btn btn-xs nb-cta kg-search-btn';
    searchBtn.textContent = 'Go';
    searchBtn.addEventListener('click', () => onSearch && onSearch(input.value.trim()));

    const namesToggle = document.createElement('button');
    namesToggle.className = 'nb-btn btn-xs kg-toggle';
    namesToggle.type = 'button';
    namesToggle.textContent = 'Names';
    namesToggle.addEventListener('click', () => onToggleNames && onToggleNames());

    const labelToggle = document.createElement('button');
    labelToggle.className = 'nb-btn btn-xs kg-toggle';
    labelToggle.type = 'button';
    labelToggle.textContent = 'Labels';
    labelToggle.addEventListener('click', () => onToggleEdgeLabels && onToggleEdgeLabels());

    const centerBtn = document.createElement('button');
    centerBtn.className = 'nb-btn btn-xs';
    centerBtn.textContent = 'Center';
    centerBtn.addEventListener('click', () => onCenter && onCenter());

    primaryRow.appendChild(input);
    primaryRow.appendChild(searchBtn);

    secondaryRow.appendChild(namesToggle);
    secondaryRow.appendChild(labelToggle);
    secondaryRow.appendChild(centerBtn);

    overlay.appendChild(primaryRow);
    overlay.appendChild(secondaryRow);

    container.style.position = 'relative';
    container.appendChild(overlay);

    return { input, overlay, namesToggle, labelToggle };
  }

  function attachLegends(container, typeColor, relColor) {
    const wrap = document.createElement('div');
    wrap.className = 'kg-legend';

    function section(title, items) {
      const sec = document.createElement('div');
      sec.className = 'nb-card kg-legend-card';
      const h = document.createElement('div'); h.className = 'kg-legend-heading'; h.textContent = title; sec.appendChild(h);
      items.forEach(([label, color]) => {
        const row = document.createElement('div'); row.className = 'kg-legend-row';
        const sw = document.createElement('span'); sw.style.background = color; sw.style.width = '12px'; sw.style.height = '12px'; sw.style.border = '2px solid #000';
        const t = document.createElement('span'); t.textContent = label || '—';
        row.appendChild(sw); row.appendChild(t); sec.appendChild(row);
      });
      return sec;
    }

    const typeItems = Array.from(typeColor.entries());
    if (typeItems.length) wrap.appendChild(section('Entity Type', typeItems));
    const relItems = Array.from(relColor.entries());
    if (relItems.length) wrap.appendChild(section('Relationship', relItems));

    container.appendChild(wrap);
    return wrap;
  }

  async function renderKnowledgeGraph(root) {
    const container = (root || document).querySelector('#knowledge-graph');
    if (!container) return;

    await ensureD3().catch(() => {
      const err = document.createElement('div');
      err.className = 'alert alert-error';
      err.textContent = 'Unable to load graph library (D3).';
      container.appendChild(err);
    });
    if (!window.d3) return;

    // Clear previous render
    container.innerHTML = '';

    const width = container.clientWidth || 800;
    const height = container.clientHeight || 600;

    const et = container.dataset.entityType || '';
    const cc = container.dataset.contentCategory || '';
    const qs = new URLSearchParams();
    if (et) qs.set('entity_type', et);
    if (cc) qs.set('content_category', cc);

    const url = '/knowledge/graph.json' + (qs.toString() ? ('?' + qs.toString()) : '');
    let data;
    try {
      const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
      if (!res.ok) throw new Error('Failed to load graph data');
      data = await res.json();
    } catch (_e) {
      const err = document.createElement('div');
      err.className = 'alert alert-error';
      err.textContent = 'Unable to load graph data.';
      container.appendChild(err);
      return;
    }

    // Color maps
    const typeColor = buildMap(data.nodes.map(n => n.entity_type));
    const relColor = linkColorMap(data.links.map(l => l.relationship_type));
    const { neighbors } = buildAdjacency(data.nodes, data.links);

    // Build overlay controls
    let namesVisible = true;
    let edgeLabelsVisible = true;

    const togglePressedState = (button, state) => {
      if (!button) return;
      button.setAttribute('aria-pressed', state ? 'true' : 'false');
      button.classList.toggle('kg-toggle-active', !!state);
    };

    const { input, namesToggle, labelToggle } = attachOverlay(container, {
      onSearch: (q) => focusSearch(q),
      onToggleNames: () => {
        namesVisible = !namesVisible;
        label.style('display', namesVisible ? null : 'none');
        togglePressedState(namesToggle, namesVisible);
      },
      onToggleEdgeLabels: () => {
        edgeLabelsVisible = !edgeLabelsVisible;
        linkLabel.style('display', edgeLabelsVisible ? null : 'none');
        togglePressedState(labelToggle, edgeLabelsVisible);
      },
      onCenter: () => zoomTo(1, [width / 2, height / 2])
    });

    togglePressedState(namesToggle, namesVisible);
    togglePressedState(labelToggle, edgeLabelsVisible);

    // SVG + zoom
    const svg = d3.select(container)
      .append('svg')
      .attr('width', '100%')
      .attr('height', height)
      .attr('viewBox', [0, 0, width, height])
      .attr('style', 'cursor: grab; touch-action: none; background: transparent;')
      .call(d3.zoom().scaleExtent([0.25, 5]).on('zoom', (event) => {
        g.attr('transform', event.transform);
      }));

    const g = svg.append('g');

    // Defs for arrows
    const defs = svg.append('defs');
    const markerFor = (key, color) => {
      const id = `arrow-${key.replace(/[^a-z0-9_-]/gi, '_')}`;
      if (!document.getElementById(id)) {
        defs.append('marker')
          .attr('id', id)
          .attr('viewBox', '0 -5 10 10')
          .attr('refX', 16)
          .attr('refY', 0)
          .attr('markerWidth', 6)
          .attr('markerHeight', 6)
          .attr('orient', 'auto')
          .append('path')
          .attr('d', 'M0,-5L10,0L0,5')
          .attr('fill', color);
      }
      return `url(#${id})`;
    };

    // Forces
    const linkForce = d3.forceLink(data.links)
      .id(d => d.id)
      .distance(l => 70)
      .strength(0.5);

    const simulation = d3.forceSimulation(data.nodes)
      .force('link', linkForce)
      .force('charge', d3.forceManyBody().strength(-220))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('collision', d3.forceCollide().radius(d => radiusForDegree(d.degree) + 6))
      .force('y', d3.forceY(height / 2).strength(0.02))
      .force('x', d3.forceX(width / 2).strength(0.02));

    // Links as paths so we can curve + arrow
    const link = g.append('g')
      .attr('fill', 'none')
      .attr('stroke-opacity', 0.7)
      .selectAll('path')
      .data(data.links)
      .join('path')
      .attr('stroke', d => relColor.get(d.relationship_type) || '#CBD5E1')
      .attr('stroke-width', 1.5)
      .attr('marker-end', d => markerFor(d.relationship_type || 'rel', relColor.get(d.relationship_type) || '#CBD5E1'));

    // Optional edge labels (midpoint)
    const linkLabel = g.append('g')
      .selectAll('text')
      .data(data.links)
      .join('text')
      .attr('font-size', 9)
      .attr('fill', '#475569')
      .attr('text-anchor', 'middle')
      .attr('opacity', 0.7)
      .text(d => d.relationship_type || '');

    // Nodes
    const node = g.append('g')
      .attr('stroke', '#fff')
      .attr('stroke-width', 1.5)
      .selectAll('circle')
      .data(data.nodes)
      .join('circle')
      .attr('r', d => radiusForDegree(d.degree))
      .attr('fill', d => typeColor.get(d.entity_type) || '#94A3B8')
      .attr('cursor', 'pointer')
      .on('mouseenter', function (_evt, d) { setHighlight(d); })
      .on('mouseleave', function () { clearHighlight(); })
      .on('click', function (_evt, d) {
        // pin/unpin on click
        if (d.fx == null) { d.fx = d.x; d.fy = d.y; this.setAttribute('data-pinned', 'true'); }
        else { d.fx = null; d.fy = null; this.removeAttribute('data-pinned'); }
      })
      .call(d3.drag()
        .on('start', (event, d) => {
          if (!event.active) simulation.alphaTarget(0.3).restart();
          d.fx = d.x; d.fy = d.y;
        })
        .on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; })
        .on('end', (event, d) => { if (!event.active) simulation.alphaTarget(0); }));

    node.append('title').text(d => `${d.name} • ${d.entity_type} • deg ${d.degree}`);

    // Labels
    const label = g.append('g')
      .selectAll('text')
      .data(data.nodes)
      .join('text')
      .text(d => d.name)
      .attr('font-size', 11)
      .attr('fill', '#111827')
      .attr('stroke', 'white')
      .attr('paint-order', 'stroke')
      .attr('stroke-width', 3)
      .attr('dx', d => radiusForDegree(d.degree) + 6)
      .attr('dy', 4);

    // Legends
    attachLegends(container, typeColor, relColor);

    // Highlight logic
    function setHighlight(n) {
      const ns = neighbors.get(n.id) || new Set();
      node.attr('opacity', d => (d.id === n.id || ns.has(d.id)) ? 1 : 0.15);
      label.attr('opacity', d => (d.id === n.id || ns.has(d.id)) ? 1 : 0.15);
      link
        .attr('stroke-opacity', d => {
          const s = (typeof d.source === 'object') ? d.source.id : d.source;
          const t = (typeof d.target === 'object') ? d.target.id : d.target;
          return (s === n.id || t === n.id || (ns.has(s) && ns.has(t))) ? 0.9 : 0.05;
        })
        .attr('marker-end', d => {
          const c = relColor.get(d.relationship_type) || '#CBD5E1';
          return markerFor(d.relationship_type || 'rel', c);
        });
      linkLabel.attr('opacity', d => {
        const s = (typeof d.source === 'object') ? d.source.id : d.source;
        const t = (typeof d.target === 'object') ? d.target.id : d.target;
        return (s === n.id || t === n.id) ? 0.9 : 0.05;
      });
    }
    function clearHighlight() {
      node.attr('opacity', 1);
      label.attr('opacity', 1);
      link.attr('stroke-opacity', 0.7);
      linkLabel.attr('opacity', 0.7);
    }

    // Search + center helpers
    function centerOnNode(n) {
      const k = 1.5; // zoom factor
      const x = n.x, y = n.y;
      const transform = d3.zoomIdentity.translate(width / 2 - k * x, height / 2 - k * y).scale(k);
      svg.transition().duration(350).call(zoom.transform, transform);
    }
    function focusSearch(query) {
      if (!query) return;
      const q = query.toLowerCase();
      const found = data.nodes.find(n => (n.name || '').toLowerCase().includes(q));
      if (found) { setHighlight(found); centerOnNode(found); }
    }

    // Expose zoom instance
    const zoom = d3.zoom().scaleExtent([0.25, 5]).on('zoom', (event) => g.attr('transform', event.transform));
    svg.call(zoom);
    function zoomTo(k, center) {
      const transform = d3.zoomIdentity.translate(width / 2 - k * center[0], height / 2 - k * center[1]).scale(k);
      svg.transition().duration(250).call(zoom.transform, transform);
    }

    // Tick update
    simulation.on('tick', () => {
      link.attr('d', curvedPath);
      node.attr('cx', d => d.x).attr('cy', d => d.y);
      label.attr('x', d => d.x).attr('y', d => d.y);
      linkLabel.attr('x', d => (d.source.x + d.target.x) / 2).attr('y', d => (d.source.y + d.target.y) / 2);
    });

    // Resize handling
    const ro = new ResizeObserver(() => {
      const w = container.clientWidth || width;
      const h = container.clientHeight || height;
      svg.attr('viewBox', [0, 0, w, h]).attr('height', h);
      simulation.force('center', d3.forceCenter(w / 2, h / 2));
      simulation.alpha(0.3).restart();
    });
    ro.observe(container);
  }

  function tryRender(root) {
    const container = (root || document).querySelector('#knowledge-graph');
    if (container) renderKnowledgeGraph(root);
  }

  // Expose for debugging/manual re-render
  window.renderKnowledgeGraph = () => renderKnowledgeGraph(document);

  // Full page load
  document.addEventListener('DOMContentLoaded', () => tryRender(document));

  // HTMX partial swaps
  document.body.addEventListener('knowledge-graph-refresh', () => {
    tryRender(document);
  });

  document.body.addEventListener('htmx:afterSettle', (evt) => {
    tryRender(evt && evt.target ? evt.target : document);
  });
})();
