/**
 * memory.js — Memory view: pyramid, list, D3 graph, temporal slider, search,
 *             formation rate, and promotion ticker.
 */

import { S, bus }       from '../state.js';
import { api }          from '../api.js';
import { openDrawer }   from '../widgets/drawer.js';
import { renderPyramid } from '../widgets/pyramid.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;');

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));
}

function timeAgo(ts) {
    if (!ts) return '';
    const d=Date.now()-ts;
    if (d<60000) return Math.round(d/1000)+'s ago';
    if (d<3600000) return Math.round(d/60000)+'m ago';
    if (d<86400000) return Math.round(d/3600000)+'h ago';
    return Math.round(d/86400000)+'d ago';
}

let _activeTier = null;

function memText(m) {
    return m.title || m.excerpt || m.content || m.summary || m.id || '';
}

function pct(value) {
    const n = Number(value);
    return Number.isFinite(n) ? `${Math.round(n * 100)}%` : '—';
}

function decayProfile(m) {
    const rawScore = Number(m.decay?.score ?? m.entropyScore ?? m.entropy ?? 0);
    const score = Number.isFinite(rawScore) ? Math.max(0, Math.min(1, rawScore)) : 0;
    const state = m.decay?.state || (score >= 0.75 ? 'retiring' : score >= 0.5 ? 'stale' : score >= 0.25 ? 'fading' : 'fresh');
    return {
        ...m.decay,
        score,
        state,
        nextAction: m.decay?.nextAction || (state === 'fresh' ? 'keep' : state === 'fading' ? 'reinforce' : state === 'stale' ? 'review' : 'archive'),
    };
}

function reinforcementProfile(m) {
    const rawScore = Number(m.reinforcement?.score ?? m.trustScore ?? m.trust ?? 0);
    const score = Number.isFinite(rawScore) ? Math.max(0, Math.min(1, rawScore)) : 0;
    return { ...m.reinforcement, score };
}

function decayStroke(state) {
    if (state === 'retiring') return '#ff5f57';
    if (state === 'stale') return '#ffd14d';
    if (state === 'fading') return '#00d4ff';
    return '#00ff88';
}

function lifecycleLabel(m) {
    const decay = decayProfile(m);
    const reinforcement = reinforcementProfile(m);
    return `${decay.state} · ${decay.nextAction} · reinforced ${pct(reinforcement.score)}`;
}

function selectedMemories() {
    const selected = new Set(S.selectedMemoryIds || []);
    return S.memories.filter(m => selected.has(m.id));
}

function fenceLanguage(m) {
    const tagged = (m.tags || []).find(tag => /^lang:|^language:/i.test(String(tag)));
    if (tagged) return String(tagged).split(':').pop().replace(/[^a-z0-9_+-]/gi, '') || 'text';
    const text = String(m.content || m.excerpt || '');
    if (/^\s*(import|export|const|let|function|class)\s/m.test(text)) return 'javascript';
    if (/^\s*(def|class)\s+\w+/m.test(text)) return 'python';
    const fenced = text.match(/```([a-z0-9_+-]+)/i);
    if (fenced?.[1]) return fenced[1];
    return 'text';
}

function memoryBlock(m) {
    const title = memText(m).split('\n')[0].slice(0, 100) || m.id;
    const body = String(m.content || m.excerpt || m.summary || title || '').trim();
    const cleaned = body.replace(/```/g, '~~~');
    return [
        `<!-- memory:${m.id || 'unknown'} ${title} -->`,
        '```' + fenceLanguage(m),
        cleaned || title,
        '```',
    ].join('\n');
}

function selectedMemoryBlock() {
    return selectedMemories().map(memoryBlock).join('\n\n');
}

function toggleMemorySelection(id) {
    const current = new Set(S.selectedMemoryIds || []);
    if (current.has(id)) current.delete(id);
    else current.add(id);
    S.selectedMemoryIds = [...current];
    renderMemorySelection();
    renderMemList();
}

