/**
 * src/knowledge/unified.ts
 * طبقة المعرفة الموحدة — Unified Knowledge Layer
 * Combines conversation graph (Graphiti) + document graph (Graphify)
 * into a single query interface for the agent's brain.
 */
import { createChildLogger } from '../utils/logger.js';
import { graphiti } from './graphiti.js';
import { graphify } from './graphify.js';
import { extractCustomerInsights } from './extraction.js';
const log = createChildLogger('knowledge:unified');
// ─── Initialization ──────────────────────────────────────────────────
let initialized = false;
/**
 * Initialize both knowledge graph clients
 * Call once at startup — safe to call multiple times
 */
export async function initKnowledgeGraphs() {
    if (initialized)
        return;
    try {
        await Promise.all([
            graphiti.connect(),
            graphify.connect(),
        ]);
        initialized = true;
        log.info('Knowledge graphs initialized');
    }
    catch (err) {
        log.error({ error: err.message }, 'Failed to initialize knowledge graphs');
        throw err;
    }
}
/**
 * (Round-7 fix) Track in-flight background learning so shutdown can drain
 * them. Previously learnFromConversation fired-and-forgot; on SIGTERM the
 * process exited while episodes were still being extracted, losing the
 * conversation data for that turn.
 */
const inFlightLearning = new Set();
/**
 * Graceful shutdown — wait for any in-flight background learning to finish
 * (up to 5 seconds) before closing KG handles.
 */
export async function closeKnowledgeGraphs() {
    if (inFlightLearning.size > 0) {
        log.info({ pending: inFlightLearning.size }, 'draining in-flight learning before shutdown');
        const drained = Promise.allSettled(Array.from(inFlightLearning));
        const timeout = new Promise((resolve) => setTimeout(() => resolve('timeout'), 5_000));
        const winner = await Promise.race([drained.then(() => 'drained'), timeout]);
        if (winner === 'timeout') {
            log.warn({ pending: inFlightLearning.size }, 'shutdown drain timeout — some learning may be lost');
        }
    }
    await Promise.all([
        graphiti.close(),
        graphify.close(),
    ]);
    initialized = false;
    log.info('Knowledge graphs closed');
}
// ─── Core Queries ────────────────────────────────────────────────────
/**
 * Query the "Business Brain" — combines customer knowledge + document knowledge
 * Returns a context string ready to inject into the agent prompt
 */
export async function queryBusinessBrain(tenantId, customerId, question) {
    const [customerKnowledge, documentResults] = await Promise.all([
        // Customer context from conversation graph
        graphiti.queryCustomerKnowledge(tenantId, customerId).catch(err => {
            log.warn({ tenantId, customerId, error: err.message }, 'Customer knowledge query failed');
            return { entities: [], relationships: [] };
        }),
        // Business context from document graph
        graphify.queryDocuments(tenantId, question).catch(err => {
            log.warn({ tenantId, error: err.message }, 'Document query failed');
            return [];
        }),
    ]);
    // Format customer context
    let customerContext = '';
    if (customerKnowledge.entities.length > 0) {
        const entityLines = customerKnowledge.entities.map(e => {
            const name = e.name ?? 'unknown';
            const type = e.entityType ?? e.type ?? '';
            return `- ${name} (${type})`;
        });
        const relLines = customerKnowledge.relationships.map(r => {
            return `- ${r.from} → ${r.type} → ${r.to}`;
        });
        customerContext = `## معلومات العميل (Customer Knowledge)\n`;
        customerContext += `### كيانات معروفة:\n${entityLines.join('\n')}\n`;
        if (relLines.length > 0) {
            customerContext += `### علاقات:\n${relLines.join('\n')}\n`;
        }
    }
    // Format business context
    let businessContext = '';
    if (documentResults.length > 0) {
        const lines = documentResults.map(r => {
            const name = r.name ?? '';
            const type = r.entityType ?? '';
            const sources = Array.isArray(r.sourceFiles) ? r.sourceFiles.join(', ') : '';
            return `- ${name} (${type})${sources ? ` [from: ${sources}]` : ''}`;
        });
        businessContext = `## معلومات العمل (Business Knowledge)\n${lines.join('\n')}\n`;
    }
    // Combined context for the agent prompt
    const combined = [customerContext, businessContext].filter(Boolean).join('\n');
    return {
        customerContext,
        businessContext,
        combined: combined || '(لا توجد معلومات إضافية متاحة)',
    };
}
/**
 * Learn from a conversation — extract and store knowledge asynchronously
 * Runs in background so it doesn't block the response
 */
