/**
 * repo.js — Repo knowledge graph view.
 *
 * Renders a d3 force-layout combining:
 *   - File nodes (source / test / config / other) from /api/knowledge-topology
 *   - Memory nodes linked to those files
 *   - Deterministic structural edges plus separate memory overlay edges
 *
 * Interactions:
 *   - Hover node → highlight 1-hop neighbours, dim others
 *   - Click node → open drawer with detail
 *   - Search input → highlight matching nodes by label
 *   - Build button → POST /api/knowledge-topology/build with live SSE status
 */

import { S }                 from '../state.js';
import { api, post, bustCache } from '../api.js';
import { openDrawer }        from '../widgets/drawer.js';

const $ = id => document.getElementById(id);
const esc = s => s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');

let _sim = null;
let _topo = null;
let _searchTerm = '';
let _buildState = { status: 'idle', message: '', at: 0 };

/* ── Data loader ── */
export async function load() {
    _topo = await api('/api/knowledge-topology', 60_000);
    if (_topo?.error) _topo = null;
    if (_topo?.nodes?.length && !['queued', 'building'].includes(_buildState.status)) {
        _setBuildState('ready', 'Graph ready.');
    }
    _renderMeta();
    _renderGraph();
    _bindControls();
}

export function render() {
    _renderMeta();
    _renderGraph();
}

export function handleBuildEvent(evt) {
    const repoRoot = evt?.payload?.repoRoot;
    const selectedRoot = S.workspace?.repoRoot;
    if (repoRoot && selectedRoot && repoRoot !== selectedRoot) return;

    if (evt?.type === 'graph.cr.build.start') {
        _setBuildState('building', 'Graph build in progress…');
        return;
    }
    if (evt?.type === 'graph.cr.build.complete') {
        _setBuildState('ready', 'Graph ready.');
        bustCache('/api/knowledge-topology');
        void load();
        return;
    }
    if (evt?.type === 'graph.cr.build.failed') {
        _setBuildState('failed', 'Graph build failed. Check backend logs and retry.');
    }
}

/* ── Meta header ── */
function _renderMeta() {
    const el = $('repo-graph-meta'); if (!el) return;
    if (!_topo) {
        el.textContent = _buildState.message || 'No topology data';
        _syncBuildControls();
        return;
    }
    const meta = _topo.meta || {};
    const provenance = _topo.provenance || {};
    const generatedAt = _topo.generatedAt
        ? new Date(_topo.generatedAt).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' })
        : '—';
    const buildLabel = _buildState.status === 'idle'
        ? 'idle'
        : `${_buildState.status}${_buildState.message ? ` · ${_buildState.message}` : ''}`;
    el.innerHTML = [
        `<span>${esc(_topo.repoName || 'workspace')}</span>`,
        `<span>${fmtNum(meta.fileCount ?? 0)} files</span>`,
        meta.moduleCount ? `<span>${fmtNum(meta.moduleCount)} modules</span>` : null,
        meta.memoryCount ? `<span>${fmtNum(meta.memoryCount)} memories</span>` : null,
        meta.conceptCount ? `<span>${fmtNum(meta.conceptCount)} concepts</span>` : null,
        `<span>${fmtNum(meta.edgeCount ?? 0)} edges</span>`,
        `<span>${esc(provenance.graphSource || meta.graphSource || 'unknown-source')}</span>`,
        `<span>${esc(buildLabel)}</span>`,
        `<span>${esc(generatedAt)}</span>`,
    ].filter(Boolean).join(' · ');
    _syncBuildControls();
}

