// NetworkView.jsx - Collaboration Network (force-directed-ish)
// Nodes = organisations; edges = symmetric collaborations from Collaborations sheet
// Layout: deterministic force simulation (no D3 dep) - runs once on mount

const { useMemo: useMemoN, useState: useStateN, useEffect: useEffectN, useRef: useRefN } = React;

const TYPE_COLORS = {
  'MDB/ IFI / DFI':                                 '#2a6a86', // ocean blue
  'Bilateral development agency':             '#aa6227', // earth clay
  'Intergovernmental organization (IGO)':     '#8c5983', // faded dewberry
  'Coalition or multi-stakeholder platform':  '#2e8055', // verdant green
  'NGO':                                      '#fdce07', // solar yellow
  'Philanthropic foundation':                 '#e07b4a', // warm coral
  'Private company (IPP)':                     '#2a7a5a', // pine green
  'Private investor':                          '#7a5da8', // violet
  'Commercial system actor':                   '#b05c7a', // muted rose
  'Research & Academic institution':          '#8bcdd2', // sky blue
};
window.TYPE_COLORS_NET = TYPE_COLORS;

function NetworkView({ orgs, allOrgs, edges, onPickOrg, highlightOrgId, dataset }) {
  const facets = (dataset || window.INITIATIVES_DATA).facets;
  const [hover, setHover] = useStateN(null);
  const [positions, setPositions] = useStateN(null);
  const [tick, setTick] = useStateN(0);
  // Filter sets - drive node dimming. Empty set = no filter.
  const [filterTypes,    setFilterTypes]    = useStateN(() => new Set());
  const [filterBarriers, setFilterBarriers] = useStateN(() => new Set());

  const W = 1600, H = 1000;

  // Symbolic centre node - "Communities". Not connected; sits at the middle of
  // the canvas with an exclusion radius that pushes all org nodes outward so
  // they circle around it.
  const COMMUNITIES_R    = 110;   // visible radius
  const COMMUNITIES_EXCL = 195;   // exclusion radius for the force simulation

  // Filter edges to currently visible orgs
  const visibleIds = useMemoN(() => new Set(orgs.map(o => o.id)), [orgs]);
  const liveEdges = useMemoN(() => edges.filter(e => visibleIds.has(e.a) && visibleIds.has(e.b)), [edges, visibleIds]);

  // Degree
  const degree = useMemoN(() => {
    const m = new Map();
    for (const o of orgs) m.set(o.id, 0);
    for (const e of liveEdges) {
      m.set(e.a, (m.get(e.a) || 0) + 1);
      m.set(e.b, (m.get(e.b) || 0) + 1);
    }
    return m;
  }, [orgs, liveEdges]);

  // Force simulation - runs once when orgs/edges change
  useEffectN(() => {
    const cx = W / 2, cy = H / 2;
    // Hard clamp - projects a position outside the Communities exclusion zone.
    const clampOutside = (p) => {
      const ex = p.x - cx, ey = p.y - cy;
      const dist = Math.sqrt(ex * ex + ey * ey);
      if (dist < COMMUNITIES_EXCL) {
        const target = COMMUNITIES_EXCL + 8;
        if (dist < 0.5) { p.x = cx + target; p.y = cy; }
        else { const s = target / dist; p.x = cx + ex * s; p.y = cy + ey * s; }
        p.vx = 0; p.vy = 0;
      }
    };

    // Stable seeded init: spread on larger ellipse, with degree influencing radius
    const sorted = [...orgs].sort((a, b) => degree.get(b.id) - degree.get(a.id));
    const n = sorted.length;
    const pos = new Map();
    sorted.forEach((o, i) => {
      const deg = degree.get(o.id);
      // Initial radius: connected hubs inner ring, mid orgs middle, isolates outer.
      // Inner ring sits outside the Communities exclusion zone so nodes start clear of the centre.
      const r = deg === 0 ? 440 : (deg > 8 ? 260 : 360);
      const a = (i / n) * Math.PI * 2 + (deg === 0 ? Math.PI / 5 : 0);
      pos.set(o.id, {
        x: cx + Math.cos(a) * r * 1.5 + (Math.random() - 0.5) * 60,
        y: cy + Math.sin(a) * r + (Math.random() - 0.5) * 60,
        vx: 0, vy: 0,
      });
    });

    // Run sim - tuned for label legibility (more spacing)
    const ITER = 460;
    const REPULSE = 22000;
    const SPRING = 0.04;
    const SPRING_LEN = 230;
    const CENTER_PULL = 0.004;
    const DAMP = 0.80;
    // Elliptical "soft canvas" - nodes clamp to an ellipse that fits the
    // visible area. Removes the rectangular corners that the previous bounds
    // clamp turned into accumulation zones.
    const ELLIPSE_A = W / 2 - 70;
    const ELLIPSE_B = H / 2 - 50;

    const ids = orgs.map(o => o.id);
    for (let it = 0; it < ITER; it++) {
      // Repulsion
      for (let i = 0; i < ids.length; i++) {
        const a = pos.get(ids[i]);
        for (let j = i + 1; j < ids.length; j++) {
          const b = pos.get(ids[j]);
          let dx = a.x - b.x, dy = a.y - b.y;
          let d2 = dx * dx + dy * dy;
          if (d2 < 1) d2 = 1;
          const f = REPULSE / d2;
          const d = Math.sqrt(d2);
          const fx = (dx / d) * f, fy = (dy / d) * f;
          a.vx += fx; a.vy += fy;
          b.vx -= fx; b.vy -= fy;
        }
      }
      // Spring (edges)
      for (const e of liveEdges) {
        const a = pos.get(e.a), b = pos.get(e.b);
        if (!a || !b) continue;
        const dx = b.x - a.x, dy = b.y - a.y;
        const d = Math.sqrt(dx * dx + dy * dy) || 1;
        const f = (d - SPRING_LEN) * SPRING;
        const fx = (dx / d) * f, fy = (dy / d) * f;
        a.vx += fx; a.vy += fy;
        b.vx -= fx; b.vy -= fy;
      }
      // Center pull + damp + integrate
      for (const id of ids) {
        const p = pos.get(id);
        p.vx += (cx - p.x) * CENTER_PULL;
        p.vy += (cy - p.y) * CENTER_PULL;
        // Communities exclusion: push outward if inside the centre zone so org
        // nodes form a ring around the symbolic centre node.
        const ex = p.x - cx, ey = p.y - cy;
        const dist = Math.sqrt(ex * ex + ey * ey) || 1;
        if (dist < COMMUNITIES_EXCL) {
          const push = (COMMUNITIES_EXCL - dist) * 1.2;
          p.vx += (ex / dist) * push;
          p.vy += (ey / dist) * push;
        }
        p.vx *= DAMP; p.vy *= DAMP;
        p.x += p.vx; p.y += p.vy;
        // Elliptical clamp - project nodes back onto the ellipse boundary when
        // they drift outside it. Removes the rectangular corners.
        const nx = (p.x - cx) / ELLIPSE_A;
        const ny = (p.y - cy) / ELLIPSE_B;
        const norm = Math.sqrt(nx * nx + ny * ny);
        if (norm > 1) {
          p.x = cx + (p.x - cx) / norm;
          p.y = cy + (p.y - cy) / norm;
          p.vx *= 0.3; p.vy *= 0.3;
        }
        clampOutside(p);
      }
    }
    // Label de-collision pass: nudge nodes apart vertically if their label boxes overlap
    const arr = [...pos.entries()].map(([id, p]) => ({ id, p }));
    const LABEL_W = 180, LABEL_H = 26;
    const ellipseClamp = (p) => {
      const nx = (p.x - cx) / ELLIPSE_A;
      const ny = (p.y - cy) / ELLIPSE_B;
      const norm = Math.sqrt(nx * nx + ny * ny);
      if (norm > 1) { p.x = cx + (p.x - cx) / norm; p.y = cy + (p.y - cy) / norm; }
    };
    for (let pass = 0; pass < 40; pass++) {
      let moved = false;
      for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
          const a = arr[i].p, b = arr[j].p;
          const dx = Math.abs(a.x - b.x), dy = Math.abs(a.y - b.y);
          if (dx < LABEL_W && dy < LABEL_H) {
            const push = (LABEL_H - dy) / 2 + 1;
            if (a.y < b.y) { a.y -= push; b.y += push; }
            else { a.y += push; b.y -= push; }
            ellipseClamp(a); ellipseClamp(b);
            moved = true;
          }
        }
      }
      if (!moved) break;
    }
    for (const id of ids) clampOutside(pos.get(id));
    setPositions(pos);
    setTick(t => t + 1);
  }, [orgs, liveEdges]);

  if (!positions) return <div style={{ padding: 40, color: 'var(--text-mid)' }}>Computing layout...</div>;

  const activeId = hover || highlightOrgId;
  const focusOrg = activeId ? orgs.find(o => o.id === activeId) : null;
  const focusNeighbors = new Set();
  if (focusOrg) {
    focusNeighbors.add(focusOrg.id);
    for (const e of liveEdges) {
      if (e.a === focusOrg.id) focusNeighbors.add(e.b);
      if (e.b === focusOrg.id) focusNeighbors.add(e.a);
    }
  }


  const anyFilterActive = filterTypes.size + filterBarriers.size > 0;
  const matchesFilters = (o) => {
    if (filterTypes.size && !filterTypes.has(o.type)) return false;
    if (filterBarriers.size) {
      const bs = o.barriers || [];
      let any = false;
      for (const b of bs) if (filterBarriers.has(b)) { any = true; break; }
      if (!any) return false;
    }
    return true;
  };

  const toggleIn = (set, setter) => (v) => {
    const next = new Set(set);
    if (next.has(v)) next.delete(v); else next.add(v);
    setter(next);
  };
  const toggleType    = toggleIn(filterTypes,    setFilterTypes);
  const toggleBarrier = toggleIn(filterBarriers, setFilterBarriers);

  // Barriers present in the visible orgs - only show chips that can match.
  const barriersPresent = (() => {
    const s = new Set();
    for (const o of orgs) for (const b of (o.barriers || [])) s.add(b);
    return [...s].sort();
  })();

  return (
    <div>
      {/* Filter legend - clickable chips for type / barrier / value-chain part */}
      <div className="net-filters">
        <div className="net-filter-row">
          <span className="net-filter-label">Type</span>
          {facets.types.order.map(t => {
            const on = filterTypes.has(t);
            const off = filterTypes.size > 0 && !on;
            return (
              <button
                key={t}
                type="button"
                className={'net-filter-chip is-clickable' + (on ? ' is-on' : '') + (off ? ' is-faded' : '')}
                onClick={() => toggleType(t)}
                title={on ? `Remove ${t} filter` : `Filter to ${t}`}
              >
                <span className="net-legend-dot" style={{ background: TYPE_COLORS[t] }}></span>
                {facets.types.labels[t]}
              </button>
            );
          })}
        </div>
        {barriersPresent.length > 0 && (
          <div className="net-filter-row">
            <span className="net-filter-label">Barrier</span>
            {barriersPresent.map(b => {
              const on = filterBarriers.has(b);
              const off = filterBarriers.size > 0 && !on;
              return (
                <button
                  key={b}
                  type="button"
                  className={'net-filter-chip is-clickable' + (on ? ' is-on' : '') + (off ? ' is-faded' : '')}
                  onClick={() => toggleBarrier(b)}
                  title={on ? `Remove ${b} filter` : `Filter to ${b}`}
                >
                  {b}
                </button>
              );
            })}
          </div>
        )}
        {anyFilterActive && (
          <button
            type="button"
            className="net-filter-clear"
            onClick={() => { setFilterTypes(new Set()); setFilterBarriers(new Set()); }}
          >
            Clear filters
          </button>
        )}
      </div>

      <div className="net-canvas">
        <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet">
          {/* Edges */}
          <g className="net-edges">
            {liveEdges.map((e, i) => {
              const pa = positions.get(e.a), pb = positions.get(e.b);
              if (!pa || !pb) return null;
              const isActive = focusOrg && (e.a === focusOrg.id || e.b === focusOrg.id);
              return (
                <line
                  key={i}
                  x1={pa.x} y1={pa.y} x2={pb.x} y2={pb.y}
                  className={'net-edge' + (isActive ? ' is-active' : '')}
                  strokeWidth={isActive ? 1.6 : 0.8}
                  opacity={focusOrg && !isActive ? 0.05 : undefined}
                />
              );
            })}
          </g>

          {/* Symbolic Communities node - sits above edges so any edges that
              graze the centre are visually covered. Not connected to anything. */}
          <g className="net-communities" transform={`translate(${W / 2},${H / 2})`} aria-label="Communities and local leadership">
            <circle r={COMMUNITIES_R} fill="#2e8055" fillOpacity="0.55" stroke="#84c98f" strokeOpacity="0.85" strokeWidth="3" />
            <text
              textAnchor="middle"
              fontSize="24"
              fontWeight="700"
              fill="#e8f5ee"
              style={{ pointerEvents: 'none', userSelect: 'none', letterSpacing: '0.02em' }}
            >
              <tspan x="0" dy="-10">Communities and</tspan>
              <tspan x="0" dy="28">local leadership</tspan>
            </text>
          </g>

          {/* Nodes */}
          <g className="net-nodes">
            {orgs.map(o => {
              const p = positions.get(o.id);
              if (!p) return null;
              const deg = degree.get(o.id) || 0;
              const r = 5 + Math.sqrt(deg) * 2.8;
              const filterMatch = matchesFilters(o);
              const dim = (focusOrg && !focusNeighbors.has(o.id)) || (anyFilterActive && !filterMatch);
              const color = TYPE_COLORS[o.type] || '#84c98f';
              const isFocus = focusNeighbors.has(o.id) || activeId === o.id;
              // Show all labels by default; emphasise on focus
              const showLabel = true;
              const labelText = o.name.length > 30 ? o.name.slice(0, 28) + '...' : o.name;
              return (
                <g
                  key={o.id}
                  transform={`translate(${p.x},${p.y})`}
                  className={'net-node' + (activeId === o.id ? ' is-focus' : '')}
                  style={{ opacity: dim ? 0.18 : 1, cursor: 'pointer' }}
                  onMouseEnter={() => setHover(o.id)}
                  onMouseLeave={() => setHover(null)}
                  onClick={() => onPickOrg(o)}
                >
                  <circle className="halo" r={r + 7} />
                  <circle r={r} fill={color} stroke={color} strokeWidth="1.2" fillOpacity="0.92" />
                  {showLabel && (
                    <text
                      className={'net-label' + (isFocus ? ' is-focus' : '')}
                      y={-(r + 7)}
                      textAnchor="middle"
                    >
                      {labelText}
                    </text>
                  )}
                </g>
              );
            })}
          </g>
        </svg>
      </div>

      <p className="net-foot">
        Hover a node to highlight its partners. Click for full details. Edges are symmetric - each pair appears once.
      </p>
    </div>
  );
}
window.NetworkView = NetworkView;
