/**
 * Memory Graph - Knowledge graph layer over the flat memory store.
 *
 * Uses graphify to build a graph from memories, enabling:
 * - Contextual retrieval via BFS/DFS traversal
 * - Community detection to cluster related memories
 * - God node detection to find key themes
 * - Surprising connections between distant memories
 *
 * The graph is stored at ~/.avis/self/memory/graph/ and rebuilt
 * incrementally when memories change.
 */
import { execFileSync, execSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { getMemoryGraphDir } from "../config.js";
import { vault } from "./vault.js";
// ============================================================================
// MemoryGraph
// ============================================================================
export class MemoryGraph {
    constructor(memoryManager, graphDir) {
        this._graphData = null;
        this._memoryManager = memoryManager;
        this._graphDir = graphDir ?? getMemoryGraphDir();
        this._graphPath = join(this._graphDir, "graph.json");
        this._pythonPath = this._detectPython();
    }
    get graphDir() {
        return this._graphDir;
    }
    _detectPython() {
        try {
            const graphifyBin = execSync("which graphify 2>/dev/null", { encoding: "utf-8" }).trim();
            if (graphifyBin) {
                const shebang = readFileSync(graphifyBin, "utf-8").split("\n")[0];
                const python = shebang.replace(/^#!\s*/, "").trim();
                if (python && /^[a-zA-Z0-9/_.-]+$/.test(python)) {
                    return python;
                }
            }
        }
        catch {
            // fall through
        }
        return "python3";
    }
    _runPython(script) {
        try {
            return execFileSync(this._pythonPath, ["-c", script], {
                encoding: "utf-8",
                timeout: 30000,
                cwd: this._graphDir,
            }).trim();
        }
        catch (err) {
            return err.stderr?.toString() ?? err.message ?? "Unknown error";
        }
    }
    /** Check if graphify is available */
    isAvailable() {
        try {
            execFileSync(this._pythonPath, ["-c", "import graphify"], {
                encoding: "utf-8",
                timeout: 5000,
            });
            return true;
        }
        catch {
            return false;
        }
    }
    /** Load graph data from disk */
    _loadGraph() {
        if (this._graphData)
            return this._graphData;
        if (!existsSync(this._graphPath))
            return null;
        try {
            const raw = vault.ready ? vault.readFileSync(this._graphPath) : readFileSync(this._graphPath, "utf-8");
            this._graphData = JSON.parse(raw);
            return this._graphData;
        }
        catch {
            return null;
        }
    }
    /** Check if graph needs rebuilding (memories changed since last build) */
    needsRebuild() {
        const graph = this._loadGraph();
        if (!graph)
            return this._memoryManager.count > 0;
        return graph.lastMemoryCount !== this._memoryManager.count;
    }
    /**
     * Build or rebuild the knowledge graph from all memories.
     *
     * Converts memories into a document that graphify can process,
     * then extracts entities/relationships and clusters them.
     */
    async build() {
        if (!this.isAvailable()) {
            throw new Error("graphify is not installed. Run: pip install graphifyy");
        }
        const memories = this._memoryManager.list();
        if (memories.length === 0) {
            this._graphData = null;
            return { nodeCount: 0, edgeCount: 0, communityCount: 0 };
        }
        if (!existsSync(this._graphDir)) {
            mkdirSync(this._graphDir, { recursive: true });
        }
        // Write memories as a structured document for graphify to extract from
        const memoryDoc = this._memoriesToDocument(memories);
        const memoryDocPath = join(this._graphDir, "memories.md");
        if (vault.ready) {
            vault.writeFileSync(memoryDocPath, memoryDoc);
        }
        else {
            writeFileSync(memoryDocPath, memoryDoc);
        }
        // Build extraction JSON directly - each memory becomes nodes + edges
        const extraction = this._extractFromMemories(memories);
        const extractionPath = join(this._graphDir, "extraction.json");
        if (vault.ready) {
            vault.writeFileSync(extractionPath, JSON.stringify(extraction, null, 2));
        }
        else {
            writeFileSync(extractionPath, JSON.stringify(extraction, null, 2));
        }
        // Use graphify to build graph, cluster, and analyze
        const result = this._runPython(`
import json, sys
from pathlib import Path
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections

extraction = json.loads(Path('extraction.json').read_text())
G = build_from_json(extraction)

if G.number_of_nodes() == 0:
    print(json.dumps({"error": "empty graph"}))
    sys.exit(0)

communities = cluster(G)
gods = god_nodes(G)
if not gods:
    # Fallback: compute by degree centrality
    deg = dict(G.degree())
    gods = sorted(deg, key=deg.get, reverse=True)[:10]
surprises = surprising_connections(G, communities)

# Build output
nodes = []
for nid, data in G.nodes(data=True):
    comm = None
    for cid, members in communities.items():
        if nid in members:
            comm = cid
            break
    nodes.append({
        "id": nid,
        "label": data.get("label", nid),
        "memoryIds": data.get("memory_ids", []),
        "community": comm,
        "source_file": data.get("source_file", ""),
    })

edges = []
for u, v, data in G.edges(data=True):
    edges.append({
        "source": u,
        "target": v,
        "relation": data.get("relation", ""),
        "confidence": data.get("confidence", "EXTRACTED"),
        "confidence_score": data.get("confidence_score", 1.0),
    })

result = {
    "nodes": nodes,
    "edges": edges,
    "communities": {str(k): v for k, v in communities.items()},
    "godNodes": [g["id"] if isinstance(g, dict) else g for g in gods[:10]],
    "nodeCount": G.number_of_nodes(),
    "edgeCount": G.number_of_edges(),
    "communityCount": len(communities),
}
print(json.dumps(result))
`);
        try {
            const parsed = JSON.parse(result);
            if (parsed.error) {
                return { nodeCount: 0, edgeCount: 0, communityCount: 0 };
            }
            // Label communities based on their node labels
            const communityLabels = {};
            for (const [cid, memberIds] of Object.entries(parsed.communities)) {
                const memberNodes = parsed.nodes.filter((n) => memberIds.includes(n.id));
                const labels = memberNodes.map((n) => n.label).slice(0, 3);
                communityLabels[cid] = labels.join(", ");
            }
            const graphData = {
                nodes: parsed.nodes,
                edges: parsed.edges,
                communities: parsed.communities,
                communityLabels,
                godNodes: parsed.godNodes,
                lastMemoryCount: memories.length,
                lastBuildTime: new Date().toISOString(),
            };
            if (vault.ready) {
                vault.writeFileSync(this._graphPath, JSON.stringify(graphData, null, 2));
            }
            else {
                writeFileSync(this._graphPath, JSON.stringify(graphData, null, 2));
            }
            this._graphData = graphData;
            return {
                nodeCount: parsed.nodeCount,
                edgeCount: parsed.edgeCount,
                communityCount: parsed.communityCount,
            };
        }
        catch {
            return { nodeCount: 0, edgeCount: 0, communityCount: 0 };
        }
    }
    /**
     * Convert memories into a structured extraction JSON that graphify can build from.
     * Each memory becomes one or more nodes with edges between related concepts.
     */
    _extractFromMemories(memories) {
        const nodes = [];
        const edges = [];
        const seenIds = new Set();
        for (const memory of memories) {
            // Each memory is a node
            const memNodeId = `memory_${memory.id}`;
            if (!seenIds.has(memNodeId)) {
                nodes.push({
                    id: memNodeId,
                    label: memory.content.slice(0, 80),
                    file_type: "document",
                    source_file: "memory",
                    memory_ids: [memory.id],
                });
                seenIds.add(memNodeId);
            }
            // Each tag becomes a node, with edges from memory to tag
            for (const tag of memory.tags) {
                const tagId = `tag_${tag.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
                if (!seenIds.has(tagId)) {
                    nodes.push({
                        id: tagId,
                        label: tag,
                        file_type: "document",
                        source_file: "tag",
                        memory_ids: [],
                    });
                    seenIds.add(tagId);
                }
                edges.push({
                    source: memNodeId,
                    target: tagId,
                    relation: "tagged_with",
                    confidence: "EXTRACTED",
                    confidence_score: 1.0,
                    source_file: "memory",
                });
            }
            // Extract key terms from content for concept nodes
            const terms = this._extractKeyTerms(memory.content);
            for (const term of terms) {
                const termId = `concept_${term.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
                if (!seenIds.has(termId)) {
                    nodes.push({
                        id: termId,
                        label: term,
                        file_type: "document",
                        source_file: "concept",
                        memory_ids: [],
                    });
                    seenIds.add(termId);
                }
                edges.push({
                    source: memNodeId,
                    target: termId,
                    relation: "mentions",
                    confidence: "EXTRACTED",
                    confidence_score: 0.9,
                    source_file: "memory",
                });
            }
        }
        // Add inferred edges between memories that share concepts
        const memoryTerms = new Map();
        for (const memory of memories) {
            const terms = this._extractKeyTerms(memory.content);
            const termIds = new Set(terms.map((t) => `concept_${t.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`));
            memoryTerms.set(`memory_${memory.id}`, termIds);
        }
        const memIds = Array.from(memoryTerms.keys());
        for (let i = 0; i < memIds.length; i++) {
            for (let j = i + 1; j < memIds.length; j++) {
                const termsA = memoryTerms.get(memIds[i]);
                const termsB = memoryTerms.get(memIds[j]);
                const shared = [...termsA].filter((t) => termsB.has(t));
                if (shared.length > 0) {
                    const score = Math.min(0.9, 0.5 + shared.length * 0.1);
                    edges.push({
                        source: memIds[i],
                        target: memIds[j],
                        relation: "semantically_similar_to",
                        confidence: "INFERRED",
                        confidence_score: score,
                        source_file: "memory",
                    });
                }
            }
        }
        return { nodes, edges, input_tokens: 0, output_tokens: 0 };
    }
    /**
     * Extract key terms from memory content.
     * Simple heuristic: words > 3 chars, not stopwords, capitalized phrases.
     */
    _extractKeyTerms(content) {
        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",
            "uses",
            "using",
            "used",
            "does",
            "make",
            "want",
            "wants",
            "need",
            "needs",
            "prefer",
            "prefers",
            "preferred",
        ]);
        const terms = [];
        const seen = new Set();
        // Extract quoted strings
        const quoted = content.match(/"([^"]+)"|'([^']+)'/g);
        if (quoted) {
            for (const q of quoted) {
                const term = q.replace(/['"]/g, "").trim();
                if (term.length > 2 && !seen.has(term.toLowerCase())) {
                    terms.push(term);
                    seen.add(term.toLowerCase());
                }
            }
        }
        // Extract capitalized phrases (likely proper nouns / tech terms)
        const capitalPhrases = content.match(/[A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*/g);
        if (capitalPhrases) {
            for (const phrase of capitalPhrases) {
                if (phrase.length > 3 && !seen.has(phrase.toLowerCase()) && !stopwords.has(phrase.toLowerCase())) {
                    terms.push(phrase);
                    seen.add(phrase.toLowerCase());
                }
            }
        }
        // Extract remaining significant words
        const words = content.split(/[\s,.;:!?()[\]{}]+/);
        for (const word of words) {
            const lower = word.toLowerCase();
            if (word.length > 3 && !stopwords.has(lower) && !seen.has(lower) && !/^\d+$/.test(word)) {
                terms.push(word);
                seen.add(lower);
            }
        }
        return terms.slice(0, 15); // Cap to avoid noise
    }
    /**
     * Convert memories to a markdown document for graphify to process.
     */
    _memoriesToDocument(memories) {
        const sections = ["# User Memories\n"];
        for (const m of memories) {
            const tags = m.tags.length > 0 ? `\nTags: ${m.tags.join(", ")}` : "";
            sections.push(`## Memory ${m.id}\n\n${m.content}${tags}\n`);
        }
        return sections.join("\n");
    }
    /**
     * Query the memory graph to find memories relevant to a user message.
     *
     * Uses BFS from matching nodes to find connected memories,
     * ranked by graph distance and confidence scores.
     */
    retrieve(query, maxResults = 10) {
        const graph = this._loadGraph();
        if (!graph || graph.nodes.length === 0) {
            // Fallback: return all memories (no graph yet)
            return {
                memories: this._memoryManager.list().slice(0, maxResults),
                matchedCommunities: [],
                godNodes: [],
            };
        }
        const queryTerms = query
            .toLowerCase()
            .split(/[\s,.;:!?()[\]{}]+/)
            .filter((t) => t.length > 2);
        // Score each node by term overlap with query
        const nodeScores = new Map();
        for (const node of graph.nodes) {
            const label = node.label.toLowerCase();
            const id = node.id.toLowerCase();
            let score = 0;
            for (const term of queryTerms) {
                if (label.includes(term))
                    score += 2;
                if (id.includes(term))
                    score += 1;
            }
            if (score > 0) {
                nodeScores.set(node.id, score);
            }
        }
        // BFS from matched nodes to find connected memory nodes
        const memoryScores = new Map();
        const matchedCommunities = new Set();
        const visited = new Set();
        // Build adjacency list
        const adj = new Map();
        for (const edge of graph.edges) {
            if (!adj.has(edge.source))
                adj.set(edge.source, []);
            if (!adj.has(edge.target))
                adj.set(edge.target, []);
            adj.get(edge.source).push({ target: edge.target, weight: edge.confidence_score });
            adj.get(edge.target).push({ target: edge.source, weight: edge.confidence_score });
        }
        // BFS from top-scoring nodes
        const startNodes = Array.from(nodeScores.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 5)
            .map(([id]) => id);
        for (const startId of startNodes) {
            const startScore = nodeScores.get(startId) ?? 1;
            const queue = [
                { id: startId, depth: 0, score: startScore },
            ];
            while (queue.length > 0) {
                const { id, depth, score } = queue.shift();
                if (visited.has(id) || depth > 3)
                    continue;
                visited.add(id);
                // If this is a memory node, record its score
                const node = graph.nodes.find((n) => n.id === id);
                if (node) {
                    if (node.id.startsWith("memory_")) {
                        const memId = node.id.slice(7);
                        const existing = memoryScores.get(memId) ?? 0;
                        memoryScores.set(memId, Math.max(existing, score));
                    }
                    // Track matched communities
                    if (node.community !== undefined) {
                        matchedCommunities.add(String(node.community));
                    }
                }
                // Traverse neighbors with score decay
                const neighbors = adj.get(id) ?? [];
                for (const { target, weight } of neighbors) {
                    if (!visited.has(target)) {
                        queue.push({
                            id: target,
                            depth: depth + 1,
                            score: score * weight * 0.7, // decay by distance
                        });
                    }
                }
            }
        }
        // If graph query found nothing, fall back to text search
        if (memoryScores.size === 0) {
            const textResults = this._memoryManager.search(query);
            return {
                memories: textResults.slice(0, maxResults),
                matchedCommunities: [],
                godNodes: graph.godNodes.slice(0, 5).map((id) => {
                    const node = graph.nodes.find((n) => n.id === id);
                    return node?.label ?? id;
                }),
            };
        }
        // Rank memories by score and retrieve
        const rankedMemoryIds = Array.from(memoryScores.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, maxResults)
            .map(([id]) => id);
        const memories = [];
        for (const id of rankedMemoryIds) {
            const memory = this._memoryManager.get(id);
            if (memory)
                memories.push(memory);
        }
        // Resolve community labels
        const communityLabels = [];
        for (const cid of matchedCommunities) {
            const label = graph.communityLabels[cid];
            if (label)
                communityLabels.push(label);
        }
        // Resolve god node labels
        const godNodeLabels = graph.godNodes.slice(0, 5).map((id) => {
            const node = graph.nodes.find((n) => n.id === id);
            return node?.label ?? id;
        });
        return {
            memories,
            matchedCommunities: communityLabels,
            godNodes: godNodeLabels,
        };
    }
    /**
     * Format a graph-based retrieval result for the system prompt.
     * Only includes memories relevant to the query, plus context from the graph.
     */
    formatRetrievalForPrompt(result) {
        if (result.memories.length === 0 && result.godNodes.length === 0) {
            return "";
        }
        const sections = ["\n\n# User Memory"];
        if (result.godNodes.length > 0) {
            sections.push(`\nKey themes from past interactions: ${result.godNodes.join(", ")}`);
        }
        if (result.matchedCommunities.length > 0) {
            sections.push(`Relevant memory clusters: ${result.matchedCommunities.join("; ")}`);
        }
        if (result.memories.length > 0) {
            sections.push("\nRelevant memories:");
            for (const m of result.memories) {
                const tagStr = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
                sections.push(`- ${m.content}${tagStr}`);
            }
        }
        return sections.join("\n");
    }
    /**
     * Get graph statistics for display.
     */
    getStats() {
        const graph = this._loadGraph();
        if (!graph) {
            return { nodeCount: 0, edgeCount: 0, communityCount: 0, godNodes: [], lastBuildTime: null };
        }
        return {
            nodeCount: graph.nodes.length,
            edgeCount: graph.edges.length,
            communityCount: Object.keys(graph.communities).length,
            godNodes: graph.godNodes.slice(0, 5).map((id) => {
                const node = graph.nodes.find((n) => n.id === id);
                return node?.label ?? id;
            }),
            lastBuildTime: graph.lastBuildTime,
        };
    }
}
//# sourceMappingURL=memory-graph.js.map