import crypto from "node:crypto";
import { AiMode, } from "@superblocksteam/library-shared/types";
import { FactAccessType, FactCreatedSource, FactEntityScope, } from "@superblocksteam/shared";
import { getErrorMeta } from "../../util/logger.js";
import { AGENT_NEEDS_USER_INPUT, } from "../state-machine/clark-fsm.js";
import { sendUserMessageChannel } from "../state-machine/helpers/peer.js";
import { parseJwt } from "../util/parse-jwt.js";
import { buildKnowledgeFeedbackMessage } from "./build-knowledge-feedback-message.js";
import { AppSkillsManager } from "./manager.js";
function resolveFactScope(scope) {
    if (scope === "org") {
        return FactEntityScope.ORG;
    }
    if (scope === "user") {
        return FactEntityScope.USER;
    }
    return FactEntityScope.INTEGRATION;
}
function resolveKnowledgeScopeSelection(selection) {
    if (selection) {
        return { scope: selection.entityScope, entityId: selection.entityId };
    }
    return { scope: "app" };
}
function extractFactId(result) {
    if (!result || typeof result !== "object") {
        return undefined;
    }
    const maybeFact = result;
    if (typeof maybeFact.id === "string") {
        return maybeFact.id;
    }
    if (typeof maybeFact.data?.id === "string") {
        return maybeFact.data.id;
    }
    return undefined;
}
/**
 * Persist approved knowledge entries to disk and send an `app_knowledge_saved` message.
 * On failure, sends a user-visible error message instead.
 */
