/**
 * src/autonomy/kg-seeder.ts
 *
 * Unified knowledge-graph seeder. Registers EVERY out-of-box Muassis asset
 * as a kg_entity so the agent (and the dashboard graph view) treats them
 * as first-class citizens of the tenant's knowledge graph.
 *
 * What gets seeded:
 *   • Skill — every workspace-template/SKILLS/*.md (bundled + native)
 *   • CronJob — every default cron job (briefs, research, skill-discovery, etc.)
 *   • Policy — every default autonomy policy
 *   • Tool — every brain-callable tool registered in TOOLS
 *
 * Why:
 *   1. The brain's `recall_fact` + KG search now surface skills/tools as
 *      naturally as customer or business facts.
 *   2. The dashboard graph tab shows the FULL inventory — owner sees what
 *      the agent ships with, no documentation hunting.
 *   3. Discovered skills (from skill-discovery cron) live alongside the
 *      bundled ones — one unified registry.
 *
 * Idempotent. Safe to run on every boot. Updates `last_seen` on existing
 * entries so the graph view can highlight "newly verified".
 */
import { randomUUID } from 'node:crypto';
import { readdir, readFile } from 'node:fs/promises';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
const log = createChildLogger('autonomy:kg-seeder');
const __ws_template = resolve(dirname(fileURLToPath(import.meta.url)), '../../workspace-template');
async function upsertEntity(tenantId, kind, type, name, properties) {
    const now = new Date().toISOString();
    const nameNorm = name.toLowerCase();
    const { data: existing } = await supabase
        .from('kg_entities')
        .select('id, properties')
        .eq('tenant_id', tenantId)
        .eq('kind', kind)
        .eq('name_normalized', nameNorm)
        .maybeSingle();
    if (existing) {
        const oldProps = existing.properties ?? {};
        await supabase
            .from('kg_entities')
            .update({
            properties: { ...oldProps, ...properties, last_seen: now },
            updated_at: now,
        })
            .eq('id', existing.id);
        return { created: false, id: existing.id };
    }
    const id = randomUUID();
    const row = {
        id,
        tenant_id: tenantId,
        customer_id: null,
        kind,
        type,
        name,
        name_normalized: nameNorm,
        properties: { ...properties, first_seen: now, last_seen: now },
        source_files: [],
        episode_count: 0,
        created_at: now,
        updated_at: now,
    };
    await supabase.from('kg_entities').insert(row);
    return { created: true, id };
}
function parseFrontmatter(content) {
    const m = content.match(/^---\n([\s\S]*?)\n---/);
    if (!m)
        return {};
    const block = m[1];
    const out = {};
    for (const line of block.split('\n')) {
        const kv = line.match(/^(\w+):\s*(.+)$/);
        if (!kv)
            continue;
        const k = kv[1];
        const v = kv[2].trim();
        if (k === 'priority')
            out[k] = Number(v);
        else
            out[k] = v;
    }
    return out;
}
async function seedSkills(tenantId) {
    const dir = join(__ws_template, 'SKILLS');
    let files;
    try {
        files = (await readdir(dir)).filter(f => f.endsWith('.md'));
    }
    catch {
        log.warn({ dir }, 'workspace-template/SKILLS not found — skipping skill seed');
        return { added: 0, updated: 0 };
    }
    let added = 0;
    let updated = 0;
    for (const file of files) {
        try {
            const raw = await readFile(join(dir, file), 'utf-8');
            const fm = parseFrontmatter(raw);
            const name = fm.name ?? file.replace(/\.md$/, '');
            const summary = (raw.replace(/^---[\s\S]*?---/, '').trim().split('\n').find(l => l.length > 20) ?? '').slice(0, 280);
            const r = await upsertEntity(tenantId, 'skill', 'Skill', name, {
                filename: file,
                priority: fm.priority ?? 3,
                keywords: fm.keywords ?? '',
                summary,
                install_status: 'bundled',
                bundled: true,
                source: 'in-box',
            });
            if (r.created)
                added++;
            else
                updated++;
        }
        catch (err) {
            log.warn({ file, err: err.message }, 'skill seed failed');
        }
    }
    return { added, updated };
}
async function seedCronJobs(tenantId) {
    // Mirror the seeder.ts list (descriptions slimmed). We don't import from
    // seeder to avoid a circular dep; this is a presentation layer over the
    // same data.
    const jobs = [
        { name: 'morning-brief', schedule: '0 8 * * *', kind: 'agentTurn', description: 'Daily 8am brief: prayers, Tadawul, agenda, escalations, overnight inbox' },
        { name: 'evening-recap', schedule: '0 18 * * *', kind: 'agentTurn', description: 'Daily 6pm recap: today done, follow-ups, tomorrow top 3' },
        { name: 'weekly-review', schedule: '0 19 * * 0', kind: 'agentTurn', description: 'Sunday 7pm: KPI movement vs goals, lessons, next-week focus' },
        { name: 'fajr-reminder', schedule: '30 4 * * *', kind: 'agentTurn', description: 'Soft Fajr reminder with prayer schedule' },
        { name: 'asr-reminder', schedule: '0 15 * * *', kind: 'agentTurn', description: 'Asr prayer reminder' },
        { name: 'tadawul-open', schedule: '0 10 * * 0-4', kind: 'agentTurn', description: 'Market open + tracked tickers brief' },
        { name: 'tadawul-close', schedule: '0 15 * * 0-4', kind: 'agentTurn', description: 'Market close + TASI recap' },
        { name: 'overdue-invoice-scan', schedule: '0 9 * * 1-5', kind: 'agentTurn', description: 'Mon-Fri 9am: scan invoices >30d, queue dunning drafts' },
        { name: 'hot-lead-followup', schedule: '0 11 * * 1-5', kind: 'agentTurn', description: 'Mon-Fri 11am: re-engage leads gone quiet 4-7d' },
        { name: 'agenda-checkin', schedule: '0 12 * * 1-6', kind: 'agentTurn', description: 'Daily noon: flag goals slipping, celebrate achieved' },
        { name: 'compliance-radar', schedule: '0 9 * * 1', kind: 'agentTurn', description: 'Mon 9am: ZATCA/GOSI/Nitaqat radar' },
        { name: 'research-cycle', schedule: '15 9,13,17 * * 0-4', kind: 'systemEvent', description: 'Web research 3x daily — competitors + products + industry' },
        { name: 'skill-discovery', schedule: '0 4 * * *', kind: 'systemEvent', description: 'Daily 4am: GitHub search for new skills relevant to this business' },
        { name: 'customer-followup-suggestions', schedule: '0 12 * * 1-5', kind: 'agentTurn', description: 'Mon-Fri noon: 3 follow-up suggestions, cite skills used' },
        { name: 'competitor-news-scan', schedule: '0 19 * * 0-4', kind: 'agentTurn', description: 'Sun-Thu 7pm: competitor news + counter-move' },
        { name: 'industry-trend-scan', schedule: '0 10 * * 6', kind: 'agentTurn', description: 'Sat 10am: weekly industry + Vision 2030 scan' },
        { name: 'seo-position-check', schedule: '0 11 * * 1', kind: 'agentTurn', description: 'Mon 11am: SEO rank check + content gap' },
        { name: 'vendor-pricing-changes', schedule: '0 9 1 * *', kind: 'agentTurn', description: 'Monthly 1st 9am: vendor pricing audit' },
        { name: 'business-health-check', schedule: '0 20 * * 0', kind: 'agentTurn', description: 'Sun 8pm: green/yellow/red scorecard + focus' },
        { name: 'customer-segment-analysis', schedule: '0 10 1 * *', kind: 'agentTurn', description: 'Monthly 1st 10am: RFM segmentation + 5 to attend / 5 to thank' },
        { name: 'skill-library-review', schedule: '0 8 * * 6', kind: 'agentTurn', description: 'Sat 8am: review discovered skills, recommend installs' },
    ];
    let added = 0;
    let updated = 0;
    for (const job of jobs) {
        const r = await upsertEntity(tenantId, 'cron', 'CronJob', job.name, {
            schedule: job.schedule,
            kind: job.kind,
            description: job.description,
            bundled: true,
        });
        if (r.created)
            added++;
        else
            updated++;
    }
    return { added, updated };
}
async function seedPolicies(tenantId) {
    const policies = [
        { key: 'send_email', mode: 'auto', description: 'Send an email to a customer or supplier' },
        { key: 'send_whatsapp', mode: 'auto', description: 'Send a WhatsApp reply' },
        { key: 'auto_reply_faq', mode: 'auto', description: 'Auto-reply to FAQ-class customer questions' },
        { key: 'schedule_meeting', mode: 'auto', description: 'Book a meeting on the calendar' },
        { key: 'create_note', mode: 'auto', description: 'Create a note' },
        { key: 'add_reminder', mode: 'auto', description: 'Add a reminder' },
        { key: 'web_search', mode: 'auto', description: 'Search the web for information' },
        { key: 'log_lead', mode: 'auto', description: 'Log a new lead' },
        { key: 'qualify_lead', mode: 'auto', description: 'Score and qualify a lead' },
        { key: 'send_quote', mode: 'auto:5000-SAR', description: 'Send a price quote (auto under 5000 SAR)' },
        { key: 'issue_invoice', mode: 'auto:5000-SAR', description: 'Issue a ZATCA-compliant invoice (auto under 5000 SAR)' },
        { key: 'post_social', mode: 'ask', description: 'Post on owner-controlled social accounts' },
        { key: 'cold_outreach', mode: 'ask', description: 'Cold outbound sequence to new leads' },
        { key: 'pay_supplier', mode: 'ask', description: 'Pay a supplier or invoice' },
        { key: 'refund_customer', mode: 'ask', description: 'Refund a customer' },
        { key: 'sign_contract', mode: 'ask', description: 'Sign a contract or NDA' },
        { key: 'commit_purchase', mode: 'ask', description: 'Commit a purchase order' },
        { key: 'hire_or_fire', mode: 'ask', description: 'Hiring or termination decisions' },
        { key: 'change_pricing', mode: 'ask', description: 'Change product pricing publicly' },
        { key: 'public_statement', mode: 'ask', description: 'Make a public statement' },
        { key: 'transfer_funds', mode: 'ask', description: 'Move money between accounts' },
        { key: 'delete_data', mode: 'ask', description: 'Delete customer or business data' },
        { key: 'inbound_access', mode: 'ask', description: 'Behavior for unknown inbound DMs (default ask)' },
    ];
    let added = 0;
    let updated = 0;
    for (const p of policies) {
        const r = await upsertEntity(tenantId, 'policy', 'Policy', p.key, {
            mode: p.mode,
            description: p.description,
            bundled: true,
        });
        if (r.created)
            added++;
        else
            updated++;
    }
    return { added, updated };
}
async function seedTools(tenantId) {
    // (Tool hygiene audit) Read from the canonical TOOLS array so newly-added
    // tools auto-seed without a parallel hand-maintained list. This was the
    // root cause of the KG holding only 32/65 tool entries — the seeder list
    // hadn't been updated since v2.0.
    const { TOOLS } = await import('../tools/types.js');
    let added = 0;
    let updated = 0;
    for (const t of TOOLS) {
        const r = await upsertEntity(tenantId, 'tool', 'Tool', t.name, {
            category: t.category,
            description: t.description,
            risk_level: t.riskLevel,
            bundled: true,
            // Persist the parameter contract so the dashboard graph can show the
            // tool's call signature on hover.
            param_keys: Object.keys(t.parameters?.properties ?? {}),
            required_params: (t.parameters?.required) ?? [],
        });
        if (r.created)
            added++;
        else
            updated++;
    }
    return { added, updated };
}
async function upsertRelationship(tenantId, sourceName, sourceKind, targetName, targetKind, type) {
    // Look up source and target by (tenant, kind, normalized name). Both must
    // exist for the edge to make sense — the entity seeders run first and we
    // run after them in seedAllToKnowledgeGraph, so this is safe.
    const srcLookup = await supabase
        .from('kg_entities')
        .select('id')
        .eq('tenant_id', tenantId)
        .eq('kind', sourceKind)
        .eq('name_normalized', sourceName.toLowerCase())
        .maybeSingle();
    const tgtLookup = await supabase
        .from('kg_entities')
        .select('id')
        .eq('tenant_id', tenantId)
        .eq('kind', targetKind)
        .eq('name_normalized', targetName.toLowerCase())
        .maybeSingle();
    const srcId = srcLookup.data?.id;
    const tgtId = tgtLookup.data?.id;
    if (!srcId || !tgtId)
        return false;
    // Dedup against the table's UNIQUE(tenant_id, source_id, target_id, type).
    const existing = await supabase
        .from('kg_relationships')
        .select('id')
        .eq('tenant_id', tenantId)
        .eq('source_id', srcId)
        .eq('target_id', tgtId)
        .eq('type', type)
        .maybeSingle();
    if (existing.data)
        return false;
    await supabase.from('kg_relationships').insert({
        id: randomUUID(),
        tenant_id: tenantId,
        source_id: srcId,
        target_id: tgtId,
        type,
        properties: JSON.stringify({ bundled: true, seeded_at: new Date().toISOString() }),
    });
    return true;
}
// Deterministic relationship seeds based on naming conventions. These are
// the edges that hold for EVERY tenant regardless of industry — they reflect
// the structural design of the toolset/policy/cron catalog rather than
// per-conversation discoveries (which the brain adds at runtime).
async function seedRelationships(tenantId) {
    let added = 0;
    // 1) Tool --GOVERNED_BY--> Policy. Most action tools have a policy of the
    //    same name (send_email, send_whatsapp, web_search, schedule_meeting,
    //    etc.). When the policy exists, the tool is governed by it.
    const { TOOLS } = await import('../tools/types.js');
    for (const tool of TOOLS) {
        const ok = await upsertRelationship(tenantId, tool.name, 'tool', tool.name, 'policy', 'GOVERNED_BY');
        if (ok)
            added++;
    }
    // 2) Cron --INVOKES--> Skill. Several crons map 1:1 to a skill of a
    //    canonical name, by convention.
    const cronSkillMap = [
        ['morning-brief', 'daily-brief'],
        ['evening-recap', 'daily-recap'],
        ['weekly-review', 'weekly-review'],
        ['fajr-reminder', 'prayer-reminder'],
        ['asr-reminder', 'prayer-reminder'],
        ['tadawul-open', 'market-brief'],
        ['tadawul-close', 'market-brief'],
        ['overdue-invoice-scan', 'collect-overdue'],
        ['hot-lead-followup', 'lead-followup'],
        ['agenda-checkin', 'agenda-checkin'],
        ['compliance-radar', 'compliance-radar'],
        ['research-cycle', 'autonomous-research'],
        ['skill-discovery', 'skill-library-discovery'],
        ['customer-followup-suggestions', 'lead-followup'],
        ['competitor-news-scan', 'competitive-intel'],
        ['industry-trend-scan', 'industry-trends'],
        ['seo-position-check', 'seo-monitor'],
        ['vendor-pricing-changes', 'vendor-watch'],
        ['business-health-check', 'business-health'],
        ['customer-segment-analysis', 'customer-segments'],
        ['skill-library-review', 'skill-library-review'],
    ];
    for (const [cronName, skillName] of cronSkillMap) {
        const ok = await upsertRelationship(tenantId, cronName, 'cron', skillName, 'skill', 'INVOKES');
        if (ok)
            added++;
    }
    // 3) Skill --USES--> Tool. Pragmatic mapping from skill filename hints to
    //    obvious tool dependencies. Skills the user hasn't installed simply
    //    won't match and no edge is created. We keep this conservative — a few
    //    high-confidence edges beat dozens of speculative ones.
    const skillToolHints = [
        [/email|inquiry|outreach/i, ['send_email']],
        [/whatsapp|wa-|message|reply/i, ['send_whatsapp']],
        [/meeting|appointment|booking|calendar/i, ['schedule_meeting']],
        [/quote|proposal|invoice/i, ['send_quote', 'issue_invoice']],
        [/research|news|intel|seo|market|trend/i, ['web_search', 'web_fetch']],
        [/lead|crm|customer/i, ['log_lead', 'qualify_lead', 'remember_fact']],
        [/brief|recap|review|report/i, ['recall_fact', 'create_pdf_report']],
        [/skill-library|skill-discovery/i, ['install_skill']],
    ];
    // Look up every Skill entity for this tenant and try-match its name against
    // hints, then create one edge per matching tool that also exists.
    const allSkills = await supabase
        .from('kg_entities')
        .select('name, properties')
        .eq('tenant_id', tenantId)
        .eq('kind', 'skill');
    const skillRows = (allSkills.data ?? []);
    for (const sk of skillRows) {
        const hay = `${sk.name} ${sk.properties?.filename ?? ''}`;
        for (const [re, tools] of skillToolHints) {
            if (!re.test(hay))
                continue;
            for (const toolName of tools) {
                const ok = await upsertRelationship(tenantId, sk.name, 'skill', toolName, 'tool', 'USES');
                if (ok)
                    added++;
            }
        }
    }
    return { added };
}
export async function seedAllToKnowledgeGraph(tenantId) {
    log.info({ tenantId }, 'seeding all out-of-box items into knowledge graph');
    const [skills, cron, policies, tools] = await Promise.all([
        seedSkills(tenantId),
        seedCronJobs(tenantId),
        seedPolicies(tenantId),
        seedTools(tenantId),
    ]);
    // Relationships run AFTER entities so the lookups in upsertRelationship
    // hit existing rows.
    const rels = await seedRelationships(tenantId);
    const totalEntities = skills.added + skills.updated +
        cron.added + cron.updated +
        policies.added + policies.updated +
        tools.added + tools.updated;
    log.info({ tenantId, skills, cron, policies, tools, relationships: rels, totalEntities }, 'KG seed complete');
    return { skills, cron, policies, tools, totalEntities };
}
//# sourceMappingURL=kg-seeder.js.map