/**
 * Semantic Trie - Fast prefix-based + inverted index for memory retrieval.
 *
 * Provides O(prefix_len) lookup for concepts/tags and O(1) inverted index
 * from terms to memory IDs. Persisted to disk as JSON for cross-session reuse.
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { vault } from "./vault.js";
// ============================================================================
// SemanticTrie
// ============================================================================
export class SemanticTrie {
    constructor(persistDir) {
        this._dirty = false;
        this._root = this._newNode();
        this._invertedIndex = {
            terms: new Map(),
            tags: new Map(),
            memoryTerms: new Map(),
        };
        this._persistPath = persistDir ? join(persistDir, "semantic-trie.json") : null;
        this._loadFromDisk();
    }
    _newNode() {
        return { children: {}, memoryIds: new Set(), terminal: false };
    }
    // =========================================================================
    // Core operations
    // =========================================================================
    /** Index a memory by its extracted terms and tags */
    index(memoryId, terms, tags) {
        const allTerms = new Set();
        for (const term of terms) {
            const normalized = term.toLowerCase().trim();
            if (normalized.length < 2)
                continue;
            this._insertTrie(normalized, memoryId);
            this._addToInverted(this._invertedIndex.terms, normalized, memoryId);
            allTerms.add(normalized);
        }
        for (const tag of tags) {
            const normalized = tag.toLowerCase().trim();
            if (!normalized)
                continue;
            this._addToInverted(this._invertedIndex.tags, normalized, memoryId);
            // Also insert tag into trie for prefix search
            this._insertTrie(`tag:${normalized}`, memoryId);
            allTerms.add(`tag:${normalized}`);
        }
        this._invertedIndex.memoryTerms.set(memoryId, allTerms);
        this._dirty = true;
    }
    /** Remove a memory from all indices */
    remove(memoryId) {
        const terms = this._invertedIndex.memoryTerms.get(memoryId);
        if (!terms)
            return;
        for (const term of terms) {
            if (term.startsWith("tag:")) {
                const tag = term.slice(4);
                this._invertedIndex.tags.get(tag)?.delete(memoryId);
            }
            else {
                this._invertedIndex.terms.get(term)?.delete(memoryId);
            }
            this._removeFromTrie(term, memoryId);
        }
        this._invertedIndex.memoryTerms.delete(memoryId);
        this._dirty = true;
    }
    /** Find memory IDs matching a query using prefix + exact match */
    search(query, maxResults = 20) {
        const queryTerms = this._tokenize(query);
        const scores = new Map();
        for (const term of queryTerms) {
            // Exact match in inverted index (fast)
            const exactMatches = this._invertedIndex.terms.get(term);
            if (exactMatches) {
                for (const id of exactMatches) {
                    scores.set(id, (scores.get(id) ?? 0) + 3);
                }
            }
            // Tag match
            const tagMatches = this._invertedIndex.tags.get(term);
            if (tagMatches) {
                for (const id of tagMatches) {
                    scores.set(id, (scores.get(id) ?? 0) + 5);
                }
            }
            // Prefix match in trie (finds memories indexed under terms starting with this prefix)
            if (term.length >= 3) {
                const prefixMatches = this._prefixSearch(term);
                for (const id of prefixMatches) {
                    scores.set(id, (scores.get(id) ?? 0) + 1);
                }
            }
        }
        return Array.from(scores.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, maxResults)
            .map(([memoryId, score]) => ({ memoryId, score }));
    }
    /** Get all memory IDs indexed under a specific tag */
    getByTag(tag) {
        return this._invertedIndex.tags.get(tag.toLowerCase()) ?? new Set();
    }
    /** Get stats */
    get stats() {
        return {
            termCount: this._invertedIndex.terms.size,
            tagCount: this._invertedIndex.tags.size,
            indexedMemories: this._invertedIndex.memoryTerms.size,
        };
    }
    // =========================================================================
    // Trie operations
    // =========================================================================
    _insertTrie(term, memoryId) {
        let node = this._root;
        for (const ch of term) {
            if (!node.children[ch])
                node.children[ch] = this._newNode();
            node = node.children[ch];
        }
        node.terminal = true;
        node.memoryIds.add(memoryId);
    }
    _removeFromTrie(term, memoryId) {
        let node = this._root;
        for (const ch of term) {
            if (!node.children[ch])
                return;
            node = node.children[ch];
        }
        node.memoryIds.delete(memoryId);
    }
    _prefixSearch(prefix) {
        let node = this._root;
        for (const ch of prefix) {
            if (!node.children[ch])
                return new Set();
            node = node.children[ch];
        }
        // Collect all memory IDs in subtree
        const result = new Set();
        this._collectMemoryIds(node, result, 100); // cap to avoid huge traversals
        return result;
    }
    _collectMemoryIds(node, result, limit) {
        if (result.size >= limit)
            return;
        for (const id of node.memoryIds) {
            result.add(id);
            if (result.size >= limit)
                return;
        }
        for (const child of Object.values(node.children)) {
            this._collectMemoryIds(child, result, limit);
            if (result.size >= limit)
                return;
        }
    }
    // =========================================================================
    // Inverted index helpers
    // =========================================================================
    _addToInverted(map, key, memoryId) {
        let set = map.get(key);
        if (!set) {
            set = new Set();
            map.set(key, set);
        }
        set.add(memoryId);
    }
    _tokenize(text) {
        const stopwords = new Set([
            "the",
            "this",
            "that",
            "with",
            "from",
            "have",
            "been",
            "were",
            "will",
            "would",
            "could",
            "should",
            "their",
            "there",
            "they",
            "them",
            "then",
            "than",
            "when",
            "what",
            "which",
            "where",
            "while",
            "about",
            "also",
            "into",
            "over",
            "after",
            "before",
            "between",
            "through",
            "during",
            "each",
            "some",
            "most",
            "more",
            "very",
            "just",
            "only",
            "like",
        ]);
        return text
            .toLowerCase()
            .split(/[\s,.;:!?()[\]{}'"]+/)
            .filter((t) => t.length > 2 && !stopwords.has(t));
    }
    // =========================================================================
    // Persistence
    // =========================================================================
    /** Save to disk if dirty */
    flush() {
        if (!this._dirty || !this._persistPath)
            return;
        try {
            const dir = join(this._persistPath, "..");
            if (!existsSync(dir))
                mkdirSync(dir, { recursive: true });
            const data = {
                trie: this._serializeTrieNode(this._root),
                terms: this._serializeMap(this._invertedIndex.terms),
                tags: this._serializeMap(this._invertedIndex.tags),
                memoryTerms: this._serializeMap(this._invertedIndex.memoryTerms),
            };
            const content = JSON.stringify(data);
            if (vault.ready) {
                vault.writeFileSync(this._persistPath, content);
            }
            else {
                writeFileSync(this._persistPath, content);
            }
            this._dirty = false;
        }
        catch {
            // best-effort
        }
    }
    _loadFromDisk() {
        if (!this._persistPath || !existsSync(this._persistPath))
            return;
        try {
            const content = vault.ready ? vault.readFileSync(this._persistPath) : readFileSync(this._persistPath, "utf-8");
            const raw = JSON.parse(content);
            this._root = this._deserializeTrieNode(raw.trie);
            this._invertedIndex.terms = this._deserializeMap(raw.terms);
            this._invertedIndex.tags = this._deserializeMap(raw.tags);
            this._invertedIndex.memoryTerms = this._deserializeMap(raw.memoryTerms);
        }
        catch {
            // corrupted, start fresh
        }
    }
    _serializeTrieNode(node) {
        const children = {};
        for (const [ch, child] of Object.entries(node.children)) {
            children[ch] = this._serializeTrieNode(child);
        }
        return { c: children, m: Array.from(node.memoryIds), t: node.terminal };
    }
    _deserializeTrieNode(data) {
        const children = {};
        for (const [ch, child] of Object.entries(data.c)) {
            children[ch] = this._deserializeTrieNode(child);
        }
        return { children, memoryIds: new Set(data.m), terminal: data.t };
    }
    _serializeMap(map) {
        const result = {};
        for (const [key, set] of map) {
            result[key] = Array.from(set);
        }
        return result;
    }
    _deserializeMap(data) {
        const map = new Map();
        for (const [key, arr] of Object.entries(data)) {
            map.set(key, new Set(arr));
        }
        return map;
    }
}
//# sourceMappingURL=semantic-trie.js.map