/**
 * ClusteringView — Visualisation WebGL des clusters d'IPs via deck.gl
 *
 * Architecture LOD :
 *   - Vue globale : PolygonLayer (hulls) + ScatterplotLayer (centroïdes)
 *   - Sur sélection : ScatterplotLayer dense (toutes les IPs du cluster)
 *   - Sidebar : profil radar, stats, liste IPs paginée
 *
 * Rendu WebGL via @deck.gl/react + OrthographicView
 */

import React, { useState, useEffect, useCallback, useRef } from 'react';
import DeckGL from '@deck.gl/react';
import { OrthographicView } from '@deck.gl/core';
import { ScatterplotLayer, PolygonLayer, TextLayer, LineLayer } from '@deck.gl/layers';
import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, Tooltip } from 'recharts';
import axios from 'axios';

// ─── Types ────────────────────────────────────────────────────────────────────

interface RadarEntry { feature: string; value: number; }

interface ClusterNode {
  id: string;
  cluster_idx: number;
  label: string;
  pca_x: number;
  pca_y: number;
  radius: number;
  color: string;
  risk_score: number;
  ip_count: number;
  hit_count: number;
  mean_ttl: number;
  mean_mss: number;
  mean_score: number;
  mean_velocity: number;
  mean_fuzzing: number;
  mean_headless: number;
  mean_ua_ch: number;
  top_threat: string;
  top_countries: string[];
  top_orgs: string[];
  sample_ips: string[];
  sample_ua: string;
  radar: RadarEntry[];
  hull: [number, number][];
}

interface ClusterEdge {
  id: string;
  source: string;
  target: string;
  similarity: number;
}

interface ClusterStats {
  total_clusters: number;
  total_ips: number;
  total_hits: number;
  bot_ips: number;
  high_risk_ips: number;
  n_samples: number;
  k: number;
  elapsed_s: number;
}

interface ClusterResult {
  status: string;
  nodes: ClusterNode[];
  edges: ClusterEdge[];
  stats: ClusterStats;
  feature_names: string[];
  message?: string;
}

interface IPPoint { ip: string; ja4: string; pca_x: number; pca_y: number; risk: number; }
interface IPDetail { ip: string; ja4: string; tcp_ttl: number; tcp_mss: number; hits: number; ua: string; avg_score: number; threat_level: string; country_code: string; asn_org: string; }

// ─── Coordonnées deck.gl ─────────────────────────────────────────────────────
// PCA normalisé [0,1] → world [0, WORLD]
const WORLD = 1000;

function toWorld(v: number): number { return v * WORLD; }

// Couleur hex → [r,g,b,a]
function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return [r, g, b, alpha];
}

// ─── Composant principal ──────────────────────────────────────────────────────