function renderMemorySelection() {
    const panel = $('memory-selection-panel');
    if (!panel) return;
    const selected = selectedMemories();
    if (!selected.length) {
        panel.innerHTML = `<div class="memory-selection-empty">No memories selected</div>`;
        return;
    }
    const block = selectedMemoryBlock();
    panel.innerHTML = `<div class="memory-selection-head">
        <div><strong>${selected.length}</strong><span> selected</span></div>
        <div class="memory-selection-actions">
            <button class="btn btn-sm" id="memory-selection-copy">Copy block</button>
            <button class="btn btn-sm" id="memory-selection-clear">Clear</button>
        </div>
    </div>
    <div class="memory-selection-chips">${selected.map(m => `<span class="chip chip-muted">${esc(memText(m).slice(0, 42) || m.id)}</span>`).join('')}</div>
    <pre class="memory-code-block"><code>${esc(block)}</code></pre>`;
    $('memory-selection-clear')?.addEventListener('click', () => {
        S.selectedMemoryIds = [];
        renderMemorySelection();
        renderMemList();
    });
    $('memory-selection-copy')?.addEventListener('click', async () => {
        try {
            await navigator.clipboard?.writeText(block);
            bus.emit('toast', { msg: 'Memory block copied', kind: 'good' });
        } catch {
            bus.emit('toast', { msg: 'Clipboard unavailable', kind: 'warn' });
        }
    });
}

/* ── Data loader ──
 * `allSettled` is deliberate: if the topology endpoint fails or times out,
 * the memory list + health + RAG collections should still render. Previous
 * behaviour (`Promise.all`) made a single failed call hide the whole section.
 */
export async function load() {
    const params = new URLSearchParams({ includeInactive: 'true', limit: '100' });
    if (S.workspace?.repoName) params.set('repo', S.workspace.repoName);
    const [memsR, healthR, ragsR, topoR] = await Promise.allSettled([
        api('/api/memory?'+params.toString(), 5000),
        api('/api/memory/health', 5000),
        api('/api/rag/collections', 15000),
        api('/api/knowledge-topology', 30000),
    ]);
    const mems = memsR.status==='fulfilled' ? memsR.value : null;
    const health = healthR.status==='fulfilled' ? healthR.value : null;
    const rags = ragsR.status==='fulfilled' ? ragsR.value : null;
    const topo = topoR.status==='fulfilled' ? topoR.value : null;
    S.memories  = Array.isArray(mems) ? mems : (mems?.memories||[]);
    S.memHealth = health;
    S.ragCols   = Array.isArray(rags) ? rags : (rags?.collections||[]);
    S.topology  = topo && !topo.error ? topo : null;
    S.topologyFailed = topoR.status !== 'fulfilled' || (topo && topo.error);
    buildGraph();
    render();
}

export function render() {
    renderPyramidWidget();
    renderFormationRate();
    renderPromotionTicker();
    renderStats();
    renderGraph();
    renderMemorySelection();
    renderMemList();
    initSlider();
}

/* ── Memory pyramid ── */
function renderPyramidWidget() {
    const container=$('mem-pyramid-widget'); if (!container) return;
    const h=S.memHealth;
    if (!h) { container.innerHTML=''; return; }
    const fallback = S.memories.reduce((acc, m) => {
        const tier = m.tier || 'semantic';
        if (tier === 'working' || tier === 'prefrontal') acc.prefrontal += 1;
        else if (tier === 'episodic' || tier === 'hippocampus') acc.hippocampus += 1;
        else acc.cortex += 1;
        return acc;
    }, { prefrontal: 0, hippocampus: 0, cortex: 0 });
    const tierCounts = h.tierCounts || {};
    const count = (fallbackValue, ...values) => {
        const positive = values.find(v => Number(v) > 0);
        return positive != null ? Number(positive) : fallbackValue;
    };
    const counts = {
        prefrontal:  count(fallback.prefrontal, h.working, h.prefrontal, tierCounts.prefrontal),
        hippocampus: count(fallback.hippocampus, h.episodic, h.hippocampus, tierCounts.hippocampus),
        cortex:      count(fallback.cortex, h.semantic, h.cortex, tierCounts.cortex),
    };
    renderPyramid(container, counts, _activeTier, tier => {
        _activeTier = tier;
        renderPyramidWidget();
        renderMemList();
    });
}

/* ── Formation rate ── */
function renderFormationRate() {
    const el=$('mem-formation-rate'); if (!el) return;
    const fr=S.memHealth?.formationRate;
    if (!fr) { el.textContent=''; return; }
    el.textContent=`Last 60 min: ${fr.lastHour??'—'} formed · Last 24h: ${fr.lastDay??'—'} formed`;
}

