import { FactAccessType, FactEntityScope, } from "@superblocksteam/shared";
import { getErrorMeta } from "../../util/logger.js";
import { formatSystemPromptFacts, formatUserMessageFacts, generateOnDemandFactsSkillIndex, } from "./helpers.js";
const CACHE_TTL_MS = 1 * 60 * 1000; // 1 minute
/**
 * Manages knowledge/facts loading, caching, and tracking for a chat session.
 * - Caches facts with TTL to reduce RPC calls
 * - Tracks which facts have been sent to UI to only push net-new knowledge
 * - Provides on-demand fact retrieval for skill-based loading
 */
export class KnowledgeManager {
    cache = {};
    sentFactIds = new Set();
    /** Cache of individual facts by ID for on-demand retrieval */
    factById = new Map();
    isCacheValid(entry) {
        return Date.now() - entry.timestamp < CACHE_TTL_MS;
    }
    getCached(key) {
        const entry = this.cache[key];
        if (entry && this.isCacheValid(entry)) {
            return entry.facts;
        }
        return null;
    }
    setCache(key, facts) {
        this.cache[key] = {
            facts,
            timestamp: Date.now(),
        };
        // Index facts by ID for on-demand retrieval
        for (const fact of facts) {
            this.factById.set(fact.id, fact);
        }
    }
    /**
     * Get a fact by ID from the cache.
     * Only returns facts that have been previously loaded and cached.
     * Used by SkillFileHandler to serve on-demand fact content.
     */
    getFactById(factId) {
        return this.factById.get(factId);
    }
    /**
     * Partition facts by access type for separate handling.
     */
    partitionByAccessType(facts) {
        const alwaysInject = [];
        const onDemand = [];
        for (const fact of facts) {
            if (fact.accessType === FactAccessType.ALWAYS_INJECT) {
                alwaysInject.push(fact);
            }
            else if (fact.accessType === FactAccessType.ON_DEMAND) {
                onDemand.push(fact);
            }
        }
        return { alwaysInject, onDemand };
    }
    /**
     * Load all facts for an organization (both ALWAYS_INJECT and ON_DEMAND).
     * Facts are fetched in a single RPC call and partitioned client-side.
     */
    async loadOrgFacts(rpcClient, organizationId, logger) {
        const cacheKey = `organization:${organizationId}`;
        const cached = this.getCached(cacheKey);
        if (cached) {
            return this.partitionByAccessType(cached);
        }
        try {
            const facts = await rpcClient.call(async (client) => {
                const { data } = await client.call.v1.fact.list({
                    entityScope: FactEntityScope.ORG,
                    entityId: organizationId,
                    isEnabled: true,
                });
                return data?.facts ?? [];
            });
            this.setCache(cacheKey, facts);
            return this.partitionByAccessType(facts);
        }
        catch (error) {
            logger.warn("Failed to load org facts", { error });
            return { alwaysInject: [], onDemand: [] };
        }
    }
    /**
     * Load all facts for an integration (both ALWAYS_INJECT and ON_DEMAND).
     * Facts are fetched in a single RPC call and partitioned client-side.
     */
    async loadIntegrationFacts(rpcClient, integrationId, logger) {
        const cacheKey = `integration:${integrationId}`;
        const cached = this.getCached(cacheKey);
        if (cached) {
            return this.partitionByAccessType(cached);
        }
        try {
            const facts = await rpcClient.call(async (client) => {
                const { data } = await client.call.v1.fact.list({
                    entityScope: FactEntityScope.INTEGRATION,
                    entityId: integrationId,
                    isEnabled: true,
                });
                return data?.facts ?? [];
            });
            this.setCache(cacheKey, facts);
            return this.partitionByAccessType(facts);
        }
        catch (error) {
            logger.warn("Failed to load integration facts", { error });
            return { alwaysInject: [], onDemand: [] };
        }
    }
    /**
     * Load all facts for a user (both ALWAYS_INJECT and ON_DEMAND).
     * Facts are fetched in a single RPC call and partitioned client-side.
     */
    async loadUserFacts(rpcClient, userId, logger) {
        const cacheKey = `user:${userId}`;
        const cached = this.getCached(cacheKey);
        if (cached) {
            return this.partitionByAccessType(cached);
        }
        try {
            const facts = await rpcClient.call(async (client) => {
                const { data } = await client.call.v1.fact.list({
                    entityScope: FactEntityScope.USER,
                    entityId: userId,
                    isEnabled: true,
                });
                return data?.facts ?? [];
            });
            this.setCache(cacheKey, facts);
            return this.partitionByAccessType(facts);
        }
        catch (error) {
            logger.warn("Failed to load user facts", { error });
            return { alwaysInject: [], onDemand: [] };
        }
    }
    /**
     * Merge and de-duplicate integrations by id.
     * This is so we can load all facts when a user might have not tagged something
     * but Clark put it in the plan.
     */
    mergeIntegrations(promptIntegrations, planIntegrations) {
        const seenIds = new Set();
        return [...promptIntegrations, ...planIntegrations].filter((integration) => {
            if (seenIds.has(integration.id)) {
                return false;
            }
            seenIds.add(integration.id);
            return true;
        });
    }
    summarizeLoadedFactsForLogs(orgFacts, userFacts, integrationFactsResults) {
        return {
            integrationCount: integrationFactsResults.length,
            integrations: integrationFactsResults.map((result) => ({
                factCount: result.facts.length,
                id: result.integration.id,
                type: result.integration.type,
            })),
            orgCount: orgFacts.length,
            userCount: userFacts.length,
        };
    }
    async loadFactsPromptSections({ rpcClient, organizationId, userId, promptIntegrations, planIntegrations, logger, }) {
        try {
            const mergedIntegrations = this.mergeIntegrations(promptIntegrations, planIntegrations ?? []);
            // Load all facts for integrations (single call per integration)
            const integrationFactsPromises = mergedIntegrations.map(async (integration) => {
                const { alwaysInject, onDemand } = await this.loadIntegrationFacts(rpcClient, integration.id, logger);
                return { integration, alwaysInject, onDemand };
            });
            // Load all facts in parallel (single call per entity type)
            const [orgFacts, userFacts, ...integrationResults] = await Promise.all([
                this.loadOrgFacts(rpcClient, organizationId, logger),
                this.loadUserFacts(rpcClient, userId, logger),
                ...integrationFactsPromises,
            ]);
            // Build results for ALWAYS_INJECT facts (for formatUserMessageFacts)
            const alwaysInjectIntegrationResults = integrationResults.map((r) => ({
                integration: r.integration,
                facts: r.alwaysInject,
            }));
            const allIntegrationResults = integrationResults.map((r) => ({
                integration: r.integration,
                facts: [...r.alwaysInject, ...r.onDemand],
            }));
            logger.info("[knowledge-manager] loaded facts for prompt", this.summarizeLoadedFactsForLogs(orgFacts.alwaysInject, userFacts.alwaysInject, alwaysInjectIntegrationResults));
            // Format ALWAYS_INJECT facts for direct prompt injection
            const systemPromptSection = formatSystemPromptFacts(orgFacts.alwaysInject);
            const userMessageSection = formatUserMessageFacts(userFacts.alwaysInject, alwaysInjectIntegrationResults);
            // Generate skill index for ON_DEMAND facts
            const onDemandSkillIndex = generateOnDemandFactsSkillIndex(orgFacts.onDemand, userFacts.onDemand, allIntegrationResults) ?? "";
            // Combine all facts for tracking (both ALWAYS_INJECT and ON_DEMAND)
            const allOrgFacts = [...orgFacts.alwaysInject, ...orgFacts.onDemand];
            const allUserFacts = [...userFacts.alwaysInject, ...userFacts.onDemand];
            const allIntegrationFacts = integrationResults.reduce((acc, r) => {
                acc[r.integration.id] = [...r.alwaysInject, ...r.onDemand];
                return acc;
            }, {});
            return {
                systemPromptSection,
                userMessageSection,
                onDemandSkillIndex,
                facts: {
                    org: allOrgFacts,
                    user: allUserFacts,
                    integrations: allIntegrationFacts,
                },
            };
        }
        catch (error) {
            logger.error("Failed to load facts for system prompt", getErrorMeta(error));
            return {
                systemPromptSection: "",
                userMessageSection: "",
                onDemandSkillIndex: "",
                facts: {
                    org: [],
                    user: [],
                    integrations: {},
                },
            };
        }
    }
    /**
     * Given loaded facts, returns only the facts that haven't been sent to UI yet.
     * Also marks these facts as sent.
     */
    getNetNewFacts(facts) {
        const filterNetNew = (factList) => {
            return factList.filter((fact) => {
                if (this.sentFactIds.has(fact.id)) {
                    return false;
                }
                this.sentFactIds.add(fact.id);
                return true;
            });
        };
        const netNewIntegrations = {};
        for (const [integrationId, integrationFacts] of Object.entries(facts.integrations)) {
            const netNew = filterNetNew(integrationFacts);
            if (netNew.length > 0) {
                netNewIntegrations[integrationId] = netNew;
            }
        }
        return {
            org: filterNetNew(facts.org),
            user: filterNetNew(facts.user),
            integrations: netNewIntegrations,
        };
    }
    hasAnyFacts(facts) {
        return (facts.org.length > 0 ||
            facts.user.length > 0 ||
            Object.keys(facts.integrations).length > 0);
    }
    /**
     * Filter LoadedFacts to only include ALWAYS_INJECT facts.
     * Used to show only always-inject facts in the initial knowledge UI message.
     */
    filterAlwaysInjectFacts(facts) {
        const filterAlwaysInject = (factList) => {
            return factList.filter((fact) => fact.accessType === FactAccessType.ALWAYS_INJECT);
        };
        const alwaysInjectIntegrations = {};
        for (const [integrationId, integrationFacts] of Object.entries(facts.integrations)) {
            const filtered = filterAlwaysInject(integrationFacts);
            if (filtered.length > 0) {
                alwaysInjectIntegrations[integrationId] = filtered;
            }
        }
        return {
            org: filterAlwaysInject(facts.org),
            user: filterAlwaysInject(facts.user),
            integrations: alwaysInjectIntegrations,
        };
    }
}
//# sourceMappingURL=knowledge-manager.js.map