/* ── D3 force graph ── */
function _renderGraph() {
    const svg = $('repo-graph-svg');
    const empty = $('repo-graph-empty');
    if (!svg || !empty) return;

    if (typeof d3 === 'undefined') {
        svg.innerHTML = '';
        _showEmpty('Graph rendering unavailable', 'D3 did not load. Build state and metadata are still live.');
        return;
    }

    if (!_topo || !_topo.nodes?.length) {
        svg.innerHTML = '';
        const fallback = _buildState.status === 'failed'
            ? ['Graph build failed', _buildState.message || 'Retry the build when the backend is healthy.']
            : _buildState.status === 'queued' || _buildState.status === 'building'
                ? ['Graph build in progress', _buildState.message || 'Waiting for backend build events…']
                : ['Repo graph not built yet', 'Queue a graph build to render deterministic repo relationships.'];
        _showEmpty(fallback[0], fallback[1]);
        return;
    }

    empty.style.display = 'none';

    const container = $('repo-graph-container');
    const W = container?.clientWidth  || 680;
    const H = container?.clientHeight || 480;

    const nodes = _topo.nodes.map(n => ({ ...n }));
    const links = (_topo.edges || []).map(e => ({ ...e }));

    const d3s = d3.select(svg);
    d3s.selectAll('*').remove();
    d3s.attr('viewBox', `0 0 ${W} ${H}`).attr('width', W).attr('height', H);

    if (_sim) _sim.stop();

    _sim = d3.forceSimulation(nodes)
        .force('link',   d3.forceLink(links).id(d => d.id).distance(d => _edgeDistance(d.type)).strength(d => _edgeLinkStrength(d)))
        .force('charge', d3.forceManyBody().strength(d => _nodeCharge(d)))
        .force('center', d3.forceCenter(W / 2, H / 2))
        .force('collide', d3.forceCollide(d => _nodeR(d) + 4));

    const linkLayer = d3s.append('g').attr('class', 'links');
    const linkEl = linkLayer.selectAll('line')
        .data(links).enter().append('line')
        .attr('stroke', d => _edgeColor(d.type))
        .attr('stroke-width', d => _edgeWidth(d))
        .attr('stroke-opacity', d => _edgeOpacity(d))
        .attr('stroke-dasharray', d => _edgeDash(d.type));

    const nodeLayer = d3s.append('g').attr('class', 'nodes');
    const nodeEl = nodeLayer.selectAll('.repo-node')
        .data(nodes).enter().append(d => _makeNodeEl(d))
        .attr('class', 'repo-node')
        .style('cursor', 'pointer')
        .on('mouseover', (ev, d) => _onHover(d, nodeEl, linkEl))
        .on('mouseout',  ()      => _onHoverOut(nodeEl, linkEl))
        .on('click',     (ev, d) => _onClick(d));

    const drag = d3.drag()
        .on('start', (ev, d) => { if (!ev.active) _sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
        .on('drag',  (ev, d) => { d.fx = ev.x; d.fy = ev.y; })
        .on('end',   (ev, d) => { if (!ev.active) _sim.alphaTarget(0); d.fx = null; d.fy = null; });
    nodeEl.call(drag);

    _sim.on('tick', () => {
        linkEl
            .attr('x1', d => d.source.x).attr('y1', d => d.source.y)
            .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
        nodeEl.attr('transform', d => `translate(${Math.max(10,Math.min(W-10,d.x))},${Math.max(10,Math.min(H-10,d.y))})`);
    });

    _applySearch();
}

function _makeNodeEl(d) {
    const doc = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    const r = _nodeR(d);

    if (d.type === 'memory') {
        const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        c.setAttribute('r', String(r));
        c.setAttribute('fill', _memColor(d));
        c.setAttribute('fill-opacity', '0.85');
        c.setAttribute('stroke', d.promoted ? '#a78bfa' : '#000');
        c.setAttribute('stroke-width', d.promoted ? '1.5' : '1');
        if (d.promoted) c.classList.add('node-pulse');
        doc.appendChild(c);
        return doc;
    }

    if (d.type === 'module') {
        // Diamond shape — rotated square
        const diamond = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
        diamond.setAttribute('points', `0,${-r} ${r},0 0,${r} ${-r},0`);
        diamond.setAttribute('fill', '#f59e0b');
        diamond.setAttribute('fill-opacity', '0.9');
        diamond.setAttribute('stroke', '#000');
        diamond.setAttribute('stroke-width', '1.2');
        doc.appendChild(diamond);
        // Short label below diamond
        const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        txt.setAttribute('text-anchor', 'middle');
        txt.setAttribute('y', String(r + 9));
        txt.setAttribute('font-size', '7');
        txt.setAttribute('fill', '#a3a3a3');
        txt.setAttribute('pointer-events', 'none');
        txt.textContent = (d.label || '').slice(0, 14);
        doc.appendChild(txt);
        return doc;
    }

    if (d.type === 'concept') {
        // Hexagon shape
        const pts = [];
        for (let i = 0; i < 6; i++) {
            const angle = (i * Math.PI) / 3 - Math.PI / 6;
            pts.push(`${(r * Math.cos(angle)).toFixed(1)},${(r * Math.sin(angle)).toFixed(1)}`);
        }
        const hex = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
        hex.setAttribute('points', pts.join(' '));
        hex.setAttribute('fill', '#a855f7');
        hex.setAttribute('fill-opacity', '0.85');
        hex.setAttribute('stroke', '#000');
        hex.setAttribute('stroke-width', '1');
        doc.appendChild(hex);
        const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        txt.setAttribute('text-anchor', 'middle');
        txt.setAttribute('y', String(r + 9));
        txt.setAttribute('font-size', '7');
        txt.setAttribute('fill', '#c084fc');
        txt.setAttribute('pointer-events', 'none');
        txt.textContent = (d.label || '').slice(0, 12);
        doc.appendChild(txt);
        return doc;
    }

    // File node — rectangle
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('x', String(-r));
    rect.setAttribute('y', String(-r));
    rect.setAttribute('width', String(r * 2));
    rect.setAttribute('height', String(r * 2));
    rect.setAttribute('rx', '2');
    rect.setAttribute('fill', _fileColor(d));
    rect.setAttribute('fill-opacity', '0.8');
    rect.setAttribute('stroke', '#000');
    rect.setAttribute('stroke-width', '1');
    doc.appendChild(rect);
    return doc;
}

function _nodeR(d) {
    if (d.type === 'memory')  return 4 + (d.priority || 0.3) * 8;
    if (d.type === 'module')  return 9;
    if (d.type === 'concept') return 7;
    return 5;
}

function _nodeCharge(d) {
    if (d.type === 'module')  return -120;
    if (d.type === 'concept') return -90;
    if (d.type === 'memory')  return -40;
    return -70;
}

function _edgeDistance(type) {
    if (type === 'belongs-to')         return 42;
    if (type === 'concept-of')         return 95;
    if (type === 'memory-association') return 85;
    if (type === 'memory-overlay')     return 70;
    if (type === 'configures')         return 80;
    if (type === 'tests')              return 60;
    return 55;
}

function _edgeLinkStrength(d) {
    if (d.type === 'belongs-to') return 0.5;
    if (d.type === 'imports')    return 0.3;
    return 0.2;
}

function _edgeColor(type) {
    if (type === 'imports')            return '#27272a';
    if (type === 'tests')              return '#22c55e';
    if (type === 'configures')         return '#78716c';
    if (type === 'memory-overlay')     return '#00d4ff';
    if (type === 'memory-association') return '#3f3f46';
    if (type === 'belongs-to')         return '#52525b';
    if (type === 'concept-of')         return '#a855f7';
    return '#27272a';
}

function _edgeWidth(d) {
    const base = d.type === 'imports' ? 1.3 : d.type === 'belongs-to' ? 0.8 : 1;
    return d.strength != null ? base * (0.5 + d.strength * 0.6) : base;
}

function _edgeOpacity(d) {
    const base = ['memory-overlay', 'concept-of', 'belongs-to'].includes(d.type) ? 0.35 : 0.5;
    return d.strength != null ? Math.min(0.85, base * (0.5 + d.strength * 0.7)) : base;
}

function _edgeDash(type) {
    if (['memory-overlay', 'memory-association', 'concept-of'].includes(type)) return '3,3';
    if (type === 'belongs-to') return '2,4';
    return null;
}

function _memColor(d) {
    const tier = d.tier || 'cortex';
    if (tier === 'prefrontal' || tier === 'working')    return '#00d4ff';
    if (tier === 'hippocampus' || tier === 'episodic')  return '#00ff88';
    return '#ffd14d';
}

function _fileColor(d) {
    const kind = d.kind || 'other';
    if (kind === 'source') return '#6366f1';
    if (kind === 'test')   return '#22c55e';
    if (kind === 'config') return '#78716c';
    return '#52525b';
}

/* ── Hover: highlight 1-hop neighbours ── */
function _onHover(d, nodeEl, linkEl) {
    const connected = new Set([d.id]);
    linkEl.each(function(l) {
        const s = typeof l.source === 'object' ? l.source.id : l.source;
        const t = typeof l.target === 'object' ? l.target.id : l.target;
        if (s === d.id) connected.add(t);
        if (t === d.id) connected.add(s);
    });
    nodeEl.attr('opacity', n => connected.has(n.id) ? 1 : 0.18);
    linkEl.attr('stroke-opacity', l => {
        const s = typeof l.source === 'object' ? l.source.id : l.source;
        const t = typeof l.target === 'object' ? l.target.id : l.target;
        return (s === d.id || t === d.id) ? 1 : 0.06;
    });
    _showTooltip(d.label || d.path || d.id);
}

function _onHoverOut(nodeEl, linkEl) {
    nodeEl.attr('opacity', 1);
    linkEl.attr('stroke-opacity', l => _edgeOpacity(l));
    _hideTooltip();
}

/* ── Click → drawer ── */
function _onClick(d) {
    const rows = [];
    if (d.type === 'file') {
        rows.push(['Path', d.path]);
        rows.push(['Kind', d.kind || 'other']);
        if (d.module) rows.push(['Module', d.module]);
    } else if (d.type === 'module') {
        rows.push(['Module path', d.path]);
    } else if (d.type === 'concept') {
        rows.push(['Terms', d.terms?.join(', ') || d.label]);
    } else {
        rows.push(['Tier', d.tier || 'cortex']);
        rows.push(['Priority', (d.priority || 0).toFixed(2)]);
        rows.push(['Tags', Array.isArray(d.tags) && d.tags.length ? d.tags.join(', ') : '—']);
    }
    rows.push(['Node type', d.type || 'unknown']);
    openDrawer({
        title: d.label || d.id,
        body: `<div class="detail-block">
            ${rows.map(([k, v]) => `<div class="detail-row"><span class="detail-lbl">${esc(k)}</span><span>${esc(String(v ?? '—'))}</span></div>`).join('')}
        </div>`,
    });
}

/* ── Search highlight ── */
function _applySearch() {
    const nodes = d3.selectAll('.repo-node');
    if (!_searchTerm) {
        nodes.attr('opacity', 1);
        return;
    }
    const q = _searchTerm.toLowerCase();
    nodes.attr('opacity', d => (d.label || d.path || d.id || '').toLowerCase().includes(q) ? 1 : 0.15);
}

/* ── Controls ── */
function _bindControls() {
    const buildHandler = async () => {
        _setBuildState('queued', 'Graph build queued…');
        const result = await post('/api/knowledge-topology/build', {});
        if (!result.ok) {
            _setBuildState('failed', result.error || 'Graph build could not be queued.');
            return;
        }
        bustCache('/api/knowledge-topology');
        _renderMeta();
        _renderGraph();
    };

    const b1 = $('repo-build-btn');
    const b2 = $('repo-build-btn2');
    if (b1) b1.onclick = buildHandler;
    if (b2) b2.onclick = buildHandler;

    const si = $('repo-search-input');
    if (si) {
        si.oninput = () => {
            _searchTerm = si.value.trim();
            if (typeof d3 === 'undefined') return;
            _applySearch();
        };
    }

    _syncBuildControls();
}

function _syncBuildControls() {
    const buttons = [$('repo-build-btn'), $('repo-build-btn2')].filter(Boolean);
    const label = _buildState.status === 'building'
        ? 'Building…'
        : _buildState.status === 'queued'
            ? 'Queued…'
            : _buildState.status === 'failed'
                ? 'Retry build'
                : _buildState.status === 'ready'
                    ? 'Rebuild graph'
                    : 'Build graph';
    for (const btn of buttons) {
        btn.textContent = label;
        btn.disabled = ['queued', 'building'].includes(_buildState.status);
        btn.title = _buildState.message || '';
    }
}

function _setBuildState(status, message='') {
    _buildState = { status, message, at: Date.now() };
    _syncBuildControls();
}

function _showEmpty(title, subtitle) {
    const empty = $('repo-graph-empty');
    if (!empty) return;
    empty.style.display = 'flex';
    empty.innerHTML = `
        <span>${esc(title)}</span>
        <span style="font-size:var(--caption);opacity:.7;max-width:360px;text-align:center">${esc(subtitle)}</span>
        <button id="repo-build-btn2" class="btn btn-sm">${_buildState.status === 'failed' ? 'Retry build' : 'Build now'}</button>`;
    $('repo-build-btn2')?.addEventListener('click', async () => {
        _setBuildState('queued', 'Graph build queued…');
        const result = await post('/api/knowledge-topology/build', {});
        if (!result.ok) {
            _setBuildState('failed', result.error || 'Graph build could not be queued.');
            _renderMeta();
            _renderGraph();
            return;
        }
        bustCache('/api/knowledge-topology');
        _renderMeta();
        _renderGraph();
    });
}

/* ── Tooltip ── */
let _tip = null;
function _showTooltip(txt) {
    if (!_tip) {
        _tip = document.createElement('div');
        _tip.style.cssText = 'position:fixed;z-index:400;background:#09090b;border:1px solid #27272a;padding:3px 8px;border-radius:4px;font-family:var(--font-mono);font-size:.62rem;color:#e4e4e7;pointer-events:none;white-space:nowrap;max-width:280px;overflow:hidden;text-overflow:ellipsis';
        document.body.appendChild(_tip);
    }
    _tip.textContent = txt;
    _tip.style.display = 'block';
    document.addEventListener('mousemove', _moveTooltip, { passive: true });
}

function _moveTooltip(ev) {
    if (_tip) {
        _tip.style.left = (ev.clientX + 12) + 'px';
        _tip.style.top = (ev.clientY - 8) + 'px';
    }
}

function _hideTooltip() {
    if (_tip) _tip.style.display = 'none';
    document.removeEventListener('mousemove', _moveTooltip);
}

function fmtNum(n) {
    if (n == null || isNaN(n)) return '—';
    const v = Number(n);
    if (v >= 1e6) return (v / 1e6).toFixed(1) + 'M';
    if (v >= 1000) return (v / 1000).toFixed(1) + 'K';
    return String(Math.round(v));
}