export async function learnFromConversation(tenantId, customerId, customerName, conversationText) {
    // (C5 fix) recordRawEpisode is now AWAITED on the hot path. It does not
    // call the LLM — uses regex stubs — so latency is bounded (~10-30ms).
    // Worth blocking on so the next turn's queryBusinessBrain sees recent
    // entities. The richer LLM extraction below remains fire-and-forget.
    try {
        await graphiti.recordRawEpisode(tenantId, customerId, customerName, conversationText);
    }
    catch (err) {
        log.debug({ err: err.message }, 'raw episode record failed (non-fatal)');
    }
    // (Round-7 fix) Retain the promise in `inFlightLearning` so shutdown can
    // wait for it. Removed from the set when settled regardless of outcome.
    const safeName = customerName && customerName.trim() ? customerName : `Customer ${customerId.slice(0, 8)}`;
    const p = graphiti
        .addConversationEpisode(tenantId, customerId, safeName, conversationText)
        .then((result) => {
        log.info({ tenantId, customerId, ...result }, 'Background learning complete');
    })
        .catch((err) => {
        log.error({ tenantId, customerId, error: err.message }, 'Background learning failed');
    })
        .finally(() => {
        inFlightLearning.delete(p);
    });
    inFlightLearning.add(p);
}
/**
 * Awaitable version of learnFromConversation. Use this in flows where you
 * want the next read-from-KG to definitely see the new episode (e.g.,
 * unit tests, eval runners, manual replay). Production message paths
 * should keep using the fire-and-forget version above.
 */
export async function learnFromConversationSync(tenantId, customerId, customerName, conversationText) {
    const safeName = customerName && customerName.trim() ? customerName : `Customer ${customerId.slice(0, 8)}`;
    try {
        await graphiti.addConversationEpisode(tenantId, customerId, safeName, conversationText);
    }
    catch (err) {
        log.warn({ tenantId, customerId, err: err.message }, 'sync learn failed');
    }
}
/**
 * Get a quick brief of everything known about a customer
 * Used for pre-loading agent context at conversation start
 */
export async function getCustomerBrief(tenantId, customerId, conversationHistory) {
    const knowledge = await graphiti
        .queryCustomerKnowledge(tenantId, customerId)
        .catch(() => ({ entities: [], relationships: [] }));
    // Optionally extract insights from recent conversation
    let insights = null;
    if (conversationHistory && conversationHistory.length > 50) {
        try {
            const extracted = await extractCustomerInsights(conversationHistory);
            insights = {
                preferences: extracted.preferences,
                sentiment: extracted.sentiment,
                topics: extracted.topics,
            };
        }
        catch {
            // Non-critical, proceed without insights
        }
    }
    // Build a human-readable summary
    const parts = [];
    if (knowledge.entities.length > 0) {
        const interests = knowledge.relationships
            .filter(r => r.type === 'interested_in' || r.type === 'prefers' || r.type === 'mentioned')
            .map(r => r.to);
        if (interests.length > 0) {
            parts.push(`مهتم بـ: ${interests.join('، ')}`);
        }
        const locations = knowledge.entities
            .filter(e => e.type === 'Location' || e.entityType === 'Location')
            .map(e => e.name);
        if (locations.length > 0) {
            parts.push(`مواقع ذات صلة: ${locations.join('، ')}`);
        }
    }
    if (insights) {
        if (insights.preferences.length > 0) {
            parts.push(`تفضيلات: ${insights.preferences.join('، ')}`);
        }
        parts.push(`المزاج: ${insights.sentiment}`);
    }
    return {
        customerId,
        knownEntities: knowledge.entities,
        relationships: knowledge.relationships,
        insights,
        summary: parts.length > 0 ? parts.join(' | ') : 'عميل جديد — لا توجد معلومات سابقة',
    };
}
/**
 * Build/refresh the document graph for a tenant
 */
export async function buildTenantDocumentGraph(tenantId, documentsDir, refresh = false) {
    if (refresh) {
        await graphify.refreshGraph(tenantId);
        log.info({ tenantId }, 'Document graph refreshed');
    }
    return graphify.buildDocumentGraph(tenantId, documentsDir);
}
//# sourceMappingURL=unified.js.map