export async function saveKnowledgeAndNotify(clark, params, chatSessionStore, appRootDirPath, responseToMessageId, knowledgeEntries, selectedKnowledgeIndices, selectedScopesByIndex, logLabel) {
    const sendUserMessage = sendUserMessageChannel(clark, chatSessionStore);
    const manager = new AppSkillsManager(appRootDirPath);
    let savedCount = 0;
    let deniedCount = 0;
    const savedFacts = [];
    const saveOutcomes = [];
    const userId = parseJwt(clark.context.jwt ?? "")?.userId;
    for (const [position, knowledgeEntry] of knowledgeEntries.entries()) {
        try {
            const action = knowledgeEntry.action ?? "create";
            const candidateIndex = selectedKnowledgeIndices[position] ?? position;
            const selection = resolveKnowledgeScopeSelection(selectedScopesByIndex.get(candidateIndex));
            if (selection.scope !== "app") {
                if (action === "delete") {
                    clark.logger.warn("[app-skills] Delete action is unsupported for non-app scope; skipping", {
                        action,
                        scope: selection.scope,
                        knowledgeName: knowledgeEntry.name,
                    });
                    continue;
                }
                const entityId = selection.entityId ??
                    (selection.scope === "org"
                        ? params.organizationId
                        : selection.scope === "user"
                            ? userId
                            : undefined);
                if (!entityId) {
                    throw new Error(`Missing entity id for knowledge copy scope '${selection.scope}'`);
                }
                const payload = {
                    name: knowledgeEntry.name,
                    description: knowledgeEntry.description,
                    content: knowledgeEntry.content,
                    entityScope: resolveFactScope(selection.scope),
                    entityId,
                    accessType: FactAccessType.ALWAYS_INJECT,
                    createdSource: FactCreatedSource.AGENT,
                };
                const createdFact = await params.rpcClient.call(async (client) => {
                    const rpcClient = client;
                    return await rpcClient.call.v1.fact.create(payload);
                });
                const createdFactId = extractFactId(createdFact);
                savedFacts.push({
                    candidateIndex,
                    scope: selection.scope,
                    factName: knowledgeEntry.name,
                    factId: createdFactId,
                });
                saveOutcomes.push({
                    candidateIndex,
                    scope: selection.scope,
                    factName: knowledgeEntry.name,
                    status: "created_remote",
                    factId: createdFactId,
                });
                savedCount += 1;
                continue;
            }
            if (action === "delete") {
                const targetName = (knowledgeEntry.targetFactName ?? knowledgeEntry.name).trim();
                const existing = await manager.findFactByName(targetName);
                if (!existing) {
                    clark.logger.warn("[app-skills] Skip delete: target app knowledge entry not found", { targetFactName: targetName });
                    saveOutcomes.push({
                        candidateIndex,
                        scope: "app",
                        factName: knowledgeEntry.name,
                        status: "skipped_not_found",
                    });
                    continue;
                }
                await manager.deleteFact(existing.path);
                savedFacts.push({
                    candidateIndex,
                    scope: "app",
                    factName: knowledgeEntry.name,
                });
                savedCount += 1;
                saveOutcomes.push({
                    candidateIndex,
                    scope: "app",
                    factName: knowledgeEntry.name,
                    status: "deleted",
                });
                continue;
            }
            if (action === "update") {
                const targetName = (knowledgeEntry.targetFactName ?? knowledgeEntry.name).trim();
                const existing = await manager.findFactByName(targetName);
                if (!existing) {
                    await manager.saveFact({
                        name: knowledgeEntry.name,
                        description: knowledgeEntry.description,
                        content: knowledgeEntry.content,
                    });
                    savedFacts.push({
                        candidateIndex,
                        scope: "app",
                        factName: knowledgeEntry.name,
                    });
                    savedCount += 1;
                    saveOutcomes.push({
                        candidateIndex,
                        scope: "app",
                        factName: knowledgeEntry.name,
                        status: "created",
                    });
                    continue;
                }
                await manager.updateFact(existing.path, {
                    name: knowledgeEntry.name,
                    description: knowledgeEntry.description,
                    content: knowledgeEntry.content,
                });
                savedFacts.push({
                    candidateIndex,
                    scope: "app",
                    factName: knowledgeEntry.name,
                });
                savedCount += 1;
                saveOutcomes.push({
                    candidateIndex,
                    scope: "app",
                    factName: knowledgeEntry.name,
                    status: "updated",
                });
                continue;
            }
            await manager.saveFact({
                name: knowledgeEntry.name,
                description: knowledgeEntry.description,
                content: knowledgeEntry.content,
            });
            savedFacts.push({
                candidateIndex,
                scope: "app",
                factName: knowledgeEntry.name,
            });
            savedCount += 1;
            saveOutcomes.push({
                candidateIndex,
                scope: "app",
                factName: knowledgeEntry.name,
                status: "created",
            });
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            deniedCount += 1;
            const candidateIndex = selectedKnowledgeIndices[position] ?? position;
            const selection = resolveKnowledgeScopeSelection(selectedScopesByIndex.get(candidateIndex));
            saveOutcomes.push({
                candidateIndex,
                scope: selection.scope,
                factName: knowledgeEntry.name,
                status: "failed",
                reason: message,
            });
            clark.logger.error(`[app-skills] Failed to save knowledge (${logLabel})`, getErrorMeta(err));
            const permissionDenied = /forbidden|permission|not authorized|unauthorized/i.test(message);
            try {
                await sendUserMessage({
                    role: "assistant",
                    type: "text",
                    content: permissionDenied
                        ? "I could not save that at the requested scope due to permissions. Try saving it as personal app knowledge."
                        : "I wasn't able to save that knowledge entry due to an error. Please try again.",
                });
            }
            catch {
                // socket may have dropped; nothing further to do
            }
        }
    }
    if (savedCount > 0) {
        const failedCount = saveOutcomes.filter((outcome) => outcome.status === "failed").length;
        const skippedCount = saveOutcomes.filter((outcome) => outcome.status.startsWith("skipped_")).length;
        const summaryParts = [`saved ${savedCount}`];
        if (skippedCount > 0) {
            summaryParts.push(`skipped ${skippedCount}`);
        }
        if (failedCount > 0) {
            summaryParts.push(`failed ${failedCount}`);
        }
        await sendUserMessage({
            role: "assistant",
            type: "app_knowledge_saved",
            content: `Knowledge update complete (${summaryParts.join(", ")} entr${knowledgeEntries.length === 1 ? "y" : "ies"}).`,
            knowledgeCount: savedCount,
            responseToMessageId,
            savedFacts,
            saveOutcomes,
        });
    }
    clark.updateContext((ctx) => ({
        ...ctx,
        recentKnowledgeSaveOutcomes: saveOutcomes.slice(-25),
    }));
    clark.logger.info(`[app-skills] Knowledge persistence complete (${logLabel})`, {
        count: savedCount,
        deniedCount,
    });
}
/**
 * Handle a user's approval or rejection of pending app knowledge from the
 * agent-planning handler. Resolves the pending entries, saves if approved,
 * and transitions to the appropriate next state.
 *
 * @returns `true` if this was a knowledge-approval flow (caller should `return`),
 *          `false` if the response was unrelated to pending app knowledge.
 */