export default function ClusteringView() {
  const [k, setK] = useState(14);
  const [hours, setHours] = useState(24);
  const [sensitivity, setSensitivity] = useState(1.0);
  const [data, setData] = useState<ClusterResult | null>(null);
  const [loading, setLoading] = useState(false);
  const [computing, setComputing] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [selected, setSelected] = useState<ClusterNode | null>(null);
  const [clusterPoints, setClusterPoints] = useState<IPPoint[]>([]);
  const [ipDetails, setIpDetails] = useState<IPDetail[]>([]);
  const [ipPage, setIpPage] = useState(0);
  const [ipTotal, setIpTotal] = useState(0);
  const [showEdges, setShowEdges] = useState(false);
  const pollRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Viewport deck.gl — centré à [WORLD/2, WORLD/2]
  const [viewState, setViewState] = useState({
    target: [WORLD / 2, WORLD / 2, 0] as [number, number, number],
    zoom: -0.5,          // montre légèrement plus que le monde [0,WORLD]
    minZoom: -3,
    maxZoom: 6,
  });

  // ── Chargement / polling ─────────────────────────────────────────────────

  const fetchClusters = useCallback(async (force = false) => {
    setLoading(true);
    setError(null);
    try {
      const res = await axios.get<ClusterResult>('/api/clustering/clusters', {
        params: { k, hours, sensitivity, force },
      });
      if (res.data.status === 'computing' || res.data.status === 'idle') {
        setComputing(true);
        // Polling
        pollRef.current = setTimeout(() => fetchClusters(), 3000);
      } else {
        setComputing(false);
        setData(res.data);
        // Fit viewport
        if (res.data.nodes?.length) {
          const xs = res.data.nodes.map(n => toWorld(n.pca_x));
          const ys = res.data.nodes.map(n => toWorld(n.pca_y));
          const minX = Math.min(...xs), maxX = Math.max(...xs);
          const minY = Math.min(...ys), maxY = Math.max(...ys);
          const pad = 0.18;  // 18% de marge de chaque côté
          const fitW = (maxX - minX) * (1 + 2 * pad) || WORLD;
          const fitH = (maxY - minY) * (1 + 2 * pad) || WORLD;
          const canvasW = window.innerWidth - 288 - (selected ? 384 : 0); // panel + sidebar
          const canvasH = window.innerHeight - 60;
          setViewState(v => ({
            ...v,
            target: [(minX + maxX) / 2, (minY + maxY) / 2, 0],
            zoom: Math.min(
              Math.log2(canvasW / fitW),
              Math.log2(canvasH / fitH),
            ),
          }));
        }
      }
    } catch (e: unknown) {
      setError((e as Error).message);
      setComputing(false);
    } finally {
      setLoading(false);
    }
  }, [k, hours]);

  useEffect(() => {
    fetchClusters();
    return () => { if (pollRef.current) clearTimeout(pollRef.current); };
  }, []); // eslint-disable-line

  // ── Drill-down : chargement des points du cluster sélectionné ───────────

  const loadClusterPoints = useCallback(async (node: ClusterNode) => {
    try {
      const res = await axios.get<{ points: IPPoint[]; total: number }>(
        `/api/clustering/cluster/${node.id}/points`,
        { params: { limit: 10000, offset: 0 } }
      );
      setClusterPoints(res.data.points);
    } catch { setClusterPoints([]); }
  }, []);

  const loadClusterIPs = useCallback(async (node: ClusterNode, page = 0) => {
    try {
      const res = await axios.get<{ ips: IPDetail[]; total: number }>(
        `/api/clustering/cluster/${node.id}/ips`,
        { params: { limit: 50, offset: page * 50 } }
      );
      setIpDetails(res.data.ips);
      setIpTotal(res.data.total);
      setIpPage(page);
    } catch { setIpDetails([]); }
  }, []);

  const handleSelectCluster = useCallback((node: ClusterNode) => {
    setSelected(node);
    setClusterPoints([]);
    setIpDetails([]);
    loadClusterPoints(node);
    loadClusterIPs(node, 0);
  }, [loadClusterPoints, loadClusterIPs]);

  // ── Layers deck.gl ─────────────────────────────────────────────────────

  const layers = React.useMemo(() => {
    if (!data?.nodes) return [];
    const nodes = data.nodes;
    const nodeMap = Object.fromEntries(nodes.map(n => [n.id, n]));

    const layerList: object[] = [];

    // 1. Hulls (enveloppes convexes) — toujours visibles
    const hullData = nodes
      .filter(n => n.hull && n.hull.length >= 3)
      .map(n => ({
        ...n,
        polygon: n.hull.map(([x, y]) => [toWorld(x), toWorld(y)]),
      }));

    layerList.push(new PolygonLayer({
      id: 'hulls',
      data: hullData,
      getPolygon: (d: typeof hullData[number]) => d.polygon,
      getFillColor: (d: typeof hullData[number]) => hexToRgba(d.color, d.id === selected?.id ? 55 : 28),
      getLineColor: (d: typeof hullData[number]) => hexToRgba(d.color, d.id === selected?.id ? 220 : 130),
      getLineWidth: (d: typeof hullData[number]) => d.id === selected?.id ? 3 : 1.5,
      lineWidthUnits: 'pixels',
      stroked: true,
      filled: true,
      pickable: true,
      autoHighlight: true,
      highlightColor: [255, 255, 255, 30],
      onClick: ({ object }: { object?: typeof hullData[number] }) => {
        if (object) handleSelectCluster(object as ClusterNode);
      },
      updateTriggers: { getFillColor: [selected?.id], getLineColor: [selected?.id], getLineWidth: [selected?.id] },
    }));

    // 2. Arêtes inter-clusters (optionnelles)
    if (showEdges && data.edges) {
      const edgeData = data.edges
        .map(e => {
          const s = nodeMap[e.source];
          const t = nodeMap[e.target];
          if (!s || !t) return null;
          return { source: [toWorld(s.pca_x), toWorld(s.pca_y)], target: [toWorld(t.pca_x), toWorld(t.pca_y)], sim: e.similarity };
        })
        .filter(Boolean) as { source: [number, number]; target: [number, number]; sim: number }[];

      layerList.push(new LineLayer({
        id: 'edges',
        data: edgeData,
        getSourcePosition: d => d.source,
        getTargetPosition: d => d.target,
        getColor: [100, 100, 120, 80],
        getWidth: 1,
        widthUnits: 'pixels',
      }));
    }

    // 3. Points IPs du cluster sélectionné
    if (selected && clusterPoints.length > 0) {
      layerList.push(new ScatterplotLayer({
        id: 'ip-points',
        data: clusterPoints,
        getPosition: (d: IPPoint) => [toWorld(d.pca_x), toWorld(d.pca_y), 0],
        getRadius: 3,
        radiusUnits: 'pixels',
        getFillColor: (d: IPPoint) => {
          const r = d.risk;
          if (r > 0.70) return [220, 38, 38, 200];
          if (r > 0.45) return [249, 115, 22, 200];
          if (r > 0.25) return [234, 179, 8, 200];
          return [34, 197, 94, 180];
        },
        pickable: false,
        updateTriggers: { getPosition: [clusterPoints.length] },
      }));
    }

    // 4. Centroïdes (cercles de taille ∝ ip_count)
    layerList.push(new ScatterplotLayer({
      id: 'centroids',
      data: nodes,
      getPosition: (d: ClusterNode) => [toWorld(d.pca_x), toWorld(d.pca_y), 0],
      getRadius: (d: ClusterNode) => d.radius,
      radiusUnits: 'pixels',
      getFillColor: (d: ClusterNode) => hexToRgba(d.color, d.id === selected?.id ? 255 : 180),
      getLineColor: [255, 255, 255, 180],
      getLineWidth: (d: ClusterNode) => d.id === selected?.id ? 3 : 1,
      lineWidthUnits: 'pixels',
      stroked: true,
      filled: true,
      pickable: true,
      autoHighlight: true,
      highlightColor: [255, 255, 255, 60],
      onClick: ({ object }: { object?: ClusterNode }) => {
        if (object) handleSelectCluster(object);
      },
      updateTriggers: { getFillColor: [selected?.id], getLineWidth: [selected?.id] },
    }));

    const stripNonAscii = (s: string) =>
      s.replace(/[\u{0080}-\u{FFFF}]/gu, c => {
        // Translitérations basiques pour la lisibilité
        const map: Record<string, string> = { é:'e',è:'e',ê:'e',ë:'e',à:'a',â:'a',ô:'o',ù:'u',û:'u',î:'i',ï:'i',ç:'c' };
        return map[c] ?? '';
      }).replace(/[\u{1F000}-\u{1FFFF}\u{2600}-\u{27FF}]/gu, '').trim();
    layerList.push(new TextLayer({
      id: 'labels',
      data: nodes,
      getPosition: (d: ClusterNode) => [toWorld(d.pca_x), toWorld(d.pca_y), 0],
      getText: (d: ClusterNode) => stripNonAscii(d.label),
      getSize: 12,
      sizeUnits: 'pixels',
      getColor: [255, 255, 255, 200],
      getAnchor: 'middle',
      getAlignmentBaseline: 'top',
      getPixelOffset: (d: ClusterNode) => [0, d.radius + 4],
      fontFamily: 'monospace',
      background: true,
      getBorderColor: [0, 0, 0, 0],
      backgroundPadding: [3, 1, 3, 1],
      getBackgroundColor: [15, 20, 30, 180],
    }));

    return layerList;
  }, [data, selected, clusterPoints, showEdges, handleSelectCluster]);

  // ── Rendering ────────────────────────────────────────────────────────────

  return (
    <div className="flex h-full overflow-hidden bg-background text-text-primary">
      {/* ── Panneau gauche ── */}
      <div className="flex flex-col w-72 flex-shrink-0 border-r border-gray-700 overflow-y-auto p-4 gap-4 z-10">
        <div>
          <h2 className="text-lg font-bold mb-1">🔬 Clustering IPs</h2>
          <p className="text-xs text-text-secondary">Rendu WebGL · K-means++ sur toutes les IPs</p>
        </div>

        {/* Paramètres */}
        <div className="bg-background-card rounded-lg p-3 space-y-3">
          {/* Sensibilité */}
          <div className="space-y-1">
            <div className="flex justify-between text-xs text-text-secondary">
              <span>Sensibilité</span>
              <span className="font-mono text-white">
                {sensitivity === 0.5 ? 'Grossière' : sensitivity <= 1.0 ? 'Normale' : sensitivity <= 1.5 ? 'Fine' : sensitivity <= 2.0 ? 'Très fine' : 'Maximum'}
                {' '}({Math.round(k * sensitivity)} clusters)
              </span>
            </div>
            <input type="range" min={0.5} max={3.0} step={0.5} value={sensitivity}
              onChange={e => setSensitivity(+e.target.value)}
              className="w-full accent-accent-primary" />
            <div className="flex justify-between text-xs text-text-disabled">
              <span>Grossière</span><span>Maximum</span>
            </div>
          </div>

          {/* k avancé */}
          <details className="text-xs text-text-secondary">
            <summary className="cursor-pointer hover:text-white">Paramètres avancés</summary>
            <div className="mt-2 space-y-2">
              <label className="block">
                Clusters de base (k)
                <input type="range" min={4} max={30} value={k}
                  onChange={e => setK(+e.target.value)}
                  className="w-full mt-1 accent-accent-primary" />
                <span className="font-mono text-white">{k}</span>
              </label>
              <label className="block">
                Fenêtre
                <select value={hours} onChange={e => setHours(+e.target.value)}
                  className="w-full mt-1 bg-background border border-gray-600 rounded px-2 py-1">
                  <option value={6}>6h</option>
                  <option value={12}>12h</option>
                  <option value={24}>24h</option>
                  <option value={48}>48h</option>
                  <option value={168}>7j</option>
                </select>
              </label>
            </div>
          </details>

          <label className="flex items-center gap-2 text-xs text-text-secondary cursor-pointer">
            <input type="checkbox" checked={showEdges} onChange={e => setShowEdges(e.target.checked)}
              className="accent-accent-primary" />
            Afficher les arêtes
          </label>
          <button onClick={() => fetchClusters(true)}
            disabled={loading || computing}
            className="w-full py-2 bg-accent-primary text-white rounded text-sm font-medium hover:opacity-90 disabled:opacity-50">
            {computing ? '⏳ Calcul…' : loading ? '⏳ Chargement…' : '🔄 Recalculer'}
          </button>
        </div>

        {/* Stats globales */}
        {data?.stats && (
          <div className="bg-background-card rounded-lg p-3 space-y-1 text-xs">
            <div className="font-semibold text-sm mb-2">Résultats</div>
            <Stat label="Clusters" value={data.stats.total_clusters} />
            <Stat label="IPs totales" value={data.stats.total_ips.toLocaleString()} />
            <Stat label="IPs bots 🤖" value={data.stats.bot_ips.toLocaleString()} color="text-red-400" />
            <Stat label="Risque élevé" value={data.stats.high_risk_ips.toLocaleString()} color="text-orange-400" />
            <Stat label="Hits totaux" value={data.stats.total_hits.toLocaleString()} />
            <Stat label="Calcul" value={`${data.stats.elapsed_s}s`} />
          </div>
        )}

        {/* Message computing */}
        {computing && (
          <div className="bg-yellow-900/30 border border-yellow-600/40 rounded-lg p-3 text-xs text-yellow-300">
            ⏳ Calcul en cours sur {data?.stats?.n_samples?.toLocaleString() ?? '…'} IPs…
            <br />Mise à jour automatique toutes les 3s
          </div>
        )}

        {error && (
          <div className="bg-red-900/30 border border-red-600/40 rounded p-3 text-xs text-red-300">
            ❌ {error}
          </div>
        )}

        {/* Liste clusters */}
        {data?.nodes && (
          <div className="space-y-1">
            <div className="text-xs text-text-secondary font-semibold uppercase tracking-wide">Clusters</div>
            {[...data.nodes]
              .sort((a, b) => b.risk_score - a.risk_score)
              .map(n => (
                <button key={n.id} onClick={() => handleSelectCluster(n)}
                  className={`w-full text-left px-3 py-2 rounded text-xs flex items-center gap-2 transition-colors
                    ${selected?.id === n.id ? 'bg-accent-primary/20 ring-1 ring-accent-primary' : 'hover:bg-background-secondary'}`}>
                  <span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: n.color }} />
                  <span className="flex-1 truncate">{n.label}</span>
                  <span className="text-text-disabled">{n.ip_count.toLocaleString()}</span>
                </button>
              ))}
          </div>
        )}
      </div>

      {/* ── Canvas WebGL (deck.gl) ── */}
      <div className="flex-1 relative">
        {!data && !loading && !computing && (
          <div className="absolute inset-0 flex items-center justify-center text-text-secondary">
            Cliquez sur <strong className="mx-1 text-white">Recalculer</strong> pour démarrer
          </div>
        )}
        <DeckGL
          views={new OrthographicView({ id: 'ortho', controller: true })}
          viewState={viewState}
          onViewStateChange={({ viewState: vs }) => setViewState(vs as typeof viewState)}
          layers={layers as any}
          style={{ width: '100%', height: '100%' }}
          controller={true}
        >
          {/* Légende overlay */}
          <div style={{ position: 'absolute', bottom: 16, left: 16, pointerEvents: 'none' }}>
            <div className="bg-black/70 rounded-lg p-2 text-xs flex flex-col gap-1">
              {[['#dc2626', 'CRITICAL'], ['#f97316', 'HIGH'], ['#eab308', 'MEDIUM'], ['#22c55e', 'LOW']].map(([c, l]) => (
                <div key={l} className="flex items-center gap-2">
                  <span className="w-3 h-3 rounded-full" style={{ background: c }} />
                  <span className="text-white/80">{l}</span>
                </div>
              ))}
            </div>
          </div>
          {/* Tooltip zoom hint */}
          <div style={{ position: 'absolute', bottom: 16, right: selected ? 320 : 16, pointerEvents: 'none' }}>
            <div className="text-xs text-white/40">Scroll pour zoomer · Drag pour déplacer · Click sur un cluster</div>
          </div>
        </DeckGL>
      </div>

      {/* ── Sidebar droite (sélection) ── */}
      {selected && (
        <ClusterSidebar
          node={selected}
          ipDetails={ipDetails}
          ipTotal={ipTotal}
          ipPage={ipPage}
          clusterPoints={clusterPoints}
          onClose={() => { setSelected(null); setClusterPoints([]); setIpDetails([]); }}
          onPageChange={(p) => loadClusterIPs(selected, p)}
        />
      )}
    </div>
  );
}