/* ── Promotion ticker ── */
function renderPromotionTicker() {
    const el=$('mem-promotion-ticker'); if (!el) return;
    const promotions=S.memHealth?.recentPromotions||[];
    if (!promotions.length) { el.innerHTML='<div style="padding:8px 12px;font-size:var(--text-sm);color:var(--text-dim)">No promotions yet</div>'; return; }
    el.innerHTML=promotions.slice(0,5).map(p=>`
        <div class="promotion-row">
            <span class="promotion-from">${esc(p.from)}</span>
            <span class="promotion-arrow">→</span>
            <span class="promotion-to">${esc(p.to)}</span>
            <span class="promotion-time">${timeAgo(p.at)}</span>
        </div>`).join('');
}

function renderStats() {
    const el = $('mem-stats'); if (!el) return;
    const h = S.memHealth || {};
    const meta = S.topology?.meta || {};
    const prov = S.topology?.provenance || {};
    const fbMode = S.gFallbackMode || getFallbackMode();
    const fbLabel = fbMode === 'none' ? 'real-topology'
        : fbMode === 'synthetic' ? 'synthetic (degraded)'
        : fbMode === 'topology-missing' ? 'unavailable'
        : 'empty';
    const rows = [
        ['Active memories', fmtNum(h.active ?? S.memories.length)],
        ['Working / episodic / semantic', `${fmtNum(h.working ?? 0)} / ${fmtNum(h.episodic ?? 0)} / ${fmtNum(h.semantic ?? 0)}`],
        ['Quarantined', fmtNum(h.quarantined ?? 0)],
        ['Graph nodes / edges', `${fmtNum(S.gNodes.length)} / ${fmtNum(S.gLinks.length)}`],
        ['Graph source', prov.graphSource || meta.graphSource || 'summary-only'],
        ['Graph mode', fbLabel],
        ['Freshness', prov.freshness || meta.freshness || 'n/a'],
    ];
    const topTags = Array.isArray(h.topTags) ? h.topTags.slice(0, 6) : [];
    el.innerHTML = `
        <div class="shd" style="margin-bottom:8px">Memory signals</div>
        ${rows.map(([k, v]) => `<div class="row"><span class="row-k">${esc(k)}</span><span class="row-v">${esc(String(v))}</span></div>`).join('')}
        <div style="margin-top:12px;font-size:var(--text-sm);color:var(--text-dim)">Top tags</div>
        <div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:8px">
            ${topTags.length ? topTags.map(tag => `<span class="chip">${esc(tag)}</span>`).join('') : '<span style="color:var(--text-dim);font-size:var(--text-sm)">No tags yet</span>'}
        </div>`;
}

function setGraphFallback(title, subtitle) {
    const emptyCta = $('graph-empty-cta');
    if (!emptyCta) return;
    emptyCta.style.display = 'flex';
    emptyCta.innerHTML = `
        <div class="empty-title">${esc(title)}</div>
        <div class="empty-sub">${esc(subtitle)}</div>`;
}

function _renderFallbackBanner(mode) {
    const container = $('graph-container');
    if (!container) return;
    let banner = document.getElementById('graph-fallback-banner');
    if (!banner) {
        banner = document.createElement('div');
        banner.id = 'graph-fallback-banner';
        banner.style.cssText = 'position:absolute;top:6px;left:8px;right:8px;padding:4px 8px;background:#1a1a1c;border:1px solid #3f3f46;border-radius:3px;font-family:var(--font-mono);font-size:.65rem;color:#facc15;z-index:10;pointer-events:none';
        container.appendChild(banner);
    }
    const msg = mode === 'synthetic'
        ? 'Showing derived relationships (file topology shell). Not a structural code graph.'
        : 'Topology unavailable — showing memory-overlay fallback. Edges are approximate.';
    banner.textContent = msg;
    banner.style.display = 'block';
}

function _clearFallbackBanner() {
    const banner = document.getElementById('graph-fallback-banner');
    if (banner) banner.style.display = 'none';
}

/* ── D3 graph ──
 * Truthfulness rules:
 *   - fallbackMode === 'none'     → real structural topology, draw normally
 *   - fallbackMode === 'synthetic' or 'topology-missing' → degraded data,
 *     render banner but keep nodes so the user still sees memory dots
 *   - fallbackMode === 'empty' or topology missing → no synthetic tag edges
 *
 * Stability rules:
 *   - Memory nodes are sorted by id so reloads don't shuffle layout.
 *   - Tag-synthesized edges only appear when the backend confirms a real
 *     topology exists — otherwise they'd be pretending to be a graph.
 */
