/**
 * src/workflows/recorder.ts
 *
 * Workflow capture-and-recall — Muassis remembers how it handled prior
 * requests so it can replay the recipe next time.
 *
 * After each successful tool-using turn we save:
 *   - The user's original message + a normalized keyword set
 *   - The intent the router classified
 *   - The full tool-call sequence (name + args)
 *   - A short response excerpt for context
 *
 * Before the next turn's LLM call we look up the closest prior recipe
 * using keyword Jaccard similarity. The match (if good enough) is
 * injected into the system prompt as a worked example: "When a customer
 * asked X last time, you called these tools in this order — they were
 * happy with the outcome." The LLM doesn't blindly replay — it adapts
 * the pattern, swapping out parameters as needed.
 *
 * After 3+ successful invocations of similar workflows we promote the
 * pattern to a permanent skill .md file under <dataDir>/skills/, so it
 * becomes part of the universal toolbelt.
 *
 * Storage: workflows table in the existing SQLite DB (or Postgres if
 * MUASSIS_MODE=control-plane). Schema is created lazily on first write
 * so we don't need a migration step.
 */
import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { getDataDir } from '../utils/data-dir.js';
const log = createChildLogger('workflows');
const SCHEMA_READY = new Set(); // tenant ids whose tables we've ensured
const PROMOTE_TO_SKILL_AT = 3; // invocations
const MIN_KEYWORD_OVERLAP = 0.35; // Jaccard threshold for recall
// Don't recall on tiny messages: short greetings and one-word inputs match
// random past workflows by accident (e.g. "yo" hitting a 1-keyword overlap
// with a stored "open chrome" workflow → Jaccard 1.0 → bogus recall →
// Llama re-runs the old task on an unrelated message). Below this floor
// the user message just doesn't have enough signal to base a recall on.
const MIN_KEYWORDS_FOR_RECALL = 3;
// Hard-skip recall on these patterns no matter what — pure social turns,
// the agent should respond conversationally, not relive a past task.
const GREETING_PATTERNS = [
    /^(yo|hi|hey|hello|sup|howdy|test|thanks?|thx|ok|okay|good|nice|cool|gm|gn)[\s!.,?]*$/i,
    /^(مرحبا|مرحباً|اهلا|أهلا|أهلاً|السلام|السلام عليكم|صباح الخير|مساء الخير|تمام|شكرا|شكراً|تجربة)[\s!.,؟،]*$/u,
];
const STOPWORDS = new Set([
    // Arabic
    'في', 'من', 'إلى', 'الى', 'على', 'عن', 'هذا', 'هذه', 'ذلك', 'تلك', 'لي',
    'أن', 'ان', 'كان', 'كانت', 'يا', 'هل', 'ما', 'لا', 'و', 'أو', 'او',
    'لكن', 'إذا', 'اذا', 'الذي', 'التي', 'كيف', 'متى', 'لماذا', 'أين', 'اين',
    'بس', 'الحين', 'يعني', 'عشان', 'بدي', 'ابي', 'أبي', 'ابغى', 'أبغى',
    // English
    'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'to', 'of', 'in',
    'on', 'at', 'and', 'or', 'but', 'if', 'for', 'with', 'as', 'this',
    'that', 'these', 'those', 'i', 'me', 'my', 'you', 'your', 'we', 'our',
    'do', 'does', 'did', 'have', 'has', 'had', 'will', 'would', 'should',
    'could', 'can', 'so', 'just', 'now', 'today', 'tomorrow', 'please',
    'me', 'us', 'them', 'it', 'its', 'her', 'him', 'his',
]);
function normalizeForKeywords(s) {
    return s
        .toLowerCase()
        // Strip Arabic diacritics + tatweel
        .replace(/[ً-ٰٟـ]/g, '')
        // Keep Arabic, Latin, digits — drop punctuation
        .replace(/[^؀-ۿݐ-ݿa-z0-9\s]/g, ' ')
        .split(/\s+/)
        .filter(t => t.length > 1 && !STOPWORDS.has(t));
}
function jaccard(a, b) {
    if (a.length === 0 || b.length === 0)
        return 0;
    const setA = new Set(a);
    const setB = new Set(b);
    let intersect = 0;
    for (const t of setA)
        if (setB.has(t))
            intersect++;
    const union = new Set([...setA, ...setB]).size;
    return intersect / union;
}
async function ensureSchema(tenantId) {
    if (SCHEMA_READY.has(tenantId))
        return;
    // The workflows table now lives in supabase/sqlite-schema.sql so it's
    // created at gateway startup. The exec_sql RPC has been removed for
    // security. We just touch a select to confirm the table exists.
    try {
        const probe = await supabase.from('workflows').select('id').limit(1);
        if (probe.error) {
            throw new Error(probe.error.message);
        }
        SCHEMA_READY.add(tenantId);
    }
    catch (err) {
        log.warn({ tenantId, error: err.message }, 'Failed to ensure workflows schema (will retry next call)');
    }
}
/**
 * Record a successful tool-using turn as a workflow. Either creates a
 * new row or bumps invocation_count on the closest existing match (so
 * "fetch latest AI news" + "what's today's AI news" merge into one
 * recipe rather than fragmenting).
 */
