{% extends "base.html" %}
{% block page_title %}Campagnes — Clusters HDBSCAN{% endblock %}
{% block content %}
<style>
    /* ── Force-graph canvas ── */
    #graph-canvas { width:100%; height:100%; }
    .graph-tooltip {
        position:absolute; background:rgba(17,24,39,0.95); border:1px solid #374151;
        border-radius:8px; padding:8px 12px; font-size:11px; color:#e5e7eb;
        pointer-events:none; z-index:100; max-width:260px; line-height:1.5;
        box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    }
    /* ── Campaign cards ── */
    .camp-card {
        background:#111827; border:1px solid #1f2937; border-radius:10px;
        padding:16px; cursor:pointer; transition:all 0.2s;
    }
    .camp-card:hover { border-color:#6366f1; background:#1e1b4b22; }
    .camp-card.active { border-color:#818cf8; background:#1e1b4b33; box-shadow:0 0 0 1px #818cf8; }
    .camp-chip {
        display:inline-flex; align-items:center; gap:4px;
        padding:2px 8px; border-radius:9999px; font-size:10px; font-weight:500;
    }
    /* ── Detail panel ── */
    .detail-panel { display:none; }
    .detail-panel.open { display:block; }
    /* ── Radar chart container ── */
    .radar-wrap { position:relative; width:100%; max-width:320px; margin:0 auto; }
    /* ── Scatter bubbles ── */
    .scatter-legend { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
    .scatter-legend-item { display:flex; align-items:center; gap:4px; font-size:10px; color:#9ca3af; }
    .scatter-dot { width:10px; height:10px; border-radius:50%; }
</style>

<div class="p-4 lg:p-6 space-y-4 max-w-[1920px] mx-auto">
    <!-- ═══ Header KPIs ═══ -->
    <div class="flex flex-wrap items-center gap-4 mb-2">
        <h1 class="text-xl font-bold text-white flex items-center gap-2">
            <svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
            Campagnes de bots
        </h1>
        <div class="ml-auto flex items-center gap-3">
            <!-- ── Sélecteur de plage temporelle ── -->
            <div class="flex items-center gap-1 bg-gray-800/60 rounded-lg p-1" id="days-selector">
                <button onclick="setDays(1)"  data-days="1"  class="days-btn px-2 py-1 rounded text-xs text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">1j</button>
                <button onclick="setDays(7)"  data-days="7"  class="days-btn px-2 py-1 rounded text-xs text-white bg-purple-600">7j</button>
                <button onclick="setDays(14)" data-days="14" class="days-btn px-2 py-1 rounded text-xs text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">14j</button>
                <button onclick="setDays(30)" data-days="30" class="days-btn px-2 py-1 rounded text-xs text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">30j</button>
                <button onclick="setDays(90)" data-days="90" class="days-btn px-2 py-1 rounded text-xs text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">90j</button>
            </div>
            <div class="text-center px-3">
                <div class="text-2xl font-bold text-purple-400" id="kpi-total">—</div>
                <div class="text-[10px] text-gray-500 uppercase tracking-wider">Campagnes</div>
            </div>
            <div class="text-center px-3 border-l border-gray-700">
                <div class="text-2xl font-bold text-red-400" id="kpi-ips">—</div>
                <div class="text-[10px] text-gray-500 uppercase tracking-wider">IPs impliquées</div>
            </div>
            <div class="text-center px-3 border-l border-gray-700">
                <div class="text-2xl font-bold text-amber-400" id="kpi-detections">—</div>
                <div class="text-[10px] text-gray-500 uppercase tracking-wider">Détections</div>
            </div>
        </div>
    </div>

    <!-- ═══ Doc banner ═══ -->
    <div class="bg-gray-900/50 border border-gray-800 rounded-lg px-4 py-3 text-xs text-gray-400 leading-relaxed">
        <strong class="text-purple-300">Clustering HDBSCAN</strong> — Le bot-detector regroupe les anomalies
        par similarité comportementale dans l'espace latent de l'autoencoder (16 dimensions).
        Chaque <strong>campagne</strong> représente un ensemble d'IPs partageant des patterns
        d'attaque similaires (même JA4, même cadence, mêmes cibles). Les IPs isolées (campaign_id = −1)
        ne sont pas affichées ici.
        <br><strong>Action SOC :</strong> Une campagne multi-IP indique une attaque coordonnée
        (botnet, scraping distribué, credential stuffing). Cliquez sur une campagne pour investiguer.
    </div>

    <!-- ═══ Top row: Scatter + Network graph ═══ -->
    <div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
        <!-- Scatter plot: Score vs Velocity -->
        <div class="section-card">
            <div class="section-header">
                <span class="section-title">
                    <svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><circle cx="5" cy="8" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="7" cy="18" r="2"/><circle cx="18" cy="16" r="2.5"/></svg>
                    Carte des clusters
                    <span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
                        <h4>Scatter — Score vs Vélocité</h4>
                        <p>Chaque bulle = une campagne. Position : score d'anomalie moyen (X) vs vitesse de requêtes moyenne (Y).
                        Taille = nombre d'IPs. Couleur = campagne.</p>
                        <p><strong>Lecture :</strong> Les campagnes proches partagent des comportements similaires.
                        Cliquer sur une bulle ouvre le détail de la campagne.</p>
                        <p class="doc-source">Source : ml_detected_anomalies WHERE campaign_id ≥ 0</p>
                    </div></span>
                </span>
            </div>
            <div class="section-body" style="height:380px">
                <canvas id="scatter-chart"></canvas>
                <div class="scatter-legend" id="scatter-legend"></div>
            </div>
        </div>

        <!-- Network graph -->
        <div class="section-card">
            <div class="section-header">
                <span class="section-title">
                    <svg class="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
                    Graphe de liens
                    <span class="relative inline-block"><button onclick="docToggle(this)" class="doc-btn">ⓘ</button><div class="doc-panel">
                        <h4>Graphe réseau inter-campagnes</h4>
                        <p>Nœuds = IPs anomales, Arêtes = JA4 partagé au sein d'une campagne.
                        Couleur du nœud = campagne. Taille = nombre de hits.</p>
                        <p><strong>Lecture :</strong> Des IPs très connectées au centre du cluster
                        sont le « cœur » de la campagne. Les nœuds isolés en périphérie sont des IPs
                        moins caractéristiques.</p>
                        <p class="doc-source">Source : ml_detected_anomalies JOINs par JA4</p>
                    </div></span>
                </span>
            </div>
            <div class="section-body relative" style="height:380px" id="graph-container">
                <canvas id="graph-canvas"></canvas>
                <div class="graph-tooltip" id="graph-tip" style="display:none"></div>
            </div>
        </div>
    </div>

    <!-- ═══ Campaign cards grid ═══ -->
    <div class="section-card">
        <div class="section-header">
            <span class="section-title">
                Campagnes détectées
                <span class="text-xs text-gray-500 ml-2" id="camp-count"></span>
            </span>
        </div>
        <div class="section-body p-4">
            <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3" id="camp-grid">
                <div class="text-center text-gray-500 text-sm py-8 col-span-full">Chargement des campagnes…</div>
            </div>
        </div>
    </div>

    <!-- ═══ Campaign detail panel (hidden until click) ═══ -->
    <div class="detail-panel" id="detail-panel">
        <div class="section-card border-purple-600/40">
            <div class="section-header">
                <span class="section-title">
                    <svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
                    Détail campagne <span class="font-mono text-purple-300" id="detail-cid"></span>
                </span>
                <button onclick="closeDetail()" class="text-gray-400 hover:text-white transition-colors text-sm">✕ Fermer</button>
                <a id="detail-link" href="/cluster/0" class="px-3 py-1 bg-brand-600 text-white rounded text-xs font-medium hover:bg-brand-500 ml-2">Ouvrir ↗</a>
            </div>
            <div class="section-body p-0">
                <!-- Detail KPIs -->
                <div class="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-0 border-b border-gray-800" id="detail-kpis"></div>

                <!-- Detail content: radar + timeline + members -->
                <div class="grid grid-cols-1 lg:grid-cols-3 gap-0">
                    <!-- Radar chart -->
                    <div class="border-r border-gray-800 p-4">
                        <h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Profil comportemental</h3>
                        <div class="radar-wrap"><canvas id="radar-chart"></canvas></div>
                        <p class="text-[10px] text-gray-600 mt-2 text-center">
                            Radar des features moyennes normalisées de la campagne.
                            Comparez avec le profil d'un trafic normal pour identifier les écarts.
                        </p>
                    </div>
                    <!-- Timeline -->
                    <div class="border-r border-gray-800 p-4">
                        <h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Activité temporelle</h3>
                        <div style="height:220px"><canvas id="timeline-chart"></canvas></div>
                        <p class="text-[10px] text-gray-600 mt-2 text-center">
                            Détections et IPs actives par heure. Des pics synchronisés confirment une coordination.
                        </p>
                    </div>
                    <!-- Shared attributes -->
                    <div class="p-4">
                        <h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Attributs partagés</h3>
                        <div id="detail-attrs" class="space-y-3 text-xs"></div>
                    </div>
                </div>

                <!-- Members table -->
                <div class="border-t border-gray-800">
                    <div class="px-4 py-3 flex items-center justify-between border-b border-gray-800/50">
                        <h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">
                            IPs membres <span class="text-gray-500" id="member-count"></span>
                        </h3>
                    </div>
                    <div class="overflow-x-auto">
                        <table class="w-full text-xs">
                            <thead class="text-[10px] text-gray-500 uppercase bg-gray-900/50">
                                <tr>
                                    <th class="px-3 py-2 text-left">IP</th>
                                    <th class="px-3 py-2 text-left">JA4</th>
                                    <th class="px-3 py-2 text-left">Host</th>
                                    <th class="px-3 py-2 text-right">Score</th>
                                    <th class="px-3 py-2 text-left">Menace</th>
                                    <th class="px-3 py-2 text-right">Hits</th>
                                    <th class="px-3 py-2 text-right">Vélocité</th>
                                    <th class="px-3 py-2 text-left">ASN</th>
                                    <th class="px-3 py-2 text-left">Pays</th>
                                    <th class="px-3 py-2 text-left">Date</th>
                                </tr>
                            </thead>
                            <tbody id="members-body" class="divide-y divide-gray-800/50"></tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Chart.js (already loaded by base, but ensure) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>

<script>
/* ════════════════════════════════════════════════════════════════════════════
 * Utilitaires
 * ════════════════════════════════════════════════════════════════════════════ */
const COLORS = [
    '#818cf8','#f472b6','#34d399','#fbbf24','#60a5fa','#f87171',
    '#a78bfa','#2dd4bf','#fb923c','#e879f9','#4ade80','#38bdf8',
    '#facc15','#c084fc','#22d3ee','#fb7185',
];
function campColor(cid) { return COLORS[Math.abs(cid) % COLORS.length]; }
function escapeHtml(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
function fmtIP(ip) { return String(ip||'').replace('::ffff:',''); }
function fmtScore(s) {
    const v = parseFloat(s)||0;
    const cls = v >= 0.7 ? 'text-red-400' : v >= 0.3 ? 'text-amber-400' : 'text-green-400';
    return `<span class="${cls}">${v.toFixed(4)}</span>`;
}
function threatBadge(t) {
    const m = {CRITICAL:'badge-critical',HIGH:'badge-high',MEDIUM:'badge-medium',
        KNOWN_BOT:'badge-bot',LEGITIMATE_BROWSER:'badge-browser',NORMAL:'badge-normal',ANUBIS_DENY:'badge-deny'};
    return `<span class="badge ${m[t]||'badge-normal'}">${escapeHtml(t||'—')}</span>`;
}
function fmtCountry(cc) {
    if (!cc) return '—';
    const flag = cc.length===2 ? String.fromCodePoint(...[...cc.toUpperCase()].map(c=>0x1F1E6-65+c.charCodeAt(0))) : '';
    return `${flag} ${cc}`;
}

/* ════════════════════════════════════════════════════════════════════════════
 * Data loading
 * ════════════════════════════════════════════════════════════════════════════ */
let _campaigns = [], _scatterData = [], _graphData = {nodes:[],edges:[]};
let _scatterChart = null, _radarChart = null, _timelineChart = null;
let _currentDays = 7;

function setDays(d) {
    _currentDays = d;
    document.querySelectorAll('.days-btn').forEach(b => {
        const active = parseInt(b.dataset.days) === d;
        b.className = `days-btn px-2 py-1 rounded text-xs transition-colors ${active ? 'text-white bg-purple-600' : 'text-gray-400 hover:text-white hover:bg-gray-700'}`;
    });
    closeDetail();
    loadAll();
}

async function loadAll() {
    document.getElementById('camp-count').textContent = '(chargement…)';
    try {
        const [campResp, scatterResp, graphResp] = await Promise.all([
            fetch(`/api/campaigns?days=${_currentDays}`).then(r=>r.json()),
            fetch(`/api/campaigns/scatter?days=${_currentDays}`).then(r=>r.json()),
            fetch(`/api/campaigns/graph?days=${_currentDays}`).then(r=>r.json()),
        ]);
        _campaigns = campResp.campaigns || [];
        _scatterData = scatterResp.data || [];
        _graphData = graphResp;

        // KPIs
        const totalIPs = _campaigns.reduce((s,c) => s + (c.unique_ips||0), 0);
        const totalDet = _campaigns.reduce((s,c) => s + (c.members||0), 0);
        document.getElementById('kpi-total').textContent = _campaigns.length;
        document.getElementById('kpi-ips').textContent = totalIPs.toLocaleString();
        document.getElementById('kpi-detections').textContent = totalDet.toLocaleString();
        const label = _currentDays === 1 ? '24h' : `${_currentDays}j`;
        document.getElementById('camp-count').textContent = `(${_campaigns.length} campagne${_campaigns.length!==1?'s':''} — ${label})`;

        renderCampGrid();
        renderScatter();
        renderGraph();
    } catch(e) {
        console.error('Campaign load error:', e);
        document.getElementById('camp-count').textContent = '(erreur)';
    }
}

/* ════════════════════════════════════════════════════════════════════════════
 * Campaign cards grid
 * ════════════════════════════════════════════════════════════════════════════ */
function renderCampGrid() {
    const el = document.getElementById('camp-grid');
    if (!_campaigns.length) {
        el.innerHTML = '<div class="text-center text-gray-500 text-sm py-8 col-span-full">Aucune campagne active détectée (7 derniers jours)</div>';
        return;
    }
    el.innerHTML = _campaigns.map(c => {
        const color = campColor(c.campaign_id);
        const countries = (c.countries||[]).map(fmtCountry).join(' ');
        const asns = (c.asn_list||[]).map(a => escapeHtml(a)).join(', ') || '—';
        const ja4s = (c.ja4_list||[]).slice(0,3).map(j => `<span class="font-mono text-[10px] text-gray-400">${escapeHtml(j).substring(0,20)}…</span>`).join(' ');
        return `<div class="camp-card" data-cid="${c.campaign_id}" onclick="selectCampaign(${c.campaign_id})">
            <div class="flex items-center gap-2 mb-2">
                <div class="w-3 h-3 rounded-full shrink-0" style="background:${color}"></div>
                <span class="font-semibold text-sm text-white">Campagne #${c.campaign_id}</span>
                <span class="ml-auto camp-chip bg-purple-500/20 text-purple-300">${c.members} dét.</span>
            </div>
            <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-[11px] text-gray-400 mb-2">
                <div>IPs uniques : <span class="text-white">${c.unique_ips}</span></div>
                <div>Score max : <span class="text-red-400">${(c.max_score||0).toFixed(3)}</span></div>
                <div>Vélocité moy. : <span class="text-amber-300">${(c.avg_velocity||0).toFixed(1)} r/s</span></div>
                <div>Fuzzing moy. : <span class="text-amber-300">${(c.avg_fuzzing||0).toFixed(2)}</span></div>
            </div>
            <div class="text-[10px] text-gray-500 mb-1">${countries}</div>
            <div class="text-[10px] text-gray-500 truncate">ASN: ${asns}</div>
            <div class="mt-1">${ja4s}</div>
        </div>`;
    }).join('');
}

/* ════════════════════════════════════════════════════════════════════════════
 * Scatter chart (Chart.js)
 * ════════════════════════════════════════════════════════════════════════════ */
function renderScatter() {
    const canvas = document.getElementById('scatter-chart');
    if (_scatterChart) _scatterChart.destroy();

    // One bubble per campaign (data is already campaign-level aggregates)
    const datasets = _scatterData.map(d => {
        const cid = d.campaign_id;
        const color = campColor(parseInt(cid));
        return {
            label: `Camp. #${cid}`,
            data: [{
                x: parseFloat(d.avg_score)||0,
                y: parseFloat(d.avg_velocity)||0,
                r: Math.max(6, Math.min(30, Math.sqrt(parseInt(d.unique_ips)||1)*3)),
                _meta: d,
            }],
            backgroundColor: color + '88',
            borderColor: color,
            borderWidth: 1.5,
        };
    });

    _scatterChart = new Chart(canvas, {
        type: 'bubble',
        data: { datasets },
        options: {
            responsive: true, maintainAspectRatio: false,
            scales: {
                x: {
                    title: { display:true, text:'Score d\'anomalie moyen', color:'#9ca3af', font:{size:11} },
                    grid: { color:'#1f293766' }, ticks: { color:'#6b7280', font:{size:10} },
                },
                y: {
                    title: { display:true, text:'Vélocité moy. (req/s)', color:'#9ca3af', font:{size:11} },
                    grid: { color:'#1f293766' }, ticks: { color:'#6b7280', font:{size:10} },
                },
            },
            plugins: {
                legend: { display:true, position:'bottom', labels:{color:'#9ca3af',boxWidth:10,font:{size:10}} },
                tooltip: {
                    callbacks: {
                        title: ctx => {
                            const m = ctx[0]?.raw?._meta;
                            return m ? `Campagne #${m.campaign_id}` : '';
                        },
                        label: ctx => {
                            const m = ctx.raw._meta;
                            const lines = [
                                `IPs : ${m.unique_ips || '—'}`,
                                `Score moy. : ${(parseFloat(m.avg_score)||0).toFixed(4)}`,
                                `Vélocité moy. : ${(parseFloat(m.avg_velocity)||0).toFixed(1)} r/s`,
                                `Hits totaux : ${m.total_hits || '—'}`,
                                `Menace : ${m.threat || '—'}`,
                            ];
                            if (m.asn_list?.length) lines.push(`ASN : ${m.asn_list.join(', ')}`);
                            if (m.country_list?.length) lines.push(`Pays : ${m.country_list.join(', ')}`);
                            if (m.ja4_list?.length) lines.push(`JA4 : ${m.ja4_list.map(j=>(j||'').substring(0,20)).join(', ')}`);
                            return lines;
                        }
                    }
                },
            },
            onClick: (e, els) => {
                if (els.length) {
                    const m = els[0].element.$context.raw._meta;
                    selectCampaign(m.campaign_id);
                }
            },
        },
    });
}

/* ════════════════════════════════════════════════════════════════════════════
 * Force-directed graph (Canvas 2D, simple spring layout)
 * ════════════════════════════════════════════════════════════════════════════ */
function renderGraph() {
    const container = document.getElementById('graph-container');
    const canvas = document.getElementById('graph-canvas');
    const tip = document.getElementById('graph-tip');
    const ctx = canvas.getContext('2d');
    const W = container.clientWidth, H = container.clientHeight;
    canvas.width = W * window.devicePixelRatio;
    canvas.height = H * window.devicePixelRatio;
    canvas.style.width = W+'px';
    canvas.style.height = H+'px';
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);

    const nodes = (_graphData.nodes||[]).map((n,i) => ({
        ...n,
        x: W/2 + (Math.random()-0.5)*W*0.6,
        y: H/2 + (Math.random()-0.5)*H*0.6,
        vx:0, vy:0,
        radius: Math.max(4, Math.min(16, Math.sqrt(parseInt(n.total_hits)||1)*0.7)),
        color: campColor(n.group),
    }));
    const nodeMap = {};
    nodes.forEach(n => nodeMap[n.id] = n);
    const edges = (_graphData.edges||[]).filter(e => nodeMap[e.source] && nodeMap[e.target]);

    if (!nodes.length) {
        ctx.fillStyle = '#6b7280';
        ctx.font = '13px sans-serif';
        ctx.textAlign = 'center';
        ctx.fillText('Aucun lien inter-IP à afficher', W/2, H/2);
        return;
    }

    // Simple force simulation (60 iterations)
    for (let iter = 0; iter < 80; iter++) {
        const alpha = 0.3 * (1 - iter/80);
        // Repulsion
        for (let i=0; i<nodes.length; i++) {
            for (let j=i+1; j<nodes.length; j++) {
                let dx = nodes[j].x - nodes[i].x;
                let dy = nodes[j].y - nodes[i].y;
                let d = Math.sqrt(dx*dx+dy*dy) || 1;
                let f = 800 / (d*d) * alpha;
                nodes[i].vx -= dx/d*f; nodes[i].vy -= dy/d*f;
                nodes[j].vx += dx/d*f; nodes[j].vy += dy/d*f;
            }
        }
        // Attraction (edges)
        edges.forEach(e => {
            const a = nodeMap[e.source], b = nodeMap[e.target];
            if (!a || !b) return;
            let dx = b.x-a.x, dy = b.y-a.y;
            let d = Math.sqrt(dx*dx+dy*dy) || 1;
            let f = (d-60)*0.01*alpha;
            a.vx += dx/d*f; a.vy += dy/d*f;
            b.vx -= dx/d*f; b.vy -= dy/d*f;
        });
        // Center gravity
        nodes.forEach(n => {
            n.vx += (W/2-n.x)*0.005*alpha;
            n.vy += (H/2-n.y)*0.005*alpha;
            n.x += n.vx; n.y += n.vy;
            n.vx *= 0.8; n.vy *= 0.8;
            n.x = Math.max(20, Math.min(W-20, n.x));
            n.y = Math.max(20, Math.min(H-20, n.y));
        });
    }

    // Draw
    function draw() {
        ctx.clearRect(0,0,W,H);
        // Edges
        ctx.strokeStyle = '#374151';
        ctx.lineWidth = 0.5;
        edges.forEach(e => {
            const a = nodeMap[e.source], b = nodeMap[e.target];
            if (!a || !b) return;
            ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y); ctx.stroke();
        });
        // Nodes
        nodes.forEach(n => {
            ctx.beginPath();
            ctx.arc(n.x, n.y, n.radius, 0, Math.PI*2);
            ctx.fillStyle = n.color + 'cc';
            ctx.fill();
            ctx.strokeStyle = n.color;
            ctx.lineWidth = 1.5;
            ctx.stroke();
        });
    }
    draw();

    // Hover tooltip
    canvas.addEventListener('mousemove', e => {
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left, my = e.clientY - rect.top;
        const hit = nodes.find(n => Math.hypot(n.x-mx, n.y-my) <= n.radius+2);
        if (hit) {
            tip.style.display = 'block';
            tip.style.left = (e.clientX - container.getBoundingClientRect().left + 12) + 'px';
            tip.style.top = (e.clientY - container.getBoundingClientRect().top - 10) + 'px';
            tip.innerHTML = `
                <div class="font-mono text-purple-300">${fmtIP(hit.id)}</div>
                <div>Campagne #${hit.group}</div>
                <div>JA4: ${escapeHtml((hit.ja4||'').substring(0,25))}</div>
                <div>ASN: ${escapeHtml(hit.asn_org||'—')}</div>
                <div>Pays: ${fmtCountry(hit.country)}</div>
                <div>Hits: ${hit.total_hits}</div>
            `;
        } else {
            tip.style.display = 'none';
        }
    });
    canvas.addEventListener('click', e => {
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left, my = e.clientY - rect.top;
        const hit = nodes.find(n => Math.hypot(n.x-mx, n.y-my) <= n.radius+2);
        if (hit) window.location.href = `/ip/${encodeURIComponent(String(hit.id||'').replace('::ffff:',''))}`;
    });
    canvas.style.cursor = 'default';
    canvas.addEventListener('mousemove', e => {
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left, my = e.clientY - rect.top;
        canvas.style.cursor = nodes.find(n => Math.hypot(n.x-mx,n.y-my)<=n.radius+2) ? 'pointer' : 'default';
    });
}

/* ════════════════════════════════════════════════════════════════════════════
 * Campaign detail panel
 * ════════════════════════════════════════════════════════════════════════════ */
async function selectCampaign(cid) {
    // Highlight card
    document.querySelectorAll('.camp-card').forEach(c => c.classList.toggle('active', c.dataset.cid == cid));

    const panel = document.getElementById('detail-panel');
    panel.classList.add('open');
    document.getElementById('detail-cid').textContent = `#${cid}`;
    document.getElementById('detail-link').href = `/cluster/${cid}`;

    try {
        const resp = await fetch(`/api/campaigns/${cid}?days=${_currentDays}`);
        const data = await resp.json();
        const p = data.profile || {};
        const members = data.members || [];
        const timeline = data.timeline || [];

        // Detail KPIs
        document.getElementById('detail-kpis').innerHTML = [
            ['IPs uniques', p.unique_ips||0, 'text-purple-400'],
            ['JA4 uniques', p.unique_ja4||0, 'text-cyan-400'],
            ['Hosts ciblés', p.unique_hosts||0, 'text-amber-400'],
            ['ASNs', p.unique_asns||0, 'text-blue-400'],
            ['Score moyen', (p.avg_score||0).toFixed(3), 'text-red-400'],
            ['Score max', (p.max_score||0).toFixed(3), 'text-red-300'],
        ].map(([label,val,cls]) => `
            <div class="px-4 py-3 text-center">
                <div class="text-lg font-bold ${cls}">${val}</div>
                <div class="text-[10px] text-gray-500 uppercase">${label}</div>
            </div>
        `).join('');

        // Shared attributes
        const attrsEl = document.getElementById('detail-attrs');
        const ja4List = (p.ja4_list||[]).slice(0,15);
        const asnList = (p.asn_list||[]).slice(0,8);
        const countryList = (p.country_list||[]).slice(0,10);
        const hostList = (p.host_list||[]).slice(0,8);
        attrsEl.innerHTML = `
            <div>
                <div class="text-[10px] text-gray-500 uppercase mb-1">JA4 signatures (${ja4List.length})</div>
                <div class="flex flex-wrap gap-1">${ja4List.map(j=>`<span class="font-mono text-[10px] bg-gray-800 px-1.5 py-0.5 rounded text-cyan-300">${escapeHtml(j).substring(0,25)}…</span>`).join('')}</div>
            </div>
            <div>
                <div class="text-[10px] text-gray-500 uppercase mb-1">ASN organisations</div>
                <div class="flex flex-wrap gap-1">${asnList.map(a=>`<span class="text-[10px] bg-gray-800 px-1.5 py-0.5 rounded text-blue-300">${escapeHtml(a)}</span>`).join('')||'—'}</div>
            </div>
            <div>
                <div class="text-[10px] text-gray-500 uppercase mb-1">Pays</div>
                <div class="flex flex-wrap gap-1">${countryList.map(c=>`<span class="text-[10px] bg-gray-800 px-1.5 py-0.5 rounded">${fmtCountry(c)}</span>`).join('')||'—'}</div>
            </div>
            <div>
                <div class="text-[10px] text-gray-500 uppercase mb-1">Hosts ciblés</div>
                <div class="flex flex-wrap gap-1">${hostList.map(h=>`<span class="text-[10px] bg-gray-800 px-1.5 py-0.5 rounded text-amber-300">${escapeHtml(h)}</span>`).join('')||'—'}</div>
            </div>
        `;

        // Member count
        document.getElementById('member-count').textContent = `(${members.length})`;

        // Members table
        document.getElementById('members-body').innerHTML = members.map(m => {
            const rawIP = String(m.src_ip||'').replace('::ffff:','');
            return `
            <tr class="hover:bg-gray-800/40 cursor-pointer" onclick="window.location='/ip/${encodeURIComponent(rawIP)}'">
                <td class="px-3 py-2 font-mono text-purple-300">${fmtIP(m.src_ip)}</td>
                <td class="px-3 py-2 font-mono text-[10px] text-gray-400 max-w-[140px] truncate">${escapeHtml(m.ja4||'')}</td>
                <td class="px-3 py-2 text-gray-300">${escapeHtml(m.host||'')}</td>
                <td class="px-3 py-2 text-right">${fmtScore(m.anomaly_score)}</td>
                <td class="px-3 py-2">${threatBadge(m.threat_level)}</td>
                <td class="px-3 py-2 text-right text-gray-300">${m.hits||0}</td>
                <td class="px-3 py-2 text-right text-amber-300">${(parseFloat(m.hit_velocity)||0).toFixed(1)}</td>
                <td class="px-3 py-2 text-gray-400 max-w-[120px] truncate">${escapeHtml(m.asn_org||'—')}</td>
                <td class="px-3 py-2">${fmtCountry(m.country_code)}</td>
                <td class="px-3 py-2 text-gray-500 text-[10px]">${m.detected_at||''}</td>
            </tr>
            `;
        }).join('') || '<tr><td colspan="10" class="text-center py-6 text-gray-500">Aucun membre</td></tr>';

        // Radar chart
        renderRadar(p);

        // Timeline chart
        renderTimeline(timeline, cid);

        // Scroll to detail
        panel.scrollIntoView({ behavior:'smooth', block:'start' });

    } catch(e) {
        console.error('Campaign detail error:', e);
    }
}

function closeDetail() {
    document.getElementById('detail-panel').classList.remove('open');
    document.querySelectorAll('.camp-card').forEach(c => c.classList.remove('active'));
}

/* ════════════════════════════════════════════════════════════════════════════
 * Radar chart (behavioral profile)
 * ════════════════════════════════════════════════════════════════════════════ */
function renderRadar(profile) {
    const canvas = document.getElementById('radar-chart');
    if (_radarChart) _radarChart.destroy();

    const labels = ['Vélocité', 'Fuzzing', 'POST ratio', 'Port exhaust.', 'Orphan ratio', 'Score anomalie'];
    const raw = [
        parseFloat(profile.avg_velocity)||0,
        parseFloat(profile.avg_fuzzing)||0,
        parseFloat(profile.avg_post_ratio)||0,
        parseFloat(profile.avg_port_exhaustion)||0,
        parseFloat(profile.avg_orphan)||0,
        Math.abs(parseFloat(profile.avg_score)||0),
    ];
    // Normalize each to [0,1] with reasonable caps
    const caps = [50, 1, 1, 1, 1, 1];
    const normalized = raw.map((v,i) => Math.min(v/caps[i], 1));

    _radarChart = new Chart(canvas, {
        type: 'radar',
        data: {
            labels,
            datasets: [{
                label: 'Campagne',
                data: normalized,
                backgroundColor: 'rgba(129,140,248,0.2)',
                borderColor: '#818cf8',
                borderWidth: 2,
                pointBackgroundColor: '#818cf8',
                pointRadius: 3,
            }],
        },
        options: {
            responsive: true, maintainAspectRatio: true,
            scales: {
                r: {
                    min: 0, max: 1,
                    ticks: { display:false, stepSize:0.25 },
                    grid: { color:'#1f293788' },
                    angleLines: { color:'#1f293788' },
                    pointLabels: { color:'#9ca3af', font:{size:10} },
                },
            },
            plugins: {
                legend: { display:false },
                tooltip: {
                    callbacks: {
                        label: ctx => `${labels[ctx.dataIndex]}: ${raw[ctx.dataIndex].toFixed(3)} (norm: ${normalized[ctx.dataIndex].toFixed(2)})`
                    }
                },
            },
        },
    });
}

/* ════════════════════════════════════════════════════════════════════════════
 * Timeline chart (hourly detections)
 * ════════════════════════════════════════════════════════════════════════════ */
function renderTimeline(timeline, cid) {
    const canvas = document.getElementById('timeline-chart');
    if (_timelineChart) _timelineChart.destroy();

    if (!timeline.length) {
        const ctx2 = canvas.getContext('2d');
        ctx2.clearRect(0,0,canvas.width,canvas.height);
        ctx2.fillStyle = '#6b7280'; ctx2.font = '12px sans-serif'; ctx2.textAlign = 'center';
        ctx2.fillText('Pas de données temporelles', canvas.width/2, canvas.height/2);
        return;
    }

    const labels = timeline.map(t => {
        const d = new Date(t.hour);
        return d.toLocaleDateString('fr-FR',{day:'2-digit',month:'short'}) + ' ' + d.toLocaleTimeString('fr-FR',{hour:'2-digit',minute:'2-digit'});
    });
    const color = campColor(cid);

    _timelineChart = new Chart(canvas, {
        type: 'bar',
        data: {
            labels,
            datasets: [
                {
                    label: 'Détections',
                    data: timeline.map(t=>t.detections),
                    backgroundColor: color + '88',
                    borderColor: color,
                    borderWidth: 1,
                    yAxisID: 'y',
                },
                {
                    label: 'IPs actives',
                    data: timeline.map(t=>t.active_ips),
                    type: 'line',
                    borderColor: '#34d399',
                    backgroundColor: '#34d39922',
                    borderWidth: 2,
                    pointRadius: 2,
                    fill: true,
                    yAxisID: 'y1',
                },
            ],
        },
        options: {
            responsive: true, maintainAspectRatio: false,
            scales: {
                x: { ticks:{color:'#6b7280',font:{size:9},maxRotation:45}, grid:{display:false} },
                y: { position:'left', title:{display:true,text:'Détections',color:'#9ca3af',font:{size:10}},
                     ticks:{color:'#6b7280',font:{size:10}}, grid:{color:'#1f293744'} },
                y1: { position:'right', title:{display:true,text:'IPs',color:'#34d399',font:{size:10}},
                      ticks:{color:'#34d399',font:{size:10}}, grid:{display:false} },
            },
            plugins: {
                legend: { position:'bottom', labels:{color:'#9ca3af',boxWidth:10,font:{size:10}} },
            },
        },
    });
}

/* ════════════════════════════════════════════════════════════════════════════
 * Init
 * ════════════════════════════════════════════════════════════════════════════ */
loadAll();
</script>
{% endblock %}