export async function handleAppKnowledgeApprovalResponse(clark, params, request) {
    const responseMetadata = request.responseMetadata;
    if (responseMetadata?.type !== "remember_knowledge_draft_response" &&
        responseMetadata?.type !== "multi_choice_response" &&
        responseMetadata?.type !== "confirm_response") {
        return false;
    }
    const responseToMessageId = responseMetadata.responseToMessageId;
    let knowledgeToProcess;
    let selectedKnowledgeIndices = [];
    if (clark.context.pendingAppKnowledge?.messageId === responseToMessageId) {
        knowledgeToProcess = clark.context.pendingAppKnowledge.knowledgeEntries;
    }
    else if (responseMetadata.type === "remember_knowledge_draft_response") {
        const messages = await params.chatSessionStore.getMessages();
        const originalMessage = messages.find((msg) => msg.type === "remember_knowledge_draft" &&
            msg.id === responseToMessageId);
        if (originalMessage?.candidates?.length) {
            knowledgeToProcess = originalMessage.candidates;
        }
    }
    else if (responseMetadata.type === "multi_choice_response") {
        const messages = await params.chatSessionStore.getMessages();
        const originalMessage = messages.find((msg) => msg.type === "multi_choice" && msg.id === responseToMessageId);
        if (originalMessage?.knowledgeCandidates?.length) {
            knowledgeToProcess = originalMessage.knowledgeCandidates;
        }
    }
    else {
        const messages = await params.chatSessionStore.getMessages();
        const originalMessage = messages.find((msg) => msg.type === "confirm" && msg.id === responseToMessageId);
        if (originalMessage?.knowledgeEntry) {
            knowledgeToProcess = [
                originalMessage.knowledgeEntry,
            ];
        }
    }
    if (!knowledgeToProcess) {
        return false;
    }
    let selectedKnowledge = knowledgeToProcess;
    let approved = false;
    let selectedScopesByIndex = new Map();
    if (responseMetadata.type === "remember_knowledge_draft_response") {
        const selected = Array.isArray(responseMetadata.selectedCandidateIndices)
            ? responseMetadata.selectedCandidateIndices
            : [];
        if (selected.length === 0) {
            return false;
        }
        selectedKnowledgeIndices = selected.filter((i) => i >= 0 && i < knowledgeToProcess.length);
        selectedKnowledge = selectedKnowledgeIndices.map((i) => knowledgeToProcess[i]);
        approved = selectedKnowledge.length > 0;
        if (Array.isArray(responseMetadata.selectedScopes)) {
            selectedScopesByIndex = new Map(responseMetadata.selectedScopes.map((scopeSelection) => [
                scopeSelection.candidateIndex,
                scopeSelection,
            ]));
        }
    }
    else if (responseMetadata.type === "multi_choice_response") {
        const selected = Array.isArray(responseMetadata.selectedChoiceIndices)
            ? responseMetadata.selectedChoiceIndices
            : [];
        if (selected.length === 0 && request.prompt.trim().length > 0) {
            return false;
        }
        const denyIndex = knowledgeToProcess.length;
        const denyAll = selected.includes(denyIndex);
        selectedKnowledgeIndices = denyAll
            ? []
            : selected.filter((i) => i >= 0 && i < knowledgeToProcess.length);
        selectedKnowledge = selectedKnowledgeIndices.map((i) => knowledgeToProcess[i]);
        approved = selectedKnowledge.length > 0;
        if (Array.isArray(responseMetadata.selectedScopes)) {
            selectedScopesByIndex = new Map(responseMetadata.selectedScopes.map((scopeSelection) => [
                scopeSelection.candidateIndex,
                scopeSelection,
            ]));
        }
    }
    else {
        approved = Boolean(responseMetadata.approved);
        selectedKnowledgeIndices = [0];
        if (Array.isArray(responseMetadata.selectedScopes)) {
            selectedScopesByIndex = new Map(responseMetadata.selectedScopes.map((scopeSelection) => [
                scopeSelection.candidateIndex,
                scopeSelection,
            ]));
        }
    }
    const persistedMessageId = await params.chatSessionStore.recordUserResponse(request.prompt, responseMetadata.type, responseMetadata.type === "remember_knowledge_draft_response"
        ? {
            responseToMessageId,
            selectedCandidateIndices: responseMetadata.selectedCandidateIndices,
            selectedScopes: responseMetadata.selectedScopes,
        }
        : responseMetadata.type === "multi_choice_response"
            ? {
                responseToMessageId,
                selectedChoiceIndices: responseMetadata.selectedChoiceIndices,
                selectedScopes: responseMetadata.selectedScopes,
            }
            : {
                responseToMessageId,
                approved: responseMetadata.approved,
                selectedScopes: responseMetadata.selectedScopes,
            });
    if (persistedMessageId) {
        clark.updateContext({
            chatmessageId: persistedMessageId,
        });
    }
    const resumeMode = clark.context.currentMode ?? AiMode.BUILD;
    clark.updateContext((ctx) => ({
        ...ctx,
        pendingAppKnowledge: undefined,
        rememberKnowledgeCandidatesForTurn: undefined,
        explicitRememberKnowledgeIntentForTurn: undefined,
        pendingKnowledgeFreeTextTurn: undefined,
        interruptOriginalPrompt: undefined,
    }));
    if (approved) {
        await saveKnowledgeAndNotify(clark, params, params.chatSessionStore, params.appRootDirPath, responseToMessageId, selectedKnowledge, selectedKnowledgeIndices, selectedScopesByIndex, responseMetadata.type === "multi_choice_response"
            ? "multi_choice"
            : "confirm");
    }
    const feedbackText = buildKnowledgeFeedbackMessage(approved, knowledgeToProcess, selectedKnowledgeIndices, clark.context.recentKnowledgeSaveOutcomes ?? []);
    await clark.recordSystemMessage(params, "Knowledge update", feedbackText);
    // Knowledge approvals are terminal for the interaction.
    // Auto-continuing in PLAN mode can trigger an extra LLM turn that re-prompts
    // for the same knowledge and leaves the UI in a misleading loading state.
    void clark.send({
        type: AGENT_NEEDS_USER_INPUT,
    });
    clark.logger.info("[app-skills] Resumed after app knowledge approval response", {
        approved,
        resumedMode: resumeMode,
        didAutoContinue: false,
        responseType: responseMetadata.type,
        selectedKnowledgeIndices: responseMetadata.type === "multi_choice_response" ||
            responseMetadata.type === "remember_knowledge_draft_response"
            ? selectedKnowledgeIndices
            : undefined,
    });
    return true;
}
/**
 * After an LLM turn where Clark called `rememberKnowledge`, build and
 * send the approval prompt (confirm for single entry, multi_choice for many)
 * and transition to `$AwaitingUser`.
 *
 * @returns `true` if there were candidates to present (caller should `return`),
 *          `false` otherwise.
 */