function getFallbackMode() {
    if (S.topologyFailed) return 'topology-missing';
    const meta = S.topology?.meta || {};
    const prov = S.topology?.provenance || {};
    return S.topology?.fallbackMode || prov.fallbackMode || meta.fallbackMode || (S.topology ? 'empty' : 'topology-missing');
}

function buildGraph() {
    const fallbackMode = getFallbackMode();
    const sortedMemories = [...S.memories].sort((a, b) => String(a.id).localeCompare(String(b.id)));
    S.gNodes = sortedMemories.map(m=>({
        id:m.id, label:m.title||(m.content||'').substring(0,50)||m.id,
        priority:m.priority||0.5, tier:m.tier||'semantic', nodeType:'memory',
        createdAt:m.createdAt||m.timestamp||Date.now(), tags:m.tags||[],
        decayState:decayProfile(m).state, decayScore:decayProfile(m).score,
        reinforcementScore:reinforcementProfile(m).score, data:m,
    }));

    const links = [];
    // Only use tag-based edges when the backend says a real topology exists.
    // Otherwise, these would be fabricated relationships that mislead the user.
    if (fallbackMode === 'none' && S.topology?.nodes) {
        const ns = S.gNodes.slice(0, 60);
        for (let i = 0; i < ns.length; i++) {
            for (let j = i + 1; j < ns.length; j++) {
                const shared = ns[i].tags.filter(t => ns[j].tags.includes(t)).length;
                if (shared >= 2) links.push({ source: ns[i].id, target: ns[j].id, v: shared });
            }
        }
    }

    if (S.topology?.nodes) {
        const existingIds = new Set(S.gNodes.map(n => n.id));
        const sortedTopoNodes = [...S.topology.nodes].sort((a, b) => String(a.id).localeCompare(String(b.id)));
        for (const tn of sortedTopoNodes) {
            if (tn.type === 'file' && !existingIds.has(tn.id)) {
                S.gNodes.push({ id: tn.id, label: tn.label || tn.path || tn.id, priority: 0.3, tier: 'file', nodeType: 'file', createdAt: tn.createdAt || 0, tags: [], data: tn });
                existingIds.add(tn.id);
            }
        }
        for (const e of (S.topology.edges || [])) links.push({ source: e.source, target: e.target, v: 1 });
    }
    S.gLinks = links;
    S.gFallbackMode = fallbackMode;
}

