/**
 * Vigthoria Project Memory Service
 *
 * Local-first project brain stored inside .vigthoria/memory. It keeps durable,
 * compact facts that can be retrieved for follow-up prompts without replaying
 * full chat history or noisy tool traces.
 */
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
const MAX_ITEMS = 240;
const MAX_TEXT_LENGTH = 320;
const MAX_CONTEXT_ITEMS = 14;
const STOP_WORDS = new Set([
    'about', 'after', 'again', 'also', 'because', 'before', 'could', 'from', 'have', 'into', 'just', 'make', 'need',
    'only', 'please', 'should', 'that', 'their', 'there', 'this', 'with', 'work', 'working', 'would', 'your', 'vigthoria',
]);
function nowIso() {
    return new Date().toISOString();
}
function normalizeText(value) {
    return String(value || '')
        .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, ' ')
        .replace(/[ \t\r\n]+/g, ' ')
        .trim();
}
function clampText(value, max = MAX_TEXT_LENGTH) {
    const normalized = normalizeText(value);
    return normalized.length > max ? `${normalized.slice(0, max - 3)}...` : normalized;
}
function slugHash(value) {
    return createHash('sha1').update(value).digest('hex').slice(0, 12);
}
function tokenize(value) {
    return Array.from(new Set(String(value || '')
        .toLowerCase()
        .replace(/[^a-z0-9_./-]+/g, ' ')
        .split(/\s+/)
        .filter((word) => word.length >= 3 && !STOP_WORDS.has(word)))).slice(0, 48);
}
function inferTags(text) {
    const tags = new Set();
    const lower = text.toLowerCase();
    if (/login|auth|token|session/.test(lower))
        tags.add('auth');
    if (/cli|terminal|command/.test(lower))
        tags.add('cli');
    if (/code|extension|vscode|webview/.test(lower))
        tags.add('code');
    if (/agent|v3|executor|planner/.test(lower))
        tags.add('agent');
    if (/memory|context|summary|follow/.test(lower))
        tags.add('memory');
    if (/windows|win32|desktop/.test(lower))
        tags.add('windows');
    if (/release|publish|version|manifest|update/.test(lower))
        tags.add('release');
    if (/test|validated|passed|build|verify/.test(lower))
        tags.add('validation');
    return Array.from(tags).slice(0, 8);
}
export class ProjectMemoryService {
    workspacePath;
    memoryDir;
    brainPath;
    constructor(workspacePath = process.cwd()) {
        this.workspacePath = path.resolve(workspacePath || process.cwd());
        this.memoryDir = path.join(this.workspacePath, '.vigthoria', 'memory');
        this.brainPath = path.join(this.memoryDir, 'brain.json');
    }
    getMemoryDir() {
        return this.memoryDir;
    }
    loadBrain() {
        try {
            if (fs.existsSync(this.brainPath)) {
                const parsed = JSON.parse(fs.readFileSync(this.brainPath, 'utf8'));
                if (parsed && parsed.version === 1 && Array.isArray(parsed.items)) {
                    return parsed;
                }
            }
        }
        catch {
            // Corrupt memory should never break chat.
        }
        return {
            version: 1,
            workspaceName: path.basename(this.workspacePath),
            updatedAt: nowIso(),
            items: [],
        };
    }
    saveBrain(brain) {
        try {
            fs.mkdirSync(this.memoryDir, { recursive: true });
            const normalized = {
                ...brain,
                workspaceName: brain.workspaceName || path.basename(this.workspacePath),
                updatedAt: nowIso(),
                items: this.dedupeAndTrim(brain.items || []),
            };
            fs.writeFileSync(this.brainPath, JSON.stringify(normalized, null, 2) + '\n', 'utf8');
            this.writeMarkdownViews(normalized);
        }
        catch {
            // Project memory is best effort. Never fail the user's main request.
        }
    }
    remember(type, text, options = {}) {
        const clean = clampText(text);
        if (!clean || clean.length < 12)
            return;
        const brain = this.loadBrain();
        const item = this.createItem(type, clean, options);
        brain.items.push(item);
        this.saveBrain(brain);
    }
    rememberConversation(messages, options = {}) {
        const recent = (messages || []).slice(-16);
        const candidates = [];
        for (const message of recent) {
            const content = clampText(message.content);
            if (!content || content.length < 16)
                continue;
            if (message.role === 'user') {
                const lower = content.toLowerCase();
                if (/\b(need|fix|make|implement|build|verify|publish|release|check|investigate|remember|prefer|should|must|do not|don't)\b/.test(lower)) {
                    candidates.push(this.createItem(this.inferUserMemoryType(content), content, { ...options, source: options.source || 'user' }));
                }
                continue;
            }
            if (message.role === 'assistant') {
                const lower = content.toLowerCase();
                if (/\b(done|fixed|implemented|published|validated|verified|passed|updated|created|rebuilt|released)\b/.test(lower)) {
                    candidates.push(this.createItem('validation', content, { ...options, source: options.source || 'assistant' }));
                }
            }
        }
        if (options.sessionSummary && options.sessionSummary.trim()) {
            for (const line of options.sessionSummary.split('\n').slice(-10)) {
                const clean = clampText(line.replace(/^[-*]\s*/, ''));
                if (clean.length >= 16) {
                    candidates.push(this.createItem('fact', clean, { ...options, source: 'session-summary' }));
                }
            }
        }
        if (candidates.length === 0)
            return;
        const brain = this.loadBrain();
        brain.items.push(...candidates);
        this.saveBrain(brain);
    }
    buildContextForPrompt(prompt) {
        const items = this.retrieveRelevantMemory(prompt, MAX_CONTEXT_ITEMS);
        if (items.length === 0)
            return '';
        const grouped = new Map();
        for (const item of items) {
            if (!grouped.has(item.type))
                grouped.set(item.type, []);
            grouped.get(item.type).push(item);
        }
        const lines = [
            'Vigthoria project brain memory.',
            'Use these durable project facts for follow-up continuity. Do not repeat them unless relevant.',
        ];
        for (const [type, group] of grouped) {
            lines.push(`${type}:`);
            for (const item of group.slice(0, 5)) {
                lines.push(`- ${item.text}`);
            }
        }
        return lines.join('\n').slice(0, 3200);
    }
    retrieveRelevantMemory(prompt, limit = MAX_CONTEXT_ITEMS) {
        const brain = this.loadBrain();
        const promptTokens = new Set(tokenize(prompt));
        const scored = brain.items.map((item) => {
            const haystack = tokenize(`${item.type} ${item.text} ${item.tags.join(' ')}`);
            let score = item.weight || 1;
            for (const token of haystack) {
                if (promptTokens.has(token))
                    score += 4;
            }
            if (item.type === 'preference')
                score += 2;
            if (item.type === 'task')
                score += 1;
            return { item, score };
        });
        return scored
            .filter((entry) => entry.score > 1 || promptTokens.size === 0)
            .sort((a, b) => b.score - a.score || b.item.updatedAt.localeCompare(a.item.updatedAt))
            .slice(0, limit)
            .map((entry) => entry.item);
    }
    getStatus() {
        const brain = this.loadBrain();
        const typeCounts = {};
        for (const item of brain.items) {
            typeCounts[item.type] = (typeCounts[item.type] || 0) + 1;
        }
        return {
            memoryDir: this.memoryDir,
            itemCount: brain.items.length,
            updatedAt: brain.updatedAt,
            typeCounts,
        };
    }
    createItem(type, text, options) {
        const clean = clampText(text);
        const tags = Array.from(new Set([...inferTags(clean), ...(options.mode ? [options.mode] : [])])).slice(0, 10);
        const source = options.source || 'conversation';
        const id = slugHash(`${type}:${source}:${clean.toLowerCase()}`);
        const timestamp = nowIso();
        return {
            id,
            type,
            text: clean,
            tags,
            source,
            createdAt: timestamp,
            updatedAt: timestamp,
            weight: type === 'preference' || type === 'decision' ? 4 : type === 'task' ? 3 : 2,
        };
    }
    inferUserMemoryType(text) {
        const lower = text.toLowerCase();
        if (/\b(prefer|should|must|always|never|do not|don't|dont)\b/.test(lower))
            return 'preference';
        if (/\b(decide|decision|architecture|approach)\b/.test(lower))
            return 'decision';
        if (/\b(error|bug|issue|problem|fail|broken)\b/.test(lower))
            return 'issue';
        return 'task';
    }
    dedupeAndTrim(items) {
        const byId = new Map();
        for (const item of items) {
            const clean = clampText(item.text);
            if (!clean)
                continue;
            const id = item.id || slugHash(`${item.type}:${clean.toLowerCase()}`);
            const existing = byId.get(id);
            if (existing) {
                byId.set(id, {
                    ...existing,
                    text: clean,
                    tags: Array.from(new Set([...(existing.tags || []), ...(item.tags || [])])).slice(0, 10),
                    updatedAt: item.updatedAt || nowIso(),
                    weight: Math.max(existing.weight || 1, item.weight || 1),
                });
            }
            else {
                byId.set(id, { ...item, id, text: clean });
            }
        }
        return Array.from(byId.values())
            .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
            .slice(0, MAX_ITEMS)
            .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
    }
    writeMarkdownViews(brain) {
        const groups = ['preference', 'decision', 'task', 'issue', 'validation', 'file', 'fact'];
        const title = `# Vigthoria Project Brain\n\nUpdated: ${brain.updatedAt}\nWorkspace: ${brain.workspaceName}\n\n`;
        const allLines = [title.trimEnd(), ''];
        for (const type of groups) {
            const items = brain.items.filter((item) => item.type === type);
            if (items.length === 0)
                continue;
            const heading = this.headingForType(type);
            const lines = [`# ${heading}`, '', ...items.map((item) => `- ${item.text}`)];
            fs.writeFileSync(path.join(this.memoryDir, `${type}s.md`), lines.join('\n') + '\n', 'utf8');
            allLines.push(`## ${heading}`, '', ...items.slice(-40).map((item) => `- ${item.text}`), '');
        }
        fs.writeFileSync(path.join(this.memoryDir, 'README.md'), allLines.join('\n').trimEnd() + '\n', 'utf8');
    }
    headingForType(type) {
        switch (type) {
            case 'preference': return 'User Preferences';
            case 'decision': return 'Decisions';
            case 'task': return 'Tasks';
            case 'issue': return 'Known Issues';
            case 'validation': return 'Validations';
            case 'file': return 'Important Files';
            default: return 'Facts';
        }
    }
}