// ─── Stat helper ─────────────────────────────────────────────────────────────

function Stat({ label, value, color }: { label: string; value: string | number; color?: string }) {
  return (
    <div className="flex justify-between items-center">
      <span className="text-text-secondary">{label}</span>
      <span className={`font-mono font-semibold ${color ?? 'text-white'}`}>{value}</span>
    </div>
  );
}

// ─── Sidebar détaillée ───────────────────────────────────────────────────────

function ClusterSidebar({ node, ipDetails, ipTotal, ipPage, clusterPoints, onClose, onPageChange }: {
  node: ClusterNode;
  ipDetails: IPDetail[];
  ipTotal: number;
  ipPage: number;
  clusterPoints: IPPoint[];
  onClose: () => void;
  onPageChange: (p: number) => void;
}) {
  const riskLabel = (r: number) =>
    r > 0.70 ? 'CRITICAL' : r > 0.45 ? 'HIGH' : r > 0.25 ? 'MEDIUM' : 'LOW';
  const riskClass = (r: number) =>
    r > 0.70 ? 'text-red-500' : r > 0.45 ? 'text-orange-500' : r > 0.25 ? 'text-yellow-400' : 'text-green-500';

  const totalPages = Math.ceil(ipTotal / 50);

  const exportCSV = () => {
    const header = 'IP,JA4,TTL,MSS,Hits,Score,Menace,Pays,ASN\n';
    const rows = ipDetails.map(ip =>
      [ip.ip, ip.ja4, ip.tcp_ttl, ip.tcp_mss, ip.hits, ip.avg_score, ip.threat_level, ip.country_code, ip.asn_org].join(',')
    ).join('\n');
    const blob = new Blob([header + rows], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a'); a.href = url; a.download = `cluster_${node.id}.csv`; a.click();
  };

  return (
    <div className="w-96 flex-shrink-0 border-l border-gray-700 bg-background-secondary flex flex-col overflow-hidden">
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
        <div>
          <div className="font-bold text-sm">{node.label}</div>
          <div className="text-xs text-text-secondary">{node.ip_count.toLocaleString()} IPs · {node.hit_count.toLocaleString()} hits</div>
        </div>
        <div className="flex items-center gap-2">
          <span className={`text-xs font-bold ${riskClass(node.risk_score)}`}>{riskLabel(node.risk_score)}</span>
          <button onClick={onClose} className="text-text-secondary hover:text-white text-lg leading-none">×</button>
        </div>
      </div>

      <div className="flex-1 overflow-y-auto px-4 py-3 space-y-4">
        {/* Score risque */}
        <div className="bg-background-card rounded-lg p-3">
          <div className="text-xs text-text-secondary mb-2">Score de risque</div>
          <div className="flex items-center gap-3">
            <div className="flex-1 h-3 bg-gray-700 rounded-full overflow-hidden">
              <div className="h-full rounded-full" style={{ width: `${node.risk_score * 100}%`, background: node.color }} />
            </div>
            <span className={`text-sm font-bold ${riskClass(node.risk_score)}`}>
              {(node.risk_score * 100).toFixed(0)}%
            </span>
          </div>
        </div>

        {/* Radar chart */}
        {node.radar?.length > 0 && (
          <div className="bg-background-card rounded-lg p-3">
            <div className="text-xs text-text-secondary mb-2">Profil 21 features</div>
            <ResponsiveContainer width="100%" height={200}>
              <RadarChart data={node.radar}>
                <PolarGrid stroke="#374151" />
                <PolarAngleAxis dataKey="feature" tick={{ fill: '#9ca3af', fontSize: 8 }} />
                <Radar dataKey="value" stroke={node.color} fill={node.color} fillOpacity={0.25} />
                <Tooltip
                  contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8, fontSize: 11 }}
                  formatter={(v: number) => [`${(v * 100).toFixed(1)}%`, '']}
                />
              </RadarChart>
            </ResponsiveContainer>
          </div>
        )}

        {/* TCP stack */}
        <div className="bg-background-card rounded-lg p-3 text-xs space-y-1">
          <div className="font-semibold mb-2">Stack TCP</div>
          <Stat label="TTL moyen" value={node.mean_ttl} />
          <Stat label="MSS moyen" value={node.mean_mss} />
          <Stat label="Score ML" value={`${(node.mean_score * 100).toFixed(1)}%`} />
          <Stat label="Vélocité" value={node.mean_velocity?.toFixed ? `${node.mean_velocity.toFixed(2)} rps` : '-'} />
          <Stat label="Headless" value={node.mean_headless ? `${(node.mean_headless * 100).toFixed(0)}%` : '-'} />
          <Stat label="UA-CH Mismatch" value={node.mean_ua_ch ? `${(node.mean_ua_ch * 100).toFixed(0)}%` : '-'} />
        </div>

        {/* Contexte */}
        {(node.top_countries?.length > 0 || node.top_orgs?.length > 0) && (
          <div className="bg-background-card rounded-lg p-3 text-xs space-y-2">
            <div className="font-semibold">Géographie & AS</div>
            {node.top_countries?.length > 0 && (
              <div className="flex flex-wrap gap-1">
                {node.top_countries.map(c => (
                  <span key={c} className="bg-blue-900/40 border border-blue-700/40 rounded px-2 py-0.5">{c}</span>
                ))}
              </div>
            )}
            {node.top_orgs?.length > 0 && (
              <div className="space-y-1">
                {node.top_orgs.map(o => (
                  <div key={o} className="truncate text-text-secondary">{o}</div>
                ))}
              </div>
            )}
          </div>
        )}

        {/* IPs paginées */}
        <div className="bg-background-card rounded-lg p-3 text-xs">
          <div className="flex items-center justify-between mb-2">
            <span className="font-semibold">IPs ({ipTotal.toLocaleString()})</span>
            <button onClick={exportCSV} className="text-accent-primary hover:underline text-xs">CSV ↓</button>
          </div>
          {ipDetails.length === 0 ? (
            <div className="text-text-disabled">Chargement…</div>
          ) : (
            <div className="space-y-1 max-h-60 overflow-y-auto font-mono">
              {ipDetails.map(ip => (
                <div key={ip.ip + ip.ja4} className="flex items-center gap-2 py-0.5 border-b border-gray-800/50">
                  <span
                    className={`w-2 h-2 rounded-full flex-shrink-0 ${
                      ip.avg_score > 0.45 ? 'bg-red-500' : ip.avg_score > 0.25 ? 'bg-orange-400' : 'bg-green-500'
                    }`}
                  />
                  <a href={`/investigation/ip/${ip.ip}`}
                    className="text-blue-400 hover:underline flex-1 truncate">{ip.ip}</a>
                  <span className="text-text-disabled">{ip.country_code}</span>
                  <span className="text-text-disabled">{ip.hits}</span>
                </div>
              ))}
            </div>
          )}
          {/* Pagination */}
          {totalPages > 1 && (
            <div className="flex items-center justify-between mt-2">
              <button onClick={() => onPageChange(ipPage - 1)} disabled={ipPage === 0}
                className="px-2 py-0.5 bg-gray-700 rounded disabled:opacity-30">←</button>
              <span className="text-text-disabled">{ipPage + 1} / {totalPages}</span>
              <button onClick={() => onPageChange(ipPage + 1)} disabled={ipPage >= totalPages - 1}
                className="px-2 py-0.5 bg-gray-700 rounded disabled:opacity-30">→</button>
            </div>
          )}
        </div>

        {/* Points info */}
        {clusterPoints.length > 0 && (
          <div className="text-xs text-text-secondary text-center pb-2">
            {clusterPoints.length.toLocaleString()} IPs affichées en WebGL
          </div>
        )}
      </div>
    </div>
  );
}
