/**
 * recall_memory tool — long-term memory recall for Avis.
 *
 * When the model doesn't have information in its current context, it calls
 * this tool to search across ALL long-term storage:
 *
 *   1. Explicit memories (memory.jsonl) — extracted facts about the user
 *   2. Past sessions (session files) — full conversation history
 *   3. Interaction log — raw interaction entries
 *   4. Knowledge graph — traversal-based retrieval
 *
 * Unlike the `memory` tool (which is for CRUD on explicit memories), this
 * tool is read-only and searches everything Avis has ever seen.
 *
 * Results are returned as text the model can use, NOT injected into the
 * system prompt (that would defeat the purpose of on-demand recall).
 */
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { Text } from "@dotkaio/avis-tui";
import { Type } from "@sinclair/typebox";
import { loadEntriesFromFile } from "../session-manager.js";
import { vault } from "../vault.js";
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const recallSchema = Type.Object({
    query: Type.String({
        description: "What to search for. Can be a topic, keyword, question, person's name, project name, date reference, or natural language query. Examples: 'Kaio preferences', 'React project structure', 'what model was used last week', 'counting game'",
    }),
    scope: Type.Optional(Type.Union([Type.Literal("all"), Type.Literal("memories"), Type.Literal("sessions"), Type.Literal("interactions")], {
        description: "Where to search. Default 'all' searches everywhere. 'memories' = extracted facts only. 'sessions' = past conversation sessions. 'interactions' = raw interaction log.",
    })),
    maxResults: Type.Optional(Type.Number({
        description: "Maximum number of results to return. Default 20.",
    })),
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function ok(text) {
    return { content: [{ type: "text", text }], details: undefined };
}
/** Score how relevant a text block is to a query (simple keyword matching). */
function relevanceScore(text, queryTerms) {
    const lower = text.toLowerCase();
    let score = 0;
    for (const term of queryTerms) {
        if (lower.includes(term)) {
            score += 1;
            // Bonus for exact word boundary matches
            const regex = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
            if (regex.test(text))
                score += 0.5;
        }
    }
    return score;
}
// ---------------------------------------------------------------------------
// Search: explicit memories
// ---------------------------------------------------------------------------
function searchMemories(memoryManager, queryTerms, query) {
    const results = [];
    // Use the graph-based retrieval if available
    try {
        const graphResult = memoryManager.retrieveRelevant(query, 15);
        for (const memory of graphResult.memories) {
            results.push({
                source: "memory",
                content: memory.content + (memory.tags.length ? ` [${memory.tags.join(", ")}]` : ""),
                timestamp: memory.createdAt,
                score: relevanceScore(`${memory.content} ${memory.tags.join(" ")}`, queryTerms) + 2, // boost explicit memories
            });
        }
        if (results.length > 0)
            return results;
    }
    catch {
        // Graph not available, fall back to text search
    }
    // Fallback: substring search
    const matches = memoryManager.search(query);
    for (const memory of matches) {
        results.push({
            source: "memory",
            content: memory.content + (memory.tags.length ? ` [${memory.tags.join(", ")}]` : ""),
            timestamp: memory.createdAt,
            score: relevanceScore(`${memory.content} ${memory.tags.join(" ")}`, queryTerms) + 2,
        });
    }
    // Also search all memories by individual terms if no substring match
    if (results.length === 0) {
        const allMemories = memoryManager.list();
        for (const memory of allMemories) {
            const score = relevanceScore(`${memory.content} ${memory.tags.join(" ")}`, queryTerms);
            if (score > 0) {
                results.push({
                    source: "memory",
                    content: memory.content + (memory.tags.length ? ` [${memory.tags.join(", ")}]` : ""),
                    timestamp: memory.createdAt,
                    score: score + 1,
                });
            }
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// Search: past sessions
// ---------------------------------------------------------------------------
function searchSessions(sessionsDir, queryTerms, maxPerSession = 5) {
    const results = [];
    if (!existsSync(sessionsDir))
        return results;
    // Walk all session directories
    let sessionDirs;
    try {
        sessionDirs = readdirSync(sessionsDir)
            .map((d) => join(sessionsDir, d))
            .filter((d) => {
            try {
                return statSync(d).isDirectory();
            }
            catch {
                return false;
            }
        });
    }
    catch {
        return results;
    }
    for (const dir of sessionDirs) {
        let sessionFiles;
        try {
            sessionFiles = readdirSync(dir)
                .filter((f) => f.endsWith(".jsonl"))
                .map((f) => join(dir, f));
        }
        catch {
            continue;
        }
        for (const filePath of sessionFiles) {
            let entries;
            try {
                entries = loadEntriesFromFile(filePath);
            }
            catch {
                continue;
            }
            let sessionHits = 0;
            const sessionId = filePath.split("/").pop()?.replace(".jsonl", "") || "unknown";
            for (const entry of entries) {
                if (entry.type !== "message")
                    continue;
                const msgEntry = entry;
                const msg = msgEntry.message;
                // Extract text content
                let text = "";
                const msgAny = msg;
                if (typeof msgAny.content === "string") {
                    text = msgAny.content;
                }
                else if (Array.isArray(msgAny.content)) {
                    text = msgAny.content
                        .filter((c) => c.type === "text")
                        .map((c) => c.text)
                        .join(" ");
                }
                if (!text)
                    continue;
                const score = relevanceScore(text, queryTerms);
                if (score > 0) {
                    // Truncate long messages
                    const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
                    results.push({
                        source: "session",
                        content: `[${msg.role}] ${truncated}`,
                        timestamp: msgEntry.timestamp,
                        score,
                        sessionId,
                    });
                    sessionHits++;
                    if (sessionHits >= maxPerSession)
                        break;
                }
            }
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// Search: interaction log
// ---------------------------------------------------------------------------
function searchInteractions(interactionsPath, queryTerms) {
    const results = [];
    if (!existsSync(interactionsPath))
        return results;
    let content;
    try {
        content = vault.ready ? vault.readFileSync(interactionsPath) : readFileSync(interactionsPath, "utf8");
    }
    catch {
        return results;
    }
    const lines = content.trim().split("\n");
    for (const line of lines) {
        if (!line.trim())
            continue;
        try {
            const entry = JSON.parse(line);
            const score = relevanceScore(entry.content, queryTerms);
            if (score > 0) {
                const truncated = entry.content.length > 400 ? `${entry.content.slice(0, 400)}...` : entry.content;
                results.push({
                    source: "interaction",
                    content: `[${entry.role}] ${truncated}`,
                    timestamp: entry.timestamp,
                    score,
                });
            }
        }
        catch {
            // Skip malformed
        }
    }
    return results;
}
// ---------------------------------------------------------------------------
// Main tool
// ---------------------------------------------------------------------------
export function createRecallMemoryToolDefinition(memoryManager, sessionsDir, interactionsPath) {
    return {
        name: "recall_memory",
        label: "recall",
        description: "Search Avis's long-term memory across all past sessions, stored memories, and interaction history. Use this when you need to recall something from a previous conversation, look up a user preference or decision from the past, or find context about a topic discussed before. This is your long-term memory — if you don't know something, try recalling it first.",
        parameters: recallSchema,
        promptSnippet: "Search long-term memory: past sessions, stored facts, interaction history",
        promptGuidelines: [
            "When you don't have context about something the user references, call recall_memory to search past conversations",
            "Use recall_memory before asking the user to repeat themselves — the answer may be in your long-term memory",
            "Search with specific keywords: project names, people, topics, decisions, dates",
            "If the first search doesn't find what you need, try different keywords or a broader scope",
        ],
        async execute(_toolCallId, params, _signal) {
            const { query, scope = "all", maxResults = 20 } = params;
            const queryTerms = query
                .toLowerCase()
                .split(/\s+/)
                .filter((t) => t.length > 1);
            if (queryTerms.length === 0) {
                return ok("Please provide a search query with at least one meaningful term.");
            }
            const allResults = [];
            // 1. Search explicit memories (always — they're the most curated)
            if (scope === "all" || scope === "memories") {
                allResults.push(...searchMemories(memoryManager, queryTerms, query));
            }
            // 2. Search past sessions
            if (scope === "all" || scope === "sessions") {
                allResults.push(...searchSessions(sessionsDir, queryTerms));
            }
            // 3. Search interaction log
            if (scope === "all" || scope === "interactions") {
                allResults.push(...searchInteractions(interactionsPath, queryTerms));
            }
            if (allResults.length === 0) {
                return ok(`No results found for "${query}" in long-term memory.`);
            }
            // Sort by relevance score (descending), then by timestamp (most recent first)
            allResults.sort((a, b) => {
                if (b.score !== a.score)
                    return b.score - a.score;
                if (a.timestamp && b.timestamp) {
                    return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
                }
                return 0;
            });
            // Deduplicate similar content
            const seen = new Set();
            const deduped = [];
            for (const result of allResults) {
                const key = result.content.slice(0, 200).toLowerCase();
                if (!seen.has(key)) {
                    seen.add(key);
                    deduped.push(result);
                }
            }
            // Take top N results
            const topResults = deduped.slice(0, maxResults);
            // Format output
            const lines = [];
            lines.push(`Found ${deduped.length} results for "${query}" (showing top ${topResults.length}):\n`);
            for (const result of topResults) {
                const time = result.timestamp ? new Date(result.timestamp).toLocaleString() : "unknown time";
                const sourceLabel = {
                    memory: "💾 Memory",
                    session: "💬 Session",
                    interaction: "📝 Interaction",
                    graph: "🔗 Graph",
                }[result.source];
                lines.push(`${sourceLabel} (${time}):`);
                lines.push(`  ${result.content}`);
                if (result.sessionId) {
                    lines.push(`  [session: ${result.sessionId}]`);
                }
                lines.push("");
            }
            return ok(lines.join("\n"));
        },
        renderCall(args, _theme, _context) {
            return new Text(`recall_memory: "${args.query}"${args.scope ? ` [${args.scope}]` : ""}`);
        },
        renderResult(result, _options, _theme, _context) {
            const text = result.content.map((c) => ("text" in c ? c.text : "")).join("");
            // Show first few lines
            const preview = text.split("\n").slice(0, 5).join("\n");
            return new Text(preview);
        },
    };
}
export function createRecallMemoryTool(memoryManager, sessionsDir, interactionsPath) {
    return wrapToolDefinition(createRecallMemoryToolDefinition(memoryManager, sessionsDir, interactionsPath));
}
//# sourceMappingURL=recall-memory.js.map