export async function recordWorkflow(params) {
    const { tenantId, userMessage, intent, toolSequence, responseExcerpt } = params;
    if (toolSequence.length === 0)
        return;
    const keywords = normalizeForKeywords(userMessage);
    if (keywords.length === 0)
        return;
    await ensureSchema(tenantId);
    try {
        // Look for an existing similar workflow to merge into.
        const { data: existing } = await supabase
            .from('workflows')
            .select('id, keywords, invocation_count')
            .eq('tenant_id', tenantId)
            .eq('intent', intent)
            .limit(20);
        let mergeTarget = null;
        let bestSim = 0;
        for (const row of (existing ?? [])) {
            const rowKeywords = row.keywords.split(' ');
            const sim = jaccard(keywords, rowKeywords);
            if (sim > bestSim && sim >= 0.6) {
                bestSim = sim;
                mergeTarget = { id: row.id, invocation_count: row.invocation_count };
            }
        }
        const now = new Date().toISOString();
        if (mergeTarget) {
            await supabase
                .from('workflows')
                .update({
                invocation_count: mergeTarget.invocation_count + 1,
                last_invoked_at: now,
                tool_sequence: JSON.stringify(toolSequence),
                response_excerpt: responseExcerpt.slice(0, 400),
            })
                .eq('id', mergeTarget.id);
            log.info({ tenantId, workflowId: mergeTarget.id, count: mergeTarget.invocation_count + 1, similarity: bestSim }, 'Merged into existing workflow');
            // Auto-promote to a permanent skill once it crosses the threshold.
            if (mergeTarget.invocation_count + 1 >= PROMOTE_TO_SKILL_AT) {
                promoteToSkill(tenantId, mergeTarget.id, userMessage, intent, toolSequence).catch(err => log.warn({ tenantId, workflowId: mergeTarget.id, error: err.message }, 'Skill promotion failed'));
            }
            return;
        }
        const id = randomUUID();
        await supabase.from('workflows').insert({
            id,
            tenant_id: tenantId,
            user_message: userMessage.slice(0, 500),
            keywords: keywords.join(' '),
            intent,
            tool_sequence: JSON.stringify(toolSequence),
            response_excerpt: responseExcerpt.slice(0, 400),
            invocation_count: 1,
            last_invoked_at: now,
            created_at: now,
        });
        log.info({ tenantId, workflowId: id, intent, toolCount: toolSequence.length }, 'New workflow recorded');
    }
    catch (err) {
        log.warn({ tenantId, error: err.message }, 'Failed to record workflow (non-fatal)');
    }
}
/**
 * Look up the closest prior workflow for an incoming message. Returns
 * formatted prompt-injection text, or null if no match crosses the
 * keyword-overlap threshold.
 */
