/**
 * Memory Manager - Persistent user memory across sessions.
 *
 * Stores memories in ~/.avis/self/memory/memory.jsonl as an append-only log.
 * Supports create, update, delete, and list operations.
 * Memories are injected into the system prompt so the agent has context
 * about user preferences, past decisions, and behavioral patterns.
 */
import { randomUUID } from "node:crypto";
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, watch } from "node:fs";
import { dirname, join } from "node:path";
import { getConversationBufferPath, getMemoryPath } from "../config.js";
import { containerClient } from "./container-client.js";
import { ConversationBuffer, DEFAULT_BUFFER_SIZE } from "./conversation-buffer.js";
import { MemoryGraph } from "./memory-graph.js";
import { vault } from "./vault.js";
// ============================================================================
// MemoryManager
// ============================================================================
export class MemoryManager {
    constructor(filePath, options) {
        this._memories = new Map();
        this._loaded = false;
        this._graph = null;
        this._graphDirty = false;
        /** Track file mtime to detect external changes */
        this._lastMtime = 0;
        /** File watcher for real-time sync across instances */
        this._watcher = null;
        /** Debounce timer for file change events */
        this._reloadTimer = null;
        /** Cached container context block (refreshed periodically) */
        this._containerContext = "";
        this._containerContextAge = 0;
        this._containerAlive = false;
        this._filePath = filePath ?? getMemoryPath();
        const bufferPath = filePath
            ? join(dirname(this._filePath), "conversation-buffer.json")
            : getConversationBufferPath();
        this._conversationBuffer = new ConversationBuffer({
            filePath: bufferPath,
            maxChars: options?.bufferSize ?? DEFAULT_BUFFER_SIZE,
        });
        this._startWatching();
    }
    /** Watch memory.jsonl for external changes (other avis instances, daemon, etc.) */
    _startWatching() {
        try {
            const dir = join(this._filePath, "..");
            if (!existsSync(dir))
                mkdirSync(dir, { recursive: true });
            // Watch the directory since the file may not exist yet
            this._watcher = watch(dir, (_eventType, filename) => {
                if (filename && !filename.endsWith("memory.jsonl"))
                    return;
                // Debounce rapid changes
                if (this._reloadTimer)
                    clearTimeout(this._reloadTimer);
                this._reloadTimer = setTimeout(() => {
                    this._checkAndReload();
                }, 200);
            });
        }
        catch {
            // Watching is best-effort
        }
    }
    /** Check if file changed externally and reload if needed */
    _checkAndReload() {
        try {
            if (!existsSync(this._filePath))
                return;
            const mtime = statSync(this._filePath).mtimeMs;
            if (mtime > this._lastMtime) {
                this._loaded = false;
                this._ensureLoaded();
                this._graphDirty = true;
            }
        }
        catch {
            // Best-effort
        }
    }
    /** Stop watching for changes (cleanup) */
    stopWatching() {
        if (this._watcher) {
            this._watcher.close();
            this._watcher = null;
        }
        if (this._reloadTimer) {
            clearTimeout(this._reloadTimer);
            this._reloadTimer = null;
        }
    }
    /** Get the conversation buffer (Tier 1 memory) */
    get conversationBuffer() {
        return this._conversationBuffer;
    }
    /** Get or create the memory graph */
    get graph() {
        if (!this._graph) {
            this._graph = new MemoryGraph(this);
        }
        return this._graph;
    }
    get filePath() {
        return this._filePath;
    }
    /** Lazy-load memories from disk */
    _ensureLoaded() {
        if (this._loaded)
            return;
        this._loaded = true;
        this._memories.clear();
        if (!existsSync(this._filePath))
            return;
        // Track mtime for external change detection
        try {
            this._lastMtime = statSync(this._filePath).mtimeMs;
        }
        catch {
            /* */
        }
        const content = vault.ready ? vault.readFileSync(this._filePath) : readFileSync(this._filePath, "utf-8");
        for (const line of content.split("\n")) {
            if (!line.trim())
                continue;
            try {
                const entry = JSON.parse(line);
                if (entry.action === "delete") {
                    this._memories.delete(entry.memory.id);
                }
                else {
                    this._memories.set(entry.memory.id, entry.memory);
                }
            }
            catch {
                // skip malformed lines
            }
        }
    }
    _append(entry) {
        const dir = join(this._filePath, "..");
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true });
        }
        if (vault.ready) {
            vault.appendFileSync(this._filePath, `${JSON.stringify(entry)}\n`);
        }
        else {
            appendFileSync(this._filePath, `${JSON.stringify(entry)}\n`);
        }
    }
    /** Get all active memories */
    list() {
        this._ensureLoaded();
        return Array.from(this._memories.values()).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
    }
    /** Search memories by content substring or tag */
    search(query) {
        const q = query.toLowerCase();
        return this.list().filter((m) => m.content.toLowerCase().includes(q) || m.tags.some((t) => t.toLowerCase().includes(q)));
    }
    /** Get a single memory by ID */
    get(id) {
        this._ensureLoaded();
        return this._memories.get(id);
    }
    /** Create a new memory. Returns the created memory. */
    create(content, tags = []) {
        this._ensureLoaded();
        // Check for duplicates (exact content match)
        for (const existing of this._memories.values()) {
            if (existing.content === content) {
                return existing;
            }
        }
        const now = new Date().toISOString();
        const memory = {
            id: randomUUID().slice(0, 8),
            content,
            tags,
            createdAt: now,
            updatedAt: now,
        };
        this._memories.set(memory.id, memory);
        this._append({ action: "create", memory, timestamp: now });
        this._graphDirty = true;
        return memory;
    }
    /** Update an existing memory. Returns the updated memory or undefined if not found. */
    update(id, content, tags) {
        this._ensureLoaded();
        const existing = this._memories.get(id);
        if (!existing)
            return undefined;
        const now = new Date().toISOString();
        const updated = {
            ...existing,
            content: content ?? existing.content,
            tags: tags ?? existing.tags,
            updatedAt: now,
        };
        this._memories.set(id, updated);
        this._append({ action: "update", memory: updated, timestamp: now });
        this._graphDirty = true;
        return updated;
    }
    /** Delete a memory. Returns true if found and deleted. */
    delete(id) {
        this._ensureLoaded();
        const existing = this._memories.get(id);
        if (!existing)
            return false;
        this._memories.delete(id);
        this._append({ action: "delete", memory: existing, timestamp: new Date().toISOString() });
        this._graphDirty = true;
        return true;
    }
    /** Delete all memories */
    clear() {
        this._ensureLoaded();
        const count = this._memories.size;
        for (const memory of this._memories.values()) {
            this._append({ action: "delete", memory, timestamp: new Date().toISOString() });
        }
        this._memories.clear();
        return count;
    }
    /** Get the count of active memories */
    get count() {
        this._ensureLoaded();
        return this._memories.size;
    }
    /**
     * Format memories for injection into the system prompt.
     * If graph is available and built, uses graph-based formatting with god nodes
     * and community context. Otherwise falls back to flat list.
     * Returns empty string if no memories exist.
     */
    formatForSystemPrompt() {
        const memories = this.list();
        const bufferBlock = this._conversationBuffer.formatForSystemPrompt();
        // Trigger async container context refresh (non-blocking)
        this._refreshContainerContext();
        if (memories.length === 0 && !bufferBlock)
            return "";
        let memoryBlock = "";
        if (memories.length > 0) {
            // If graph is available, include god nodes and community context
            if (this._graph && !this._graph.needsRebuild()) {
                const stats = this._graph.getStats();
                const sections = ["\n\n# User Memory"];
                sections.push("\nThings you remember about the user from past interactions. Use these to personalize responses.");
                if (stats.godNodes.length > 0) {
                    sections.push(`\nKey themes: ${stats.godNodes.join(", ")}`);
                }
                sections.push("");
                for (const m of memories) {
                    const tagStr = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
                    sections.push(`- ${m.content}${tagStr}`);
                }
                memoryBlock = sections.join("\n");
            }
            else {
                // Flat fallback
                const lines = memories.map((m) => {
                    const tagStr = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
                    return `- ${m.content}${tagStr}`;
                });
                memoryBlock = `\n\n# User Memory\n\nThings you remember about the user from past interactions. Use these to personalize responses:\n\n${lines.join("\n")}`;
            }
        }
        // Tier 1 (verbatim buffer) comes after Tier 2 (compressed memories)
        // Container context comes last (persistent brain)
        return memoryBlock + bufferBlock + this._containerContext;
    }
    /** Non-blocking refresh of container context. Caches for 60s. */
    _refreshContainerContext() {
        const now = Date.now();
        if (now - this._containerContextAge < 60_000)
            return;
        this._containerContextAge = now;
        // Fire-and-forget async fetch
        this._fetchContainerContext().catch(() => { });
    }
    /** Fetch key memories from the container daemon and format as prompt block. */
    async _fetchContainerContext() {
        const alive = await containerClient.isAlive();
        this._containerAlive = alive;
        if (!alive) {
            this._containerContext = "";
            return;
        }
        const [identity, project, preference, system] = await Promise.all([
            containerClient.queryMemories({ category: "identity", limit: 10 }),
            containerClient.queryMemories({ category: "project", limit: 10 }),
            containerClient.queryMemories({ category: "preference", limit: 10 }),
            containerClient.queryMemories({ category: "system", limit: 5 }),
        ]);
        const all = [...identity, ...project, ...preference, ...system];
        if (all.length === 0) {
            this._containerContext = "";
            return;
        }
        const lines = all.map((m) => `- ${m.content}`);
        this._containerContext = `\n\n# Persistent Memory (Container)\n\nFrom your persistent brain running at localhost:7777:\n\n${lines.join("\n")}`;
    }
    /** Whether the container daemon is reachable. */
    get isContainerAlive() {
        return this._containerAlive;
    }
    /** Store a memory in both the local file and the container. */
    async storeToContainer(content, category = "general", tags = []) {
        await containerClient.storeMemory(content, category, tags, "agent");
    }
    /**
     * Retrieve memories relevant to a specific query using graph traversal.
     * Falls back to text search if graph is not available.
     * @param query The user's message or search query
     * @param maxResults Maximum number of memories to return
     */
    retrieveRelevant(query, maxResults = 10) {
        return this.graph.retrieve(query, maxResults);
    }
    /**
     * Format context-aware memories for the system prompt.
     * Uses graph traversal to find only memories relevant to the current query.
     * Falls back to formatForSystemPrompt() if graph is not available.
     */
    formatRelevantForPrompt(query, maxResults = 10) {
        if (this.count === 0)
            return "";
        // If we have a working graph, use it for contextual retrieval
        if (this._graph && !this._graph.needsRebuild()) {
            const result = this.graph.retrieve(query, maxResults);
            return this.graph.formatRetrievalForPrompt(result);
        }
        // Fallback to full list
        return this.formatForSystemPrompt();
    }
    /**
     * Rebuild the memory graph if needed.
     * Call this after batch memory operations or periodically.
     * Returns stats if rebuild happened, null if not needed.
     */
    async rebuildGraphIfNeeded() {
        if (!this._graphDirty && !this.graph.needsRebuild())
            return null;
        try {
            const result = await this.graph.build();
            this._graphDirty = false;
            return result;
        }
        catch {
            return null;
        }
    }
    /** Force reload from disk */
    reload() {
        this._loaded = false;
        this._graphDirty = true;
        this._ensureLoaded();
    }
}
//# sourceMappingURL=memory-manager.js.map