/**
 * src/knowledge/linker.ts
 *
 * Turns real USAGE into knowledge-graph relationships. The KG had ~200 entities
 * (tools, skills, goals, …) but almost no edges — tools that get used never got
 * connected to anything, so the graph rendered as a field of disconnected dots.
 *
 * The journal already records every tool use with the goal it served
 * (related_goal_id), who ran it (actor), and when. This derives edges from that:
 *   • goal  —[نُفّذ بـ]→  tool      (a goal was pursued using this tool)
 *   • tool  —[عمل معه]→  tool       (two tools used on the SAME goal/session)
 * Resolved by entity NAME (tools/skills) + agenda-goal title, deduped, and
 * capped so the graph stays legible (and within the endpoint's 400-link budget).
 */
import { randomUUID } from 'node:crypto';
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('knowledge:linker');
const norm = (s) => (s || '').toLowerCase().trim();
export async function relinkFromJournal(tenantId, opts = {}) {
    const sinceDays = opts.sinceDays ?? 45;
    const maxEdges = opts.maxEdges ?? 350;
    const perGroupCap = opts.perGroupToolCap ?? 6;
    const result = { toolsResolved: 0, goalToolEdges: 0, toolToolEdges: 0, skipped: 0 };
    // 1. Entity maps (resolve usage names → KG entity ids).
    const { data: ents } = await supabase
        .from('kg_entities')
        .select('id, kind, name_normalized')
        .eq('tenant_id', tenantId)
        .in('kind', ['tool', 'skill', 'goal']);
    const toolByName = new Map(); // tool/skill name → entity id
    const goalEntByTitle = new Map(); // goal title → entity id
    for (const e of (ents ?? [])) {
        if (e.kind === 'goal')
            goalEntByTitle.set(e.name_normalized, e.id);
        else
            toolByName.set(e.name_normalized, e.id);
    }
    if (toolByName.size === 0) {
        log.info({ tenantId }, 'relink: no tool/skill entities to link');
        return result;
    }
    // 2. agenda goal id → title (to map journal.related_goal_id → goal entity).
    const { data: goals } = await supabase.from('agenda_goals').select('id, title').eq('tenant_id', tenantId);
    const goalIdToEnt = new Map();
    for (const g of (goals ?? [])) {
        const ent = goalEntByTitle.get(norm(g.title));
        if (ent)
            goalIdToEnt.set(g.id, ent);
    }
    // 3. Journal tool-uses in the window.
    const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
    const { data: jr } = await supabase
        .from('journal_entries')
        .select('action, related_goal_id, actor, created_at')
        .eq('tenant_id', tenantId)
        .eq('kind', 'tool')
        .gte('created_at', since)
        .limit(5000);
    const rows = (jr ?? []);
    // 4. Group co-occurring tools by the goal they served (or actor+day bucket),
    //    and collect goal→tool edges.
    const groups = new Map();
    const wanted = [];
    const seenGoalTool = new Set();
    const resolvedTools = new Set();
    for (const r of rows) {
        const toolId = toolByName.get(norm(r.action));
        if (!toolId) {
            result.skipped++;
            continue;
        }
        resolvedTools.add(toolId);
        const key = r.related_goal_id || `${r.actor || 'agent'}|${(r.created_at || '').slice(0, 10)}`;
        let set = groups.get(key);
        if (!set) {
            set = new Set();
            groups.set(key, set);
        }
        set.add(toolId);
        // goal → tool
        if (r.related_goal_id) {
            const gEnt = goalIdToEnt.get(r.related_goal_id);
            if (gEnt && gEnt !== toolId) {
                const dk = `${gEnt}>${toolId}`;
                if (!seenGoalTool.has(dk)) {
                    seenGoalTool.add(dk);
                    wanted.push({ source_id: gEnt, target_id: toolId, type: 'نُفّذ بـ' });
                }
            }
        }
    }
    result.toolsResolved = resolvedTools.size;
    // 5. tool ↔ tool co-occurrence (one canonical direction per pair).
    const seenPair = new Set();
    for (const set of groups.values()) {
        const arr = [...set].slice(0, perGroupCap);
        for (let i = 0; i < arr.length; i++) {
            for (let j = i + 1; j < arr.length; j++) {
                const [a, b] = arr[i] < arr[j] ? [arr[i], arr[j]] : [arr[j], arr[i]];
                const pk = `${a}~${b}`;
                if (seenPair.has(pk))
                    continue;
                seenPair.add(pk);
                wanted.push({ source_id: a, target_id: b, type: 'عمل معه' });
            }
        }
    }
    // 6. Upsert (deduped against what already exists), capped.
    const now = new Date().toISOString();
    let created = 0;
    for (const w of wanted) {
        if (created >= maxEdges)
            break;
        try {
            const ex = await supabase
                .from('kg_relationships')
                .select('id')
                .eq('tenant_id', tenantId)
                .eq('source_id', w.source_id)
                .eq('target_id', w.target_id)
                .eq('type', w.type)
                .maybeSingle();
            if (ex.data)
                continue;
            const ins = await supabase.from('kg_relationships').insert({
                id: randomUUID(), tenant_id: tenantId,
                source_id: w.source_id, target_id: w.target_id, type: w.type,
                properties: { derived_from: 'journal_usage' }, created_at: now, updated_at: now,
            });
            if (ins.error)
                continue;
            created++;
            if (w.type === 'نُفّذ بـ')
                result.goalToolEdges++;
            else
                result.toolToolEdges++;
        }
        catch { /* best-effort per edge */ }
    }
    log.info({ tenantId, ...result }, 'relink from journal complete');
    return result;
}
//# sourceMappingURL=linker.js.map