{% extends 'base.html' %}
{% load static %}
{% block title %}聚类概览 — 运行 #{{ run.display_id }}{% endblock %}
{% block content %}
<style>
/* ── 布局 ── */
.main-layout { display:flex; gap:0; transition:all 0.3s; position:relative; }
.left-panel { flex:1; min-width:0; transition:flex 0.3s; }
.left-panel.globe-open { flex:0 0 55%; }
.right-panel { flex:0 0 0; overflow:hidden; transition:flex 0.3s; border-left:1px solid #e0e0e0; }
.right-panel.globe-open { flex:0 0 45%; }
.globe-toggle-handle { position:absolute; z-index:10; top:50%; left:calc(100% - 24px); transform:translateY(-50%); width:24px; height:48px; background:#1a1a2e; color:#fff; border:none; border-radius:6px 0 0 6px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:14px; transition:left 0.3s, background 0.15s; }
.globe-toggle-handle:hover { background:#4361ee; }
.globe-toggle-handle.open { left:calc(55% - 24px); border-radius:6px 0 0 6px; }
/* ── Scatter ── */
.scatter-wrapper { position:relative; cursor:crosshair; }
.scatter-wrapper canvas { width:100%; height:400px; display:block; }
#scatter3D { width:100%; height:450px; }
/* ── Detail Panel ── */
.detail-overlay { position:fixed; bottom:0; left:0; right:0; z-index:100; transform:translateY(100%); transition:transform 0.35s cubic-bezier(.4,0,.2,1); }
.detail-overlay.open { transform:translateY(0); }
.detail-content { background:#fff; border-radius:16px 16px 0 0; box-shadow:0 -4px 24px rgba(0,0,0,0.15); max-height:45vh; overflow-y:auto; padding:1rem 2rem 2rem; }
.detail-handle { width:40px; height:4px; background:#ccc; border-radius:2px; margin:0.5rem auto; cursor:pointer; }
.detail-handle:hover { background:#999; }
.detail-close { float:right; cursor:pointer; font-size:1.5rem; color:#999; line-height:1; }
.detail-close:hover { color:#333; }
/* ── Globe ── */
#globeContainer { width:100%; height:calc(100vh - 120px); position:sticky; top:0; }
/* ── Legend pills ── */
.legend-pills { display:flex; flex-wrap:wrap; gap:4px 8px; padding:6px 0; }
.pill { display:inline-flex; align-items:center; gap:4px; padding:2px 10px; border-radius:12px; font-size:0.75rem; cursor:pointer; border:2px solid transparent; transition:all 0.15s; white-space:nowrap; }
.pill:hover { opacity:0.8; }
.pill.active { border-color:#333; font-weight:600; }
.cluster-card { border:1px solid #e0e0e0; border-radius:8px; padding:1rem; background:#fff; cursor:pointer; transition:box-shadow 0.15s; }
.cluster-card:hover { box-shadow:0 2px 8px rgba(0,0,0,0.1); }
.cluster-card.selected { border-color:#4361ee; box-shadow:0 0 0 2px #4361ee33; }
.nl-summary { background:#f8f9fa; border-left:3px solid #4361ee; padding:0.6rem; margin:0.5rem 0; border-radius:0 4px 4px 0; font-size:0.85rem; line-height:1.6; color:#333; }
.feat-positive { color:#2e7d32; }
.feat-negative { color:#c62828; }
</style>
<script src="{% static 'tianxuan/three.min.js' %}"></script>

<div class="card">
    <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
        <div>
            <h2 style="margin:0;">聚类概览</h2>
            <p style="margin:0.25rem 0 0;color:#666;font-size:0.9rem;">
                运行 #{{ run.display_id }} — {{ run.total_flows }} 条流, {{ run.cluster_count }} 个簇
                {% if noise %}, {{ noise.size }} 个噪声点{% endif %}
            </p>
        </div>
        <div style="display:flex;gap:0.5rem;">
            <a href="{% url 'analysis:run_detail' run.display_id %}" class="btn btn-sm">← 运行详情</a>
        </div>
    </div>
</div>

<div class="main-layout">
<div class="left-panel" id="leftPanel">

<!-- ── UMAP 2D / 3D ── -->
<div class="card">
    <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
        <h3 style="margin:0;">UMAP 嵌入 <span style="font-weight:400;font-size:0.8rem;color:#888;" id="pointCount">计算中...</span></h3>
        <span id="viewToggle" style="display:none;">
            <button id="btn2D" class="btn btn-sm" style="font-weight:bold;padding:2px 10px;">2D</button>
            <button id="btn3D" class="btn btn-sm btn-outline" style="padding:2px 10px;">3D</button>
        </span>
    </div>
    <div class="scatter-wrapper" style="margin-top:0.5rem;">
        <canvas id="scatterChart" style="width:100%;height:400px;"></canvas>
        <div id="scatterLegend" class="legend-pills"></div>
    </div>
    <div id="scatter3DContainer" style="display:none;">
        <div id="scatter3D"></div>
        <div id="scatter3DLegend" class="legend-pills"></div>
    </div>
</div>

<!-- ── Noise Analysis ── -->
{% if noise %}
<div class="card" style="background:#fff8e1;border-color:#ffe082;">
    <div style="display:flex;justify-content:space-between;align-items:center;">
        <h3>⚠️ 噪声点 <span style="font-weight:400;font-size:0.8rem;">{{ noise.size }} 个实体</span></h3>
        <a href="{% url 'analysis:cluster_detail' run.display_id -1 %}" class="btn btn-sm">详情 →</a>
    </div>
    {% if noise_summary %}<div class="nl-summary" style="border-left-color:#e65100;background:#fff3e0;">{{ noise_summary }}</div>{% endif %}
</div>
{% endif %}

<!-- ── Cluster Grid ── -->
<div class="legend-pills" style="margin-bottom:0.5rem;">
    <span class="pill" style="background:#e8e8e8;" onclick="clearClusterFilter()">全部</span>
    {% for c in clusters %}
    <span class="pill" id="pill-{{ c.cluster_label }}" data-label="{{ c.cluster_label }}" onclick="selectCluster({{ c.cluster_label }})">#{{ c.cluster_label }} ({{ c.size }})</span>
    {% endfor %}
    {% if noise %}<span class="pill" style="background:#999;color:#fff;" onclick="selectCluster(-1)">Noise ({{ noise.size }})</span>{% endif %}
</div>

<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.75rem;">
{% for c in clusters %}
<div class="cluster-card" data-label="{{ c.cluster_label }}" onclick="selectCluster({{ c.cluster_label }})">
    <div style="font-weight:700;font-size:1rem;">簇 #{{ c.cluster_label }}</div>
    <div style="font-size:0.8rem;color:#666;margin-top:0.2rem;">{{ c.size }} 个实体{% if c.proportion %} · {{ c.proportion|floatformat:1 }}%{% endif %}</div>
    {% if c.nl_summary %}<div class="nl-summary">{{ c.nl_summary }}</div>{% endif %}
</div>
{% empty %}
<div class="card"><div class="empty-state"><p>No clusters</p></div></div>
{% endfor %}
</div>

{% if geo_count > 0 %}
<div class="card" style="margin-top:1rem;">
    <h3>🌍 地理分布</h3>
    <div class="scatter-wrapper"><canvas id="geoChart" style="width:100%;height:300px;"></canvas><div id="geoLegend" class="legend-pills"></div></div>
</div>
{% endif %}

</div><!-- /left-panel -->

<!-- ── Right Panel: Globe ── -->
<div class="right-panel" id="rightPanel">
    <iframe id="globeIframe" src="/globe/?embed=1&data={{ selected_ds_id|default:'' }}" style="width:100%;height:100%;border:none;"></iframe>
</div>
<button class="globe-toggle-handle" id="globeHandle" onclick="toggleGlobe()">◀</button>
</div><!-- /main-layout -->

<!-- ── Bottom Detail Panel ── -->
<div class="detail-overlay" id="detailPanel">
    <div class="detail-content">
        <div class="detail-handle" onclick="closeDetail()"></div>
        <span class="detail-close" onclick="closeDetail()">✕</span>
        <div id="detailBody"></div>
    </div>
</div>

<script>
const scatterData = {{ scatter_data_json|safe }};
const scatterCount = scatterData.length;
document.getElementById('pointCount').textContent = scatterCount + ' 个点';
const hasZ = {{ has_z|yesno:"true,false" }};
const colors = ['#4361ee','#f72585','#7209b7','#4cc9f0','#f8961e','#43aa8b','#f94144','#577590','#e9c46a','#9b5de5',
    '#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf',
    '#aec7e8','#ffbb78','#98df8a','#ff9896','#c5b0d5','#c49c94','#f7b6d2','#c7c7c7','#dbdb8d','#9edae5',
    '#393b79','#637939','#8c6d31','#843c39','#7b4173','#5254a3','#8ca252','#bd9e39','#ad494a','#a55194'];

let selectedCluster = null, selectedPoint = null;
let globeOpen = false;
const clusterSizes = {{ cluster_sizes_json|safe }};

// Color pills after DOM ready
(function() {
    clusterSizes.forEach(function(c) {
        var el = document.getElementById('pill-' + c.label);
        if (el) { el.style.background = colorFor(c.label); el.style.color = '#fff'; }
    });
    var noisePill = document.querySelector('.pill[data-label="-1"]');
    if (noisePill) { noisePill.style.background = '#999'; noisePill.style.color = '#fff'; }
})();

function colorFor(label) { return label === -1 ? 'rgba(150,150,150,0.8)' : colors[Math.abs(label) % colors.length]; }

// ══════════════════════════════════════════════════════════
//  SCATTER — 2D Canvas with click interaction
// ══════════════════════════════════════════════════════════

function drawScatter(canvasId, data, xLabel, yLabel, highlightLabel) {
    const canvas = document.getElementById(canvasId);
    if (!canvas || data.length === 0) return;
    const rect = canvas.parentElement.getBoundingClientRect();
    const W = rect.width || 600, H = 400;
    canvas.width = W; canvas.height = H;
    const ctx = canvas.getContext('2d');
    const pad = { top: 30, bottom: 45, left: 60, right: 30 };
    const pw = W - pad.left - pad.right, ph = H - pad.top - pad.bottom;
    const xs = data.map(d => d.x), ys = data.map(d => d.y);
    const xMin = Math.min(...xs), xMax = Math.max(...xs);
    const yMin = Math.min(...ys), yMax = Math.max(...ys);
    const xR = xMax - xMin || 1, yR = yMax - yMin || 1;
    const toX = v => pad.left + (v - xMin) / xR * pw;
    const toY = v => pad.top + ph - (v - yMin) / yR * ph;

    ctx.clearRect(0, 0, W, H);
    ctx.strokeStyle = '#f0f0f0'; ctx.lineWidth = 1;
    for (let i = 0; i <= 5; i++) { const y = pad.top + i * ph / 5; ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke(); }
    ctx.fillStyle = '#999'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center';
    ctx.fillText(xLabel, W / 2, H - 3);
    ctx.save(); ctx.translate(16, H / 2); ctx.rotate(-Math.PI / 2); ctx.fillText(yLabel, 0, 0); ctx.restore();

    data.forEach(d => {
        const cx = toX(d.x), cy = toY(d.y);
        let color = d.label === -1 ? 'rgba(150,150,150,0.4)' : colors[Math.abs(d.label) % colors.length];
        let radius = d.label === -1 ? 2 : 4;
        if (highlightLabel !== null && highlightLabel !== undefined) {
            if (d.label === highlightLabel) { color = d.label === -1 ? 'rgba(150,150,150,0.9)' : colors[Math.abs(d.label) % colors.length]; radius = 6; }
            else { color = 'rgba(200,200,200,0.2)'; radius = 2; }
        }
        ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2);
        ctx.fillStyle = color; ctx.fill();
    });

    // Click handler
    canvas.onclick = function(e) {
        const rr = canvas.getBoundingClientRect();
        const mx = e.clientX - rr.left, my = e.clientY - rr.top;
        // Find closest point within 8px
        let best = null, bestDist = 8;
        for (let i = 0; i < data.length; i++) {
            const d = data[i];
            const dx = toX(d.x) - mx, dy = toY(d.y) - my;
            const dist = Math.sqrt(dx * dx + dy * dy);
            if (dist < bestDist) { bestDist = dist; best = d; }
        }
        if (best) {
            if (best.label === -1) selectCluster(-1);
            else selectCluster(best.label);
            showEntityDetail(best);
        }
    };

    const legendEl = document.getElementById(canvasId.replace('Chart', 'Legend'));
    if (legendEl) {
        legendEl.innerHTML = '';
        var seen = {};
        data.forEach(d => { if (!seen[d.label]) { seen[d.label] = true; legendEl.innerHTML += '<span class="pill" style="background:' + colorFor(d.label) + ';color:#fff;cursor:pointer;" onclick="selectCluster(' + d.label + ')">' + (d.label === -1 ? 'Noise' : '#' + d.label) + '</span>'; } });
    }
}

function showEntityDetail(point) {
    const body = document.getElementById('detailBody');
    body.innerHTML = '<h3>实体: ' + point.entity + '</h3>' +
        '<p>所属簇: ' + (point.label === -1 ? '噪声' : '#' + point.label) + '</p>' +
        '<p>UMAP 坐标: (' + point.x.toFixed(4) + ', ' + point.y.toFixed(4) + ')</p>' +
        '<a href="?entity=' + encodeURIComponent(point.entity) + '" class="btn btn-sm btn-primary">查看画像</a>';
    document.getElementById('detailPanel').classList.add('open');
}

// ══════════════════════════════════════════════════════════
//  CLUSTER SELECTION
// ══════════════════════════════════════════════════════════

function selectCluster(label) {
    selectedCluster = label;
    selectedPoint = null;
    // Highlight cards
    document.querySelectorAll('.cluster-card').forEach(el => el.classList.toggle('selected', parseInt(el.dataset.label) === label));
    // Highlight scatter
    drawScatter('scatterChart', scatterData, 'UMAP-1', 'UMAP-2', label);
    // Focus legend pills
    document.querySelectorAll('.pill').forEach(el => el.classList.toggle('active', parseInt(el.dataset.label) === label));
    // Show cluster detail in bottom panel
    const clusterInfo = clusterSizes.find(c => c.label === label);
    if (clusterInfo) {
        const body = document.getElementById('detailBody');
        body.innerHTML = '<h3>簇 #' + label + '</h3>' +
            '<p>大小: ' + clusterInfo.size + ' | 轮廓系数: ' + (clusterInfo.silhouette != null ? clusterInfo.silhouette.toFixed(4) : '-') + '</p>' +
            '<a href="/clusters/{{ run.display_id }}/' + label + '/" class="btn btn-sm btn-primary">详细分析 →</a>';
        document.getElementById('detailPanel').classList.add('open');
    }
    // Gray out globe if open
    if (globeOpen) {
        var ifr = document.getElementById('globeIframe');
        if (ifr && ifr.contentWindow) ifr.contentWindow.postMessage({ type: 'globe_filter', cluster_label: label }, '*');
    }
}

function clearClusterFilter() {
    selectedCluster = null;
    drawScatter('scatterChart', scatterData, 'UMAP-1', 'UMAP-2', null);
    document.querySelectorAll('.cluster-card').forEach(el => el.classList.remove('selected'));
    document.querySelectorAll('.pill').forEach(el => el.classList.remove('active'));
    document.getElementById('detailPanel').classList.remove('open');
    if (globeOpen) {
        var ifr = document.getElementById('globeIframe');
        if (ifr && ifr.contentWindow) ifr.contentWindow.postMessage({ type: 'globe_filter', cluster_label: null }, '*');
    }
}

function closeDetail() { document.getElementById('detailPanel').classList.remove('open'); }

function toggleGlobe() {
    globeOpen = !globeOpen;
    document.getElementById('leftPanel').classList.toggle('globe-open', globeOpen);
    document.getElementById('rightPanel').classList.toggle('globe-open', globeOpen);
    var handle = document.getElementById('globeHandle');
    handle.innerHTML = globeOpen ? '▶' : '◀';
    handle.classList.toggle('open', globeOpen);
}

// ══════════════════════════════════════════════════════════
//  3D Three.js SCATTER (for 3D UMAP toggle)
// ══════════════════════════════════════════════════════════

var scatter3DRenderer = null, scatter3DScene = null, scatter3DCamera = null, scatter3DPoints = null;
var scatter3DInitialized = false, is3DMode = false, scatterData3D = scatterData;

function hasWebGL2() { try { var c = document.createElement('canvas'); return !!c.getContext('webgl2'); } catch(e) { return false; } }

(function() {
    var supports3D = hasWebGL2();
    if (supports3D && hasZ) document.getElementById('viewToggle').style.display = 'inline';

    var btn2D = document.getElementById('btn2D'), btn3D = document.getElementById('btn3D');
    var c2d = document.querySelector('#scatterChart').parentElement;
    var c3d = document.getElementById('scatter3DContainer');

    function show2D() {
        is3DMode = false; c2d.style.display = ''; c3d.style.display = 'none';
        btn2D.style.fontWeight = 'bold'; btn2D.className = 'btn btn-sm';
        btn3D.style.fontWeight = 'normal'; btn3D.className = 'btn btn-sm btn-outline';
        drawScatter('scatterChart', scatterData, 'UMAP-1', 'UMAP-2', selectedCluster);
    }
    function show3D() {
        is3DMode = true; c2d.style.display = 'none'; c3d.style.display = '';
        btn3D.style.fontWeight = 'bold'; btn3D.className = 'btn btn-sm';
        btn2D.style.fontWeight = 'normal'; btn2D.className = 'btn btn-sm btn-outline';
        if (!scatter3DInitialized) initScatter3D();
    }
    btn2D.addEventListener('click', show2D);
    btn3D.addEventListener('click', show3D);
    window.addEventListener('resize', function() {
        if (!scatter3DRenderer || !is3DMode) return;
        var cont = document.getElementById('scatter3D');
        if (!cont) return;
        var W = cont.clientWidth || 600;
        scatter3DRenderer.setSize(W, 450);
        scatter3DCamera.aspect = W / 450;
        scatter3DCamera.updateProjectionMatrix();
    });
})();

function initScatter3D() {
    if (scatter3DInitialized) return;
    var container = document.getElementById('scatter3D');
    if (!container || scatterData.length === 0) return;
    var W = container.clientWidth || 600, H = 450;
    var xs = scatterData.map(d => d.x), ys = scatterData.map(d => d.y), zs = scatterData.map(d => d.z || 0);
    var xMin = Math.min(...xs), xMax = Math.max(...xs), yMin = Math.min(...ys), yMax = Math.max(...ys), zMin = Math.min(...zs), zMax = Math.max(...zs);
    var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cz = (zMin + zMax) / 2;
    var range = Math.max(xMax - xMin, yMax - yMin, zMax - zMin) || 1;
    function norm(v, c, r) { return ((v - c) / r) * 3; }

    var scene = new THREE.Scene(); scene.background = new THREE.Color(0xf8f9fa);
    var camera = new THREE.PerspectiveCamera(50, W / H, 0.1, 50); camera.position.set(4.5, 3.5, 6); camera.lookAt(0, 0, 0);
    var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(W, H); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    container.appendChild(renderer.domElement);
    scatter3DRenderer = renderer; scatter3DScene = scene; scatter3DCamera = camera;

    var positions = new Float32Array(scatterData.length * 3);
    var pointColors = new Float32Array(scatterData.length * 3);
    for (var i = 0; i < scatterData.length; i++) {
        var d = scatterData[i];
        positions[i*3] = norm(d.x, cx, range); positions[i*3+1] = norm(d.y, cy, range); positions[i*3+2] = d.z != null ? norm(d.z, cz, range) : 0;
        var c3 = new THREE.Color(d.label === -1 ? '#999999' : colors[Math.abs(d.label) % colors.length]);
        pointColors[i*3] = c3.r; pointColors[i*3+1] = c3.g; pointColors[i*3+2] = c3.b;
    }
    var geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geo.setAttribute('color', new THREE.BufferAttribute(pointColors, 3));
    var mat = new THREE.PointsMaterial({ size: 0.15, vertexColors: true, sizeAttenuation: true, depthWrite: true, transparent: true, opacity: 0.85 });
    var points = new THREE.Points(geo, mat);
    scene.add(points);
    scatter3DPoints = points;

    var isDragging = false, prevMouse = { x: 0, y: 0 };
    renderer.domElement.addEventListener('mousedown', function(e) { isDragging = true; prevMouse = { x: e.clientX, y: e.clientY }; });
    window.addEventListener('mouseup', function() { isDragging = false; });
    window.addEventListener('mousemove', function(e) {
        if (!isDragging || !is3DMode) return;
        var dx = e.clientX - prevMouse.x, dy = e.clientY - prevMouse.y;
        points.rotation.y += dx * 0.005; points.rotation.x += dy * 0.005;
        points.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, points.rotation.x));
        prevMouse = { x: e.clientX, y: e.clientY };
    });
    renderer.domElement.addEventListener('wheel', function(e) { if (!is3DMode) return; e.preventDefault(); camera.position.z += e.deltaY * 0.008; camera.position.z = Math.max(1.5, Math.min(15, camera.position.z)); }, { passive: false });

    function animate() { if (!is3DMode) { requestAnimationFrame(animate); return; } requestAnimationFrame(animate); renderer.render(scene, camera); }
    animate();
    scatter3DInitialized = true;
}

// ══════════════════════════════════════════════════════════
//  GEO SCATTER
// ══════════════════════════════════════════════════════════

{% if geo_count > 0 %}
const geoData = {{ geo_data_json|safe }};
(function() {
    const canvas = document.getElementById('geoChart');
    if (!canvas) return;
    const W = canvas.parentElement.clientWidth || 600, H = 300;
    canvas.width = W; canvas.height = H;
    const ctx = canvas.getContext('2d');
    const pad = { top: 20, bottom: 30, left: 50, right: 20 };
    const pw = W - pad.left - pad.right, ph = H - pad.top - pad.bottom;
    const xs = geoData.map(d => d.x), ys = geoData.map(d => d.y);
    const xMin = Math.min(...xs), xMax = Math.max(...xs), yMin = Math.min(...ys), yMax = Math.max(...ys);
    const toX = v => pad.left + (v - xMin) / (xMax - xMin || 1) * pw;
    const toY = v => pad.top + ph - (v - yMin) / (yMax - yMin || 1) * ph;
    ctx.clearRect(0, 0, W, H);
    geoData.forEach(d => {
        ctx.beginPath(); ctx.arc(toX(d.x), toY(d.y), d.label === -1 ? 2 : 4, 0, Math.PI * 2);
        ctx.fillStyle = colorFor(d.label); ctx.fill();
    });
    var leg = document.getElementById('geoLegend');
    if (leg) { leg.innerHTML = ''; var seen = {}; geoData.forEach(d => { if (!seen[d.label]) { seen[d.label] = true; leg.innerHTML += '<span class="pill" style="background:' + colorFor(d.label) + ';color:#fff;">' + (d.label === -1?'Noise':'#'+d.label) + '</span>'; } }); }
})();
{% endif %}

// Initial draw
drawScatter('scatterChart', scatterData, 'UMAP-1', 'UMAP-2', null);
</script>
{% endblock %}