function renderGraph() {
    const svg=$('graph-svg'); if (!svg) return;
    const c=$('graph-container'), W=c?.clientWidth||640, H=c?.clientHeight||320;
    const cutoff=S.temporalCursor||Date.now();
    const nodes=S.gNodes.filter(n=>(n.createdAt||0)<=cutoff).map(n=>({...n}));
    const nset=new Set(nodes.map(n=>n.id));
    const links=S.gLinks.filter(l=>{
        const s=typeof l.source==='object'?l.source.id:l.source;
        const t=typeof l.target==='object'?l.target.id:l.target;
        return nset.has(s)&&nset.has(t);
    }).map(l=>({...l}));

    if (typeof d3 === 'undefined') {
        svg.innerHTML = '';
        setGraphFallback('Graph rendering unavailable', 'D3 did not load. Memory list and time controls still work.');
        return;
    }

    // Stop any running simulation — we either redraw, or we clear below. Either
    // way the old sim must be halted so reloads don't leave ghost animations.
    if (S.graphSim) { try { S.graphSim.stop(); } catch {} S.graphSim = null; }

    const d3s=d3.select(svg);
    d3s.selectAll('*').remove();
    d3s.attr('width',W).attr('height',H);

    // Truthfulness: if the backend couldn't produce a real graph, clear the SVG
    // and show the explicit empty-state banner. No synthetic edges rendered.
    const fallbackMode = S.gFallbackMode || getFallbackMode();
    if (fallbackMode === 'topology-missing') {
        setGraphFallback('Knowledge graph unavailable', 'Could not reach /api/knowledge-topology. Memory list below still works.');
        return;
    }
    if (fallbackMode === 'empty' || !nodes.length) {
        setGraphFallback('No memory graph yet', 'Run a few tasks and memories will appear here.');
        return;
    }
    const emptyCta=$('graph-empty-cta');
    if (emptyCta) emptyCta.style.display='none';
    if (fallbackMode === 'synthetic' || fallbackMode === 'topology-missing') {
        // Render a visible "synthetic" banner ABOVE the graph so the user knows
        // the edges come from tag overlap, not structural code relationships.
        _renderFallbackBanner(fallbackMode);
    } else {
        _clearFallbackBanner();
    }

    if (S.graphSim) S.graphSim.stop();
    const sim=d3.forceSimulation(nodes)
        .force('link',d3.forceLink(links).id(d=>d.id).distance(55).strength(0.25))
        .force('charge',d3.forceManyBody().strength(-55))
        .force('center',d3.forceCenter(W/2,H/2))
        .force('collision',d3.forceCollide(d=>5+d.priority*9));
    S.graphSim=sim;

    const linkEl=d3s.append('g').selectAll('line').data(links).enter().append('line')
        .attr('stroke','#27272a').attr('stroke-width',1).attr('stroke-opacity',0.6);
    const fileNodes=nodes.filter(n=>n.nodeType==='file');
    const memNodes =nodes.filter(n=>n.nodeType!=='file');
    const fileEl=d3s.append('g').selectAll('rect').data(fileNodes).enter().append('rect')
        .attr('width',d=>6+d.priority*8).attr('height',d=>6+d.priority*8)
        .attr('rx',2).attr('fill','#6366f1').attr('fill-opacity',0.75).attr('stroke','#000').attr('stroke-width',1)
        .style('cursor','pointer')
        .on('mouseover',function(ev,d){ _memHover(d, nodeEl, fileEl, linkEl); _showTip(ev,d.label); })
        .on('mouseout', function(){ _memHoverOut(nodeEl, fileEl, linkEl); _hideTip(); });
    const nc=d=>{ const age=Date.now()-(d.createdAt||Date.now()); if(age<3600000) return '#00d4ff'; if(age<86400000) return '#00ff88'; if(age<604800000) return '#ffd14d'; return '#ff5f57'; };
    // Mark promoted nodes (hippocampus or cortex) for pulsating ring
    const isPromoted=d=>(d.tier==='hippocampus'||d.tier==='cortex'||d.tier==='episodic'||d.tier==='semantic');
    const nodeEl=d3s.append('g').selectAll('circle').data(memNodes).enter().append('circle')
        .attr('r',d=>4+d.priority*9).attr('fill',nc).attr('fill-opacity',0.85)
        .attr('stroke',d=>decayStroke(d.decayState)).attr('stroke-width',d=>isPromoted(d)||d.decayState==='stale'||d.decayState==='retiring'?1.8:1)
        .style('cursor','pointer')
        .classed('node-pulse', d=>isPromoted(d) || d.reinforcementScore >= 0.7)
        .on('click',(_,d)=>_openMemDrawer(d))
        .on('mouseover',function(ev,d){ _memHover(d, nodeEl, fileEl, linkEl); _showTip(ev,`${d.label} · ${lifecycleLabel(d.data||d)}`); })
        .on('mouseout', function(){ _memHoverOut(nodeEl, fileEl, linkEl); _hideTip(); });
    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); fileEl.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('cx',d=>Math.max(8,Math.min(W-8,d.x))).attr('cy',d=>Math.max(8,Math.min(H-8,d.y)));
        fileEl.attr('x',d=>Math.max(4,Math.min(W-12,d.x-(3+d.priority*4)))).attr('y',d=>Math.max(4,Math.min(H-12,d.y-(3+d.priority*4))));
    });
}

let _tip=null;
function _showTip(ev,txt) {
    if (!_tip) { _tip=document.createElement('div'); _tip.style.cssText='position:fixed;z-index:300;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:260px;overflow:hidden;text-overflow:ellipsis'; document.body.appendChild(_tip); }
    _tip.textContent=txt; _tip.style.left=(ev.clientX+12)+'px'; _tip.style.top=(ev.clientY-8)+'px'; _tip.style.display='block';
}
function _hideTip() { if (_tip) _tip.style.display='none'; }

/* ── 1-hop highlight helpers ── */
function _memHover(d, nodeEl, fileEl, 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.15);
    fileEl.attr('opacity',n=>connected.has(n.id)?1:0.15);
    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.05;
    });
}
function _memHoverOut(nodeEl, fileEl, linkEl) {
    nodeEl.attr('opacity',1); fileEl.attr('opacity',1); linkEl.attr('stroke-opacity',0.6);
}