export async function presentKnowledgeApprovalMessage(clark, chatSessionStore) {
    const candidates = clark.context.rememberKnowledgeCandidatesForTurn;
    if (!candidates?.length) {
        return false;
    }
    const messageId = crypto.randomUUID();
    const confirmPayload = candidates.length === 1 && candidates[0]
        ? {
            type: "confirm",
            text: candidates[0].action === "update"
                ? "Should I update this knowledge entry?"
                : "Should I remember this knowledge entry?",
            role: "assistant",
            content: candidates[0].action === "update"
                ? "Should I update this knowledge entry?"
                : "Should I remember this knowledge entry?",
            id: messageId,
            knowledgeEntry: {
                name: candidates[0].name,
                description: candidates[0].description,
                content: candidates[0].content,
                action: candidates[0].action ?? "create",
                targetFactName: candidates[0].targetFactName,
            },
        }
        : {
            type: "multi_choice",
            text: "Should I remember any of these knowledge entries?",
            role: "assistant",
            content: "Should I remember any of these knowledge entries?",
            id: messageId,
            choices: [
                ...candidates.map((c) => ({
                    text: c.name,
                    subText: c.description,
                    action: "approve",
                })),
                { text: "Don't save any", action: "deny" },
            ],
            selectionType: "multiple",
            knowledgeCandidates: candidates,
        };
    clark.updateContext({
        pendingAppKnowledge: { messageId, knowledgeEntries: candidates },
        rememberKnowledgeCandidatesForTurn: undefined,
    });
    const sendUserMessage = sendUserMessageChannel(clark, chatSessionStore);
    await sendUserMessage(confirmPayload);
    clark.logger.info("[app-skills] Presenting knowledge draft for user approval", {
        candidateCount: candidates.length,
    });
    void clark.send({
        type: AGENT_NEEDS_USER_INPUT,
    });
    return true;
}
//# sourceMappingURL=helpers.js.map