/**
 * Memory consolidator — cold-path hippocampus clustering and promotion.
 *
 * Groups stale hippocampus entries by tag/theme similarity, merges each
 * cluster into a single cortex-level summary, and archives the originals.
 *
 * Design:
 *   - No external LLM required. Uses extractive summarization (term freq).
 *   - Pluggable Summarizer interface: swap in LLM backend when available.
 *   - Safe: never deletes originals; marks them 'archived' in state.
 *   - Run nightly via the watchdog or on-demand via nexus_memory_hygiene.
 */
import { createHash } from 'crypto';
/** Extractive summarizer — no LLM dependency. */
export class ExtractiveSummarizer {
    async summarize(entries, _topic) {
        if (entries.length === 0)
            return '';
        if (entries.length === 1)
            return entries[0].slice(0, 300);
        // score sentences by term frequency across all entries
        const allText = entries.join(' ');
        const termFreq = buildTermFreq(allText);
        const scored = [];
        for (const entry of entries) {
            const sentences = entry.split(/[.!?\n]+/).filter(s => s.trim().length > 10);
            for (const s of sentences) {
                const score = s.toLowerCase().split(/\W+/)
                    .filter(t => t.length > 3)
                    .reduce((sum, t) => sum + (termFreq.get(t) ?? 0), 0);
                scored.push([s.trim(), score]);
            }
        }
        scored.sort((a, b) => b[1] - a[1]);
        const topSentences = [...new Set(scored.slice(0, 5).map(([s]) => s))];
        return topSentences.join('. ').slice(0, 600);
    }
}
function buildTermFreq(text) {
    const freq = new Map();
    for (const term of text.toLowerCase().split(/\W+/)) {
        if (term.length > 3)
            freq.set(term, (freq.get(term) ?? 0) + 1);
    }
    return freq;
}
/**
 * Parse tags from the `memories.tags` column.
 *
 * The canonical on-disk format is a JSON array string (see memory.ts writers),
 * but legacy rows written by older code paths may contain a comma-separated
 * string. Accept both so consolidation never silently loses tags.
 */
function parseTags(raw) {
    const text = String(raw ?? '').trim();
    if (!text)
        return [];
    if (text.startsWith('[')) {
        try {
            const parsed = JSON.parse(text);
            if (Array.isArray(parsed)) {
                return parsed.map(t => String(t).trim()).filter(Boolean);
            }
        }
        catch { /* fall through to CSV */ }
    }
    return text.split(',').map(t => t.trim()).filter(Boolean);
}
/** Jaccard similarity on tag sets. */
function tagSimilarity(a, b) {
    const setA = new Set(parseTags(a));
    const setB = new Set(parseTags(b));
    if (setA.size === 0 && setB.size === 0)
        return 1;
    let intersection = 0;
    for (const t of setA)
        if (setB.has(t))
            intersection++;
    const union = new Set([...setA, ...setB]).size;
    return union > 0 ? intersection / union : 0;
}
/** Group entries by tag similarity threshold. */
function clusterEntries(rows, threshold = 0.3) {
    const used = new Set();
    const clusters = [];
    for (const row of rows) {
        if (used.has(row.id))
            continue;
        const cluster = [row];
        used.add(row.id);
        for (const other of rows) {
            if (used.has(other.id))
                continue;
            if (tagSimilarity(row.tags, other.tags) >= threshold) {
                cluster.push(other);
                used.add(other.id);
            }
        }
        clusters.push(cluster);
    }
    return clusters;
}
/**
 * Consolidate stale hippocampus entries into cortex summaries.
 * Safe: marks originals 'archived', never deletes.
 */
export async function consolidateMemory(db, opts = {}) {
    const minAgeMs = opts.minAgeMs ?? 24 * 60 * 60 * 1000;
    const minClusterSize = opts.minClusterSize ?? 3;
    const threshold = opts.similarityThreshold ?? 0.3;
    const summarizer = opts.summarizer ?? new ExtractiveSummarizer();
    const cutoff = Date.now() - minAgeMs;
    const rows = (db.prepare(`
        SELECT id, content, tags, tier, scope, priority, timestamp, state
        FROM memories
        WHERE tier = 'hippocampus'
          AND state = 'active'
          AND timestamp < ?
        ORDER BY timestamp ASC
        LIMIT 500
    `).all(cutoff));
    if (rows.length === 0)
        return { clustersProcessed: 0, entriesArchived: 0, memoriesPromoted: 0 };
    const clusters = clusterEntries(rows, threshold).filter(c => c.length >= minClusterSize);
    let entriesArchived = 0;
    let memoriesPromoted = 0;
    for (const cluster of clusters) {
        if (cluster.length < minClusterSize)
            continue;
        const topic = parseTags(cluster[0].tags)[0] ?? 'general';
        const summary = await summarizer.summarize(cluster.map(r => r.content), topic);
        if (!summary.trim())
            continue;
        const maxPriority = Math.max(...cluster.map(r => r.priority));
        // Write JSON array — readers in memory.ts call JSON.parse(row.tags).
        // Previous CSV output crashed recall paths with SyntaxError.
        const mergedTags = JSON.stringify([...new Set(cluster.flatMap(r => parseTags(r.tags)))]);
        const scope = cluster[0].scope; // inherit scope from first entry
        // Insert consolidated cortex entry + archive sources atomically
        const mergedId = `consol-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
        const fp = createHash('sha256').update(summary).digest('hex').slice(0, 32);
        db.transaction(() => {
            db.prepare(`
                INSERT OR IGNORE INTO memories
                  (id, content, content_fingerprint, priority, timestamp, tags, tier, scope, state, source, session_id, access_count, parent_id, depth, entropy, mass, trust)
                VALUES (?, ?, ?, ?, ?, ?, 'cortex', ?, 'active', 'consolidator', 'consolidator', 0, NULL, 0, 0.1, 1.0, 0.9)
            `).run(mergedId, summary, fp, maxPriority, Date.now(), mergedTags, scope);
            const archiveStmt = db.prepare(`UPDATE memories SET state = 'archived' WHERE id = ?`);
            for (const row of cluster) {
                archiveStmt.run(row.id);
                entriesArchived++;
            }
        })();
        memoriesPromoted++;
    }
    return { clustersProcessed: clusters.length, entriesArchived, memoriesPromoted };
}