/* ── Temporal slider ── */
function initSlider() {
    const slider=$('temporal-slider'); if (!slider) return;
    if (!S.gNodes.length) {
        slider.disabled = true;
        const sL=$('slider-start'), nL=$('slider-now');
        if (sL) sL.textContent = '—';
        if (nL) nL.textContent = 'Now';
        return;
    }
    const minTs=Math.min(...S.gNodes.map(n=>n.createdAt||Date.now())), maxTs=Date.now();
    slider.disabled = false;
    slider.min=String(minTs); slider.max=String(maxTs); slider.value=String(maxTs);
    S.temporalCursor=maxTs;
    const sL=$('slider-start'), nL=$('slider-now');
    if (sL) sL.textContent=new Date(minTs).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
    slider.oninput=()=>{ const v=parseInt(slider.value); S.temporalCursor=v; if(nL) nL.textContent=v>=maxTs-1000?'Now':new Date(v).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); renderGraph(); };
}

/* ── Memory list ── */
export function renderMemList() {
    const el=$('mem-list'); if (!el) return;
    const q=S.memQuery.toLowerCase();
    let mems=S.memories.filter(m=>{
        if (_activeTier) {
            const tierMap={prefrontal:['working','prefrontal'],hippocampus:['episodic','hippocampus'],cortex:['semantic','cortex']};
            if (!tierMap[_activeTier].includes(m.tier||'semantic')) return false;
        }
        if (!q) return true;
        return [memText(m),(m.content||''),(m.excerpt||''),(m.tags||[]).join(' ')].some(s=>s.toLowerCase().includes(q));
    }).slice(0,60);
    if (!mems.length) {
        const repoHint=S.workspace?.repoName?` for <code>${esc(S.workspace.repoName)}</code>`:'';
        el.innerHTML=q?`<div class="empty"><div class="empty-title">No matches for "${esc(q)}"</div></div>`:`<div class="empty"><div class="empty-title">No memories${repoHint}</div><div class="empty-sub">Run a few tool calls and they'll appear here automatically.</div></div>`;
        return;
    }
    el.innerHTML=mems.map(m=>{
        const tier=m.tier||'semantic';
        const pri=m.priority!=null?`p:${Math.round(m.priority*100)}%`:'';
        const state=m.state && m.state !== 'active' ? ` · ${m.state}` : '';
        const decay=decayProfile(m);
        const reinforcement=reinforcementProfile(m);
        const decayStyle=decay.state==='stale'||decay.state==='retiring'?'opacity:0.72':'';
        const createdAt=m.createdAt||m.timestamp;
        const selected = (S.selectedMemoryIds || []).includes(m.id);
        return `<div class="mem-item ${selected ? 'selected' : ''}" style="${decayStyle}" data-memid="${esc(m.id)}">
            <button class="mem-select-btn ${selected ? 'active' : ''}" data-mem-toggle="${esc(m.id)}" aria-pressed="${selected ? 'true' : 'false'}">${selected ? 'Selected' : 'Select'}</button>
            <div class="tier-dot t-${esc(tier)}"></div>
            <div style="flex:1;min-width:0">
                <div class="mem-title">${esc(memText(m).substring(0,90))}</div>
                <div class="mem-meta">${esc(tier)}${esc(state)}${pri?' · '+pri:''}${createdAt?' · '+timeAgo(createdAt):''} · ${esc(decay.state)}:${esc(decay.nextAction)} · r:${esc(pct(reinforcement.score))}</div>
            </div>
        </div>`;
    }).join('');
    el.querySelectorAll('[data-mem-toggle]').forEach(button => {
        button.addEventListener('click', event => {
            event.stopPropagation();
            toggleMemorySelection(button.dataset.memToggle);
        });
    });
    el.querySelectorAll('[data-memid]').forEach(item => {
        item.addEventListener('click', () => {
            const m=S.memories.find(x=>x.id===item.dataset.memid); if (m) _openMemDrawer({id:m.id,label:memText(m),data:m});
        });
    });
}

