/**
 * Session DNA Manager
 *
 * Generates structured "Session DNA" snapshots that capture the full context
 * of a session — files accessed, decisions made, skills used, and recommended
 * next steps. This enables near-perfect context handover between sessions.
 *
 * Storage: ~/.nexus-prime/sessions/{sessionId}.json
 * Phase: 8A (no dependencies)
 */
import * as fs from 'fs';
import * as path from 'path';
import { resolveNexusStateDir } from './runtime-registry.js';
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
function isPidAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
// ─────────────────────────────────────────────────────────────────────────────
// Session DNA Manager
// ─────────────────────────────────────────────────────────────────────────────
export class SessionDNAManager {
    sessionsDir;
    sessionId;
    startTime;
    // Accumulators — fed by external calls during the session
    filesAccessedMap = new Map(); // path → access count
    filesModifiedSet = new Set();
    decisions = [];
    skillsActivated = new Set();
    skillsLearned = new Set();
    openQuestions = [];
    nextSteps = [];
    entitiesMap = new Map(); // entity → mention count
    memoriesStored = 0;
    memoriesRecalled = 0;
    totalToolCalls = 0;
    tokensOptimized = 0;
    constructor(sessionId, sessionsDir) {
        this.sessionId = sessionId;
        this.startTime = Date.now();
        this.sessionsDir = sessionsDir ?? path.join(resolveNexusStateDir(), 'sessions');
        fs.mkdirSync(this.sessionsDir, { recursive: true });
        this.writeLock();
        this.cleanStaleSessions();
    }
    /** Write a lock file for this session with PID for stale detection */
    writeLock() {
        try {
            const lockPath = path.join(this.sessionsDir, `${this.sessionId}.lock`);
            fs.writeFileSync(lockPath, JSON.stringify({
                pid: process.pid,
                sessionId: this.sessionId,
                startedAt: this.startTime,
            }));
        }
        catch { /* best-effort */ }
    }
    /** Remove the lock file for this session */
    removeLock() {
        try {
            const lockPath = path.join(this.sessionsDir, `${this.sessionId}.lock`);
            if (fs.existsSync(lockPath))
                fs.unlinkSync(lockPath);
        }
        catch { /* best-effort */ }
    }
    /** Clean up stale sessions from dead processes */
    cleanStaleSessions() {
        try {
            const files = fs.readdirSync(this.sessionsDir);
            const lockFiles = files.filter(f => f.endsWith('.lock'));
            const now = Date.now();
            const STALE_THRESHOLD = 24 * 60 * 60 * 1000; // 24 hours
            for (const lockFile of lockFiles) {
                const lockPath = path.join(this.sessionsDir, lockFile);
                try {
                    const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
                    const isAlive = isPidAlive(lock.pid);
                    const isOld = now - (lock.startedAt ?? 0) > STALE_THRESHOLD;
                    if (!isAlive || isOld) {
                        // Remove lock
                        fs.unlinkSync(lockPath);
                        // Remove corresponding session JSON
                        const sessionFile = lockFile.replace('.lock', '.json');
                        const sessionPath = path.join(this.sessionsDir, sessionFile);
                        if (fs.existsSync(sessionPath)) {
                            fs.unlinkSync(sessionPath);
                        }
                    }
                }
                catch {
                    // Corrupted lock file - remove it
                    try {
                        fs.unlinkSync(lockPath);
                    }
                    catch { /* ignore */ }
                }
            }
            // Also clean orphaned session JSONs older than 24h with no lock
            const jsonFiles = files.filter(f => f.endsWith('.json') && !f.endsWith('.lock'));
            for (const jsonFile of jsonFiles) {
                const lockExists = fs.existsSync(path.join(this.sessionsDir, jsonFile.replace('.json', '.lock')));
                if (!lockExists) {
                    const jsonPath = path.join(this.sessionsDir, jsonFile);
                    try {
                        const stat = fs.statSync(jsonPath);
                        if (now - stat.mtimeMs > STALE_THRESHOLD) {
                            fs.unlinkSync(jsonPath);
                        }
                    }
                    catch { /* ignore */ }
                }
            }
        }
        catch { /* best-effort cleanup */ }
    }
    /** Static cleanup for use from CLI commands */
    static cleanStale(sessionsDir) {
        const dir = sessionsDir ?? path.join(resolveNexusStateDir(), 'sessions');
        if (!fs.existsSync(dir))
            return 0;
        let cleaned = 0;
        const files = fs.readdirSync(dir);
        const now = Date.now();
        const STALE_THRESHOLD = 24 * 60 * 60 * 1000;
        for (const lockFile of files.filter(f => f.endsWith('.lock'))) {
            try {
                const lock = JSON.parse(fs.readFileSync(path.join(dir, lockFile), 'utf8'));
                if (!isPidAlive(lock.pid) || now - (lock.startedAt ?? 0) > STALE_THRESHOLD) {
                    fs.unlinkSync(path.join(dir, lockFile));
                    const sessionPath = path.join(dir, lockFile.replace('.lock', '.json'));
                    if (fs.existsSync(sessionPath)) {
                        fs.unlinkSync(sessionPath);
                        cleaned++;
                    }
                }
            }
            catch {
                try {
                    fs.unlinkSync(path.join(dir, lockFile));
                }
                catch { /* ignore */ }
            }
        }
        for (const jsonFile of files.filter(f => f.endsWith('.json'))) {
            if (!fs.existsSync(path.join(dir, jsonFile.replace('.json', '.lock')))) {
                try {
                    const stat = fs.statSync(path.join(dir, jsonFile));
                    if (now - stat.mtimeMs > STALE_THRESHOLD) {
                        fs.unlinkSync(path.join(dir, jsonFile));
                        cleaned++;
                    }
                }
                catch { /* ignore */ }
            }
        }
        return cleaned;
    }
    getSessionId() {
        return this.sessionId;
    }
    // ── Accumulation API ───────────────────────────────────────────────────
    /** Record a file being read/accessed */
    recordFileAccess(filePath) {
        const count = this.filesAccessedMap.get(filePath) ?? 0;
        this.filesAccessedMap.set(filePath, count + 1);
    }
    /** Record a file being modified */
    recordFileModified(filePath) {
        this.filesModifiedSet.add(filePath);
    }
    /** Record a decision made during the session */
    recordDecision(description, rationale, confidence = 0.8) {
        this.decisions.push({ description, rationale, confidence });
    }
    /** Record a skill being activated */
    recordSkill(skillName) {
        this.skillsActivated.add(skillName);
    }
    /** Record a skill learned during the session */
    recordSkillLearned(skillName) {
        this.skillsLearned.add(skillName);
    }
    /** Record an entity being referenced */
    recordEntity(entity) {
        const count = this.entitiesMap.get(entity) ?? 0;
        this.entitiesMap.set(entity, count + 1);
    }
    /** Record a tool call (for handover score) */
    recordToolCall() {
        this.totalToolCalls++;
    }
    /** Record a memory store event */
    recordMemoryStore() {
        this.memoriesStored++;
    }
    /** Record a memory recall event */
    recordMemoryRecall() {
        this.memoriesRecalled++;
    }
    /** Add an open question for the next session */
    addOpenQuestion(question) {
        this.openQuestions.push(question);
    }
    /** Add a recommended next step */
    addNextStep(step) {
        this.nextSteps.push(step);
    }
    // ── Bulk update from telemetry ─────────────────────────────────────────
    syncFromTelemetry(telemetry) {
        this.totalToolCalls = Math.max(this.totalToolCalls, telemetry.callCount);
        this.memoriesStored = Math.max(this.memoriesStored, telemetry.memoriesStored);
        this.memoriesRecalled = Math.max(this.memoriesRecalled, telemetry.memoriesRecalled);
        this.tokensOptimized = Math.max(this.tokensOptimized, telemetry.tokensOptimized ?? 0);
    }
    // ── Generation ─────────────────────────────────────────────────────────
    /** Generate the SessionDNA snapshot */
    generate() {
        const duration = Math.round((Date.now() - this.startTime) / 1000);
        // Sort files by access frequency (most accessed first)
        const filesAccessed = [...this.filesAccessedMap.entries()]
            .sort((a, b) => b[1] - a[1])
            .map(([filePath]) => filePath);
        // Top entities by mention count (max 10)
        const topEntities = [...this.entitiesMap.entries()]
            .sort((a, b) => b[1] - a[1])
            .slice(0, 10)
            .map(([entity]) => entity);
        // Handover score: (memoriesStored + decisions.length) / totalToolCalls
        const numerator = this.memoriesStored + this.decisions.length;
        const handoverScore = this.totalToolCalls > 0
            ? Math.min(1.0, numerator / this.totalToolCalls)
            : 0.0;
        return {
            sessionId: this.sessionId,
            timestamp: Date.now(),
            duration,
            filesAccessed,
            filesModified: [...this.filesModifiedSet],
            memoriesStored: this.memoriesStored,
            memoriesRecalled: this.memoriesRecalled,
            topEntities,
            decisions: this.decisions,
            skillsActivated: [...this.skillsActivated],
            skillsLearned: [...this.skillsLearned],
            openQuestions: this.openQuestions,
            nextSteps: this.nextSteps,
            handoverScore,
            tokensOptimized: this.tokensOptimized,
        };
    }
    // ── Persistence ────────────────────────────────────────────────────────
    /** Save the current SessionDNA to disk and release the session lock */
    flush() {
        const dna = this.generate();
        const filePath = path.join(this.sessionsDir, `${this.sessionId}.json`);
        fs.writeFileSync(filePath, JSON.stringify(dna, null, 2));
        this.removeLock();
        return dna;
    }
    /** Load the most recent SessionDNA from disk */
    static loadLatest(sessionsDir) {
        const dir = sessionsDir ?? path.join(resolveNexusStateDir(), 'sessions');
        if (!fs.existsSync(dir))
            return null;
        const files = fs.readdirSync(dir)
            .filter(f => f.endsWith('.json'))
            .map(f => ({
            name: f,
            mtime: fs.statSync(path.join(dir, f)).mtimeMs,
        }))
            .sort((a, b) => b.mtime - a.mtime);
        if (files.length === 0)
            return null;
        try {
            const raw = fs.readFileSync(path.join(dir, files[0].name), 'utf-8');
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    }
    /** Load a specific session by ID */
    static loadById(sessionId, sessionsDir) {
        const dir = sessionsDir ?? path.join(resolveNexusStateDir(), 'sessions');
        const filePath = path.join(dir, `${sessionId}.json`);
        if (!fs.existsSync(filePath))
            return null;
        try {
            const raw = fs.readFileSync(filePath, 'utf-8');
            return JSON.parse(raw);
        }
        catch {
            return null;
        }
    }
    static listSnapshots(sessionsDir) {
        return SessionDnaSearchSidecar.listSnapshots(sessionsDir);
    }
    static searchSnapshots(query, options = {}) {
        return SessionDnaSearchSidecar.search(query, options);
    }
    // ── Formatting ─────────────────────────────────────────────────────────
    /** Format SessionDNA for MCP response */
    static format(dna) {
        const lines = [
            `📊 Session DNA: ${dna.sessionId.slice(0, 8)}`,
            `Duration: ${formatDuration(dna.duration)} │ Handover Score: ${(dna.handoverScore * 100).toFixed(0)}%`,
            '',
        ];
        if (dna.filesModified.length > 0) {
            lines.push(`📝 Files Modified: ${dna.filesModified.length}`);
            for (const f of dna.filesModified.slice(0, 5)) {
                lines.push(`  • ${path.basename(f)}`);
            }
            if (dna.filesModified.length > 5) {
                lines.push(`  ... and ${dna.filesModified.length - 5} more`);
            }
            lines.push('');
        }
        if (dna.filesAccessed.length > 0) {
            lines.push(`👁️ Top Files Accessed: ${Math.min(dna.filesAccessed.length, 5)}`);
            for (const f of dna.filesAccessed.slice(0, 5)) {
                lines.push(`  • ${path.basename(f)}`);
            }
            lines.push('');
        }
        lines.push(`🧠 Memory: ${dna.memoriesStored} stored, ${dna.memoriesRecalled} recalled`);
        if (dna.tokensOptimized && dna.tokensOptimized > 0) {
            lines.push(`🪙 Tokens Optimally Saved: ${dna.tokensOptimized.toLocaleString()}`);
        }
        if (dna.decisions.length > 0) {
            lines.push('');
            lines.push(`📌 Decisions Made: ${dna.decisions.length}`);
            for (const d of dna.decisions.slice(0, 3)) {
                lines.push(`  • ${d.description} (confidence: ${(d.confidence * 100).toFixed(0)}%)`);
            }
        }
        if (dna.topEntities.length > 0) {
            lines.push('');
            lines.push(`🔗 Top Entities: ${dna.topEntities.slice(0, 5).join(', ')}`);
        }
        if (dna.nextSteps.length > 0) {
            lines.push('');
            lines.push('➡️ Recommended Next Steps:');
            for (const step of dna.nextSteps.slice(0, 5)) {
                lines.push(`  ${step}`);
            }
        }
        if (dna.openQuestions.length > 0) {
            lines.push('');
            lines.push('❓ Open Questions:');
            for (const q of dna.openQuestions.slice(0, 3)) {
                lines.push(`  ${q}`);
            }
        }
        return lines.join('\n');
    }
}
// ── Helpers ────────────────────────────────────────────────────────────────
function formatDuration(seconds) {
    if (seconds < 60)
        return `${seconds}s`;
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    if (mins < 60)
        return `${mins}m ${secs}s`;
    const hrs = Math.floor(mins / 60);
    return `${hrs}h ${mins % 60}m`;
}
class SessionDnaSearchSidecar {
    static listSnapshots(sessionsDir) {
        const dir = sessionsDir ?? path.join(resolveNexusStateDir(), 'sessions');
        if (!fs.existsSync(dir))
            return [];
        return fs.readdirSync(dir)
            .filter((fileName) => fileName.endsWith('.json'))
            .map((fileName) => {
            const sessionId = fileName.slice(0, -'.json'.length);
            const session = SessionDNAManager.loadById(sessionId, dir);
            if (!session)
                return null;
            return {
                path: path.join(dir, fileName),
                session,
            };
        })
            .filter((record) => Boolean(record))
            .sort((left, right) => {
            const timestampDelta = right.session.timestamp - left.session.timestamp;
            if (timestampDelta !== 0)
                return timestampDelta;
            return right.session.sessionId.localeCompare(left.session.sessionId);
        });
    }
    static search(query, options = {}) {
        const normalizedQuery = normalizeSearchText(query);
        const terms = tokenizeSearchText(query);
        if (!normalizedQuery && terms.length === 0)
            return [];
        const limit = Math.max(1, Math.floor(options.limit ?? 5));
        const minScore = Number(options.minScore ?? 0);
        return this.listSnapshots(options.sessionsDir)
            .map((record) => scoreSessionSnapshot(record, normalizedQuery, terms))
            .filter((hit) => hit.score >= minScore)
            .sort((left, right) => {
            const scoreDelta = right.score - left.score;
            if (scoreDelta !== 0)
                return scoreDelta;
            const timestampDelta = right.session.timestamp - left.session.timestamp;
            if (timestampDelta !== 0)
                return timestampDelta;
            return right.session.sessionId.localeCompare(left.session.sessionId);
        })
            .slice(0, limit);
    }
}
function normalizeSearchText(value) {
    return String(value ?? '')
        .toLowerCase()
        .replace(/\s+/g, ' ')
        .trim();
}
function tokenizeSearchText(value) {
    return Array.from(new Set(normalizeSearchText(value)
        .match(/[a-z0-9#:_/.-]+/g)
        ?.filter((term) => term.length >= 2) ?? []));
}
function scoreSessionSnapshot(record, normalizedQuery, terms) {
    const matchedFields = new Set();
    let score = 0;
    const session = record.session;
    score += scoreField('sessionId', session.sessionId, normalizedQuery, terms, 7, matchedFields);
    score += scoreField('filesAccessed', session.filesAccessed.join(' '), normalizedQuery, terms, 3, matchedFields);
    score += scoreField('filesModified', session.filesModified.join(' '), normalizedQuery, terms, 3, matchedFields);
    score += scoreField('decisions', session.decisions.map((decision) => `${decision.description} ${decision.rationale}`).join(' '), normalizedQuery, terms, 4, matchedFields);
    score += scoreField('skillsActivated', session.skillsActivated.join(' '), normalizedQuery, terms, 2, matchedFields);
    score += scoreField('skillsLearned', session.skillsLearned.join(' '), normalizedQuery, terms, 2.5, matchedFields);
    score += scoreField('topEntities', session.topEntities.join(' '), normalizedQuery, terms, 2.5, matchedFields);
    score += scoreField('openQuestions', session.openQuestions.join(' '), normalizedQuery, terms, 1.5, matchedFields);
    score += scoreField('nextSteps', session.nextSteps.join(' '), normalizedQuery, terms, 1.5, matchedFields);
    if (normalizedQuery && JSON.stringify(session).toLowerCase().includes(normalizedQuery)) {
        score += 1;
        matchedFields.add('snapshot');
    }
    const excerpt = buildSessionExcerpt(session, Array.from(matchedFields));
    return {
        ...record,
        score,
        matchedFields: Array.from(matchedFields),
        excerpt,
    };
}
function scoreField(fieldName, value, normalizedQuery, terms, weight, matchedFields) {
    const text = normalizeSearchText(value);
    if (!text)
        return 0;
    let score = 0;
    if (normalizedQuery && text.includes(normalizedQuery)) {
        score += weight * 2;
        matchedFields.add(fieldName);
    }
    let matched = false;
    for (const term of terms) {
        if (text.includes(term)) {
            score += weight;
            matched = true;
        }
    }
    if (matched) {
        matchedFields.add(fieldName);
    }
    return score;
}
function buildSessionExcerpt(session, matchedFields) {
    const snippets = [];
    if (matchedFields.includes('sessionId')) {
        snippets.push(`Session ${session.sessionId}`);
    }
    if (matchedFields.includes('decisions') && session.decisions.length > 0) {
        snippets.push(`Decision: ${session.decisions[0].description}`);
    }
    if (matchedFields.includes('nextSteps') && session.nextSteps.length > 0) {
        snippets.push(`Next: ${session.nextSteps[0]}`);
    }
    if (matchedFields.includes('openQuestions') && session.openQuestions.length > 0) {
        snippets.push(`Question: ${session.openQuestions[0]}`);
    }
    if (matchedFields.includes('filesModified') && session.filesModified.length > 0) {
        snippets.push(`Modified: ${session.filesModified[0]}`);
    }
    if (matchedFields.includes('filesAccessed') && session.filesAccessed.length > 0) {
        snippets.push(`Accessed: ${session.filesAccessed[0]}`);
    }
    if (matchedFields.includes('skillsActivated') && session.skillsActivated.length > 0) {
        snippets.push(`Skills: ${session.skillsActivated[0]}`);
    }
    if (matchedFields.includes('skillsLearned') && session.skillsLearned.length > 0) {
        snippets.push(`Learned: ${session.skillsLearned[0]}`);
    }
    if (matchedFields.includes('topEntities') && session.topEntities.length > 0) {
        snippets.push(`Entities: ${session.topEntities[0]}`);
    }
    if (snippets.length === 0) {
        snippets.push(`Session ${session.sessionId} · ${session.filesModified.length} modified file(s) · ${session.decisions.length} decision(s)`);
    }
    return snippets.join(' · ').slice(0, 240);
}