export async function recallWorkflow(params) {
    const { tenantId, userMessage, intent } = params;
    // Skip recall on greetings/social turns — these have ~zero task content
    // and any keyword overlap with stored workflows is incidental.
    const trimmed = userMessage.trim();
    for (const re of GREETING_PATTERNS) {
        if (re.test(trimmed)) {
            log.debug({ tenantId, userMessage: trimmed }, 'Skipping recall: greeting pattern');
            return null;
        }
    }
    const keywords = normalizeForKeywords(userMessage);
    // Need enough distinct content words to make a Jaccard similarity score
    // meaningful. Below the floor, even a 100% match is statistical noise.
    if (keywords.length < MIN_KEYWORDS_FOR_RECALL) {
        log.debug({ tenantId, keywordCount: keywords.length, floor: MIN_KEYWORDS_FOR_RECALL }, 'Skipping recall: too few keywords');
        return null;
    }
    await ensureSchema(tenantId);
    try {
        const query = supabase
            .from('workflows')
            .select('user_message, keywords, intent, tool_sequence, invocation_count, response_excerpt')
            .eq('tenant_id', tenantId)
            .order('invocation_count', { ascending: false })
            .limit(40);
        if (intent)
            query.eq('intent', intent);
        const { data } = await query;
        if (!data || data.length === 0)
            return null;
        let best = null;
        for (const row of data) {
            const sim = jaccard(keywords, row.keywords.split(' '));
            if (sim >= MIN_KEYWORD_OVERLAP && (!best || sim > best.sim || (sim === best.sim && row.invocation_count > best.invocation_count))) {
                best = { ...row, sim };
            }
        }
        if (!best)
            return null;
        let toolSeq = [];
        try {
            toolSeq = JSON.parse(best.tool_sequence);
        }
        catch { /* ignore malformed */ }
        if (toolSeq.length === 0)
            return null;
        const toolList = toolSeq.map((t, i) => {
            const argsSummary = JSON.stringify(t.args).slice(0, 120);
            return `   ${i + 1}. \`${t.name}\` ${argsSummary === '{}' ? '' : `← ${argsSummary}`}`;
        }).join('\n');
        return [
            '## 💡 مرجع: وصفة سابقة (للاسترشاد فقط — ليست المهمة الحالية)',
            `كان العميل قبل فترة قال: "${best.user_message}" — وحينها استخدمت هذه الأدوات بهذا الترتيب:`,
            toolList,
            `(تطابق ${Math.round(best.sim * 100)}%، تم استخدامها ${best.invocation_count} مرة)`,
            ``,
            `⚠️ هذه مجرد ذاكرة، **ليست أمر تنفيذ**. أعد قراءة طلب العميل الحالي بعناية:`,
            `   • إذا كان الطلب الحالي يطابق فعلاً نية الوصفة (نفس النوع من المهام) → اقتبس منها.`,
            `   • إذا كان الطلب مختلف (تحية، سؤال، طلب جديد) → **تجاهل الوصفة تماماً** ورد على الطلب الحقيقي.`,
            `   • لا تنفّذ خطوات الوصفة لمجرد وجودها هنا.`,
        ].join('\n');
    }
    catch (err) {
        log.warn({ tenantId, error: err.message }, 'Workflow recall failed (non-fatal)');
        return null;
    }
}
/**
 * Once a workflow has been invoked PROMOTE_TO_SKILL_AT times, write it
 * as a permanent skill markdown under <dataDir>/skills/. The skill
 * loader picks these up automatically. We only promote once — guarded
 * by checking if the file already exists.
 */
async function promoteToSkill(tenantId, workflowId, exampleMessage, intent, toolSequence) {
    const dir = join(getDataDir(), 'skills');
    mkdirSync(dir, { recursive: true });
    // Use a stable filename so re-promotion doesn't create duplicates.
    const safeName = `learned-${intent.replace(/[^a-z0-9_-]/gi, '_').toLowerCase()}-${workflowId.slice(0, 8)}.md`;
    const skillPath = join(dir, safeName);
    if (existsSync(skillPath))
        return;
    const toolList = toolSequence.map((t, i) => `   ${i + 1}. **${t.name}** — ${JSON.stringify(t.args).slice(0, 200)}`).join('\n');
    const keywords = normalizeForKeywords(exampleMessage).slice(0, 8).join(', ');
    const md = `---
name: مهارة مكتسبة — ${intent}
priority: 2
keywords: ${keywords}
---

# مهارة مكتسبة: ${intent}

## متى تستخدم
عندما يطلب العميل شيئاً مشابهاً لـ:
> ${exampleMessage}

## كيف تنفّذ
استدعِ هذه الأدوات بنفس الترتيب (مع تكييف القيم على طلب العميل الحالي):
${toolList}

## أمثلة
- ${exampleMessage}

---
*تم إنشاء هذه المهارة تلقائياً من ${PROMOTE_TO_SKILL_AT} تكرارات ناجحة.*
`;
    writeFileSync(skillPath, md, 'utf-8');
    log.info({ tenantId, workflowId, skillPath }, 'Workflow promoted to permanent skill');
}
//# sourceMappingURL=recorder.js.map