function _browseMemories() {
    const items = S.memories.slice(0, 120);
    openDrawer({
        title: `Memories (${items.length})`,
        body: items.length ? `<div class="trust-event-list">
            ${items.map(m => `<div class="trust-event-row" data-drawer-mem="${esc(m.id)}">
                <div class="trust-event-main">
                    <span class="chip chip-muted">${esc(m.tier || 'semantic')}</span>
                    <span class="chip chip-muted">${esc(lifecycleLabel(m))}</span>
                    <span class="trust-event-type">${esc(memText(m).slice(0, 120))}</span>
                </div>
                <div class="trust-event-meta">${esc(m.state || 'active')}${(m.createdAt||m.timestamp) ? ' · '+esc(timeAgo(m.createdAt||m.timestamp)) : ''}</div>
            </div>`).join('')}
        </div>` : '<div class="empty-sub">No memories available for the selected runtime.</div>',
    });
    document.querySelectorAll('[data-drawer-mem]').forEach(row => {
        row.addEventListener('click', () => {
            const memory = S.memories.find(item => item.id === row.dataset.drawerMem);
            if (memory) _openMemDrawer({ id: memory.id, label: memText(memory), data: memory });
        });
    });
}

function _openMemDrawer(node) {
    const m=node.data||node;
    const tags=(m.tags||[]).map(t=>`<span class="chip">${esc(t)}</span>`).join(' ');
    const drows=rows=>rows.map(([k,v])=>`<div class="drow"><span class="drow-k">${esc(k)}</span><span class="drow-v">${esc(String(v??'—'))}</span></div>`).join('');
    const decay=decayProfile(m);
    const reinforcement=reinforcementProfile(m);
    const createdAt=m.createdAt||m.timestamp;
    const decayReasons=(decay.reasons||[]).slice(0,4).map(reason=>`<span class="chip chip-muted">${esc(reason)}</span>`).join(' ');
    const reinforcementReasons=(reinforcement.reasons||[]).slice(0,4).map(reason=>`<span class="chip chip-muted">${esc(reason)}</span>`).join(' ');
    openDrawer({ title: node.label||'Memory', body:
        `<div class="dsec"><div class="dsec-title">Memory</div>${drows([
            ['ID',m.id],['Tier',m.tier||'semantic'],
            ['Priority',m.priority!=null?Math.round(m.priority*100)+'%':'—'],
            ['Created',createdAt?new Date(createdAt).toLocaleString():'—'],
            ['Entropy',m.entropyScore!=null?m.entropyScore.toFixed(3):(m.entropy!=null?m.entropy.toFixed(3):'—')]
        ])}</div>
        <div class="dsec"><div class="dsec-title">Lifecycle</div>${drows([
            ['Decay',`${decay.state} (${pct(decay.score)})`],
            ['Next action',decay.nextAction],
            ['Half-life',decay.halfLifeDays!=null?`${Math.round(decay.halfLifeDays)}d`:'—'],
            ['Reinforcement',pct(reinforcement.score)],
            ['Feedback',reinforcement.feedbackCount ?? 0],
            ['Recalls',reinforcement.accessCount ?? m.accessCount ?? 0]
        ])}${decayReasons?`<div class="dtags">${decayReasons}</div>`:''}${reinforcementReasons?`<div class="dtags">${reinforcementReasons}</div>`:''}</div>
        ${tags?`<div class="dsec"><div class="dsec-title">Tags</div><div class="dtags">${tags}</div></div>`:''}
        <div class="dsec"><div class="dsec-title">Content</div><div class="dcontent">${esc(m.content||m.excerpt||m.title||'(no content)')}</div></div>
        <div class="dsec"><div class="dsec-title">Code block</div><pre class="memory-code-block"><code>${esc(memoryBlock(m))}</code></pre></div>` });
}

/* ── Init ── */
export function init() {
    $('mem-search')?.addEventListener('input', e => { S.memQuery=e.target.value; renderMemList(); });
    $('mem-browse-btn')?.addEventListener('click', _browseMemories);
    $('mem-list-browse-btn')?.addEventListener('click', _browseMemories);
    $('mem-graph-max-btn')?.addEventListener('click', () => {
        const c = $('graph-container');
        const b = $('mem-graph-max-btn');
        if (!c || !b) return;
        const expanded = c.classList.toggle('graph-expanded');
        b.textContent = expanded ? 'Restore graph' : 'Maximize graph';
        renderGraph();
    });
    document.querySelectorAll('.lane-btn').forEach(b => b.addEventListener('click', () => {
        S.memLane=b.dataset.lane;
        document.querySelectorAll('.lane-btn').forEach(x=>x.classList.toggle('active',x===b));
        renderMemList();
    }));
}
