import { AiEntityType, AiGenerationState, AiMode, } from "@superblocksteam/library-shared/types";
import { generateShortId } from "../../../ids.js";
import { getErrorMeta, getLogger } from "../../../util/logger.js";
import { debugCache } from "../../agent/tools/debug-cache.js";
import { getModeAfterFactCapture, handleAppFactsApprovalResponse, } from "../../app-skills/helpers.js";
import { AttachmentsStore } from "../../attachments/index.js";
import { getActiveContext } from "../../llm/context-v2/utils/get-loaded-context.js";
import { NpmInstallError } from "../../types.js";
import { parseJwt } from "../../util/parse-jwt.js";
import { AGENT_PLANNED, AGENT_NEEDS_USER_INPUT, LLM_ERRORED, LLM_FINISHED_UNEXPECTEDLY, POST_PROCESSING_ERRORED, RUNTIME_REVIEW_ERRORED, USER_SENT_PROMPT, BUILD_SYSTEM_ERRORED, AGENT_CANCELED, } from "../clark-fsm.js";
import { sendChangeInfoHelper } from "../helpers/change-info.js";
import { handlerMetadata } from "../helpers/metadata.js";
import { sendUserMessageChannel, sendUserGenerationStateChannel, clearPendingToolPermissionRequest, } from "../helpers/peer.js";
import { transitionFrom } from "../helpers/transition.js";
// TODO: make dynamically configurable
const MAX_DEBUG_ATTEMPTS = 5;
/**
 * Gets user-facing error message for terminal LLM errors.
 *
 * Terminal errors are errors that cannot be retried (either because they're
 * inherently non-retryable or because we've exhausted retry attempts).
 *
 * @param error - The LLMProviderError with standardized type and context
 * @returns User-facing error message
 */
function getTerminalErrorMessage(error) {
    const { type, provider, data } = error;
    const isClarkError = data?.isClark === true;
    const effectiveProvider = isClarkError ? "clark" : provider;
    switch (type) {
        case "validation_error":
            return "There was an issue processing your request. Please review your input and try again.";
        case "auth_error":
            if (effectiveProvider === "clark") {
                return "Your session couldn't be authenticated. Please refresh the page and try again.";
            }
            return "There was an authentication issue. Please check your API key and permissions, then try again.";
        case "not_found":
            return "The requested resource could not be found. Please verify your configuration and try again.";
        case "rate_limit":
            return "Rate limit reached. Please wait a few minutes before trying again.";
        case "api_error":
            return "The AI model returned an error. Please retry your prompt below. If the problem persists, the model provider may be experiencing issues.";
        case "timeout":
            return "The request timed out. Please check your connection and try again.";
        case "overloaded":
            return "The model is currently busy. Please retry your prompt below.";
        case "network":
            return "A network error occurred. Please check your connection and try again.";
        case "retries_exhausted":
            return "The request failed after multiple attempts. Please refresh the page and try again.";
        case "unknown":
        default:
            return "Something went wrong. Please try again, or contact support if the issue persists.";
    }
}
export const doAgentPlanning = (clark, params) => {
    const transitionTo = transitionFrom(clark);
    const sendUserMessage = sendUserMessageChannel(clark, params.chatSessionStore);
    const sendUserGenerationState = sendUserGenerationStateChannel(clark);
    const sendChangeInfo = sendChangeInfoHelper(clark, params.chatSessionStore);
    const { getMeta, updateMeta } = handlerMetadata(clark);
    const debug = clark.tracer.wrap({ kind: "task" }, async (debuggingPromptIntro, filePaths = []) => {
        return params.clarkProfiler
            .getProfiler()
            .timeAsync("debug_session", async () => {
            let { debugCount = 0 } = getMeta() ?? {};
            if (!filePaths.includes("package.json")) {
                filePaths.push("package.json");
            }
            debugCount++;
            clark.tracer.annotate({
                tags: {
                    "debug.loop": true,
                    "debug.count": debugCount,
                },
            });
            updateMeta({ debugCount });
            clark.updateContext({
                usedDebugging: true,
            });
            if (debugCount > MAX_DEBUG_ATTEMPTS) {
                void transitionTo({
                    type: AGENT_NEEDS_USER_INPUT,
                    advice: "I've reached the maximum number of attempts to fix the errors. You may need to reject the changes and try again.",
                });
                return;
            }
            // sendUserMessage({
            //   type: "control",
            //   group: "debugging",
            //   status: "pending",
            //   text:
            //     debugCount === 1
            //       ? "I found some problems in the app. Let me address them."
            //       : `Still trying to fix the errors… (attempt ${debugCount} of ${MAX_DEBUG_ATTEMPTS})`,
            // });
            const diff = await params.draftInterface.getDraftDiff(filePaths);
            const debuggingPrompt = `${debuggingPromptIntro}

Below is the diff of the changes made to the relevant files:
---
${diff}
---

Address the errors and return the fixed code.`;
            getLogger().info(`[ai-service] debugging ${debugCount}: ${debuggingPromptIntro}`);
            void transitionTo({
                type: AGENT_PLANNED,
                userPrompt: debuggingPrompt,
                debugging: true,
                isUserPrompt: false, // Debugging prompts are system-generated
            });
        });
    });
    return async ({ event }) => {
        switch (event.type) {
            case USER_SENT_PROMPT: {
                // Clear error history for new generation
                debugCache.getErrorHistoryTracker().clear();
                const { request, peer, peerId } = event;
                const previousMode = clark.context.currentMode;
                // Tier 1 — hard-coded /remember command: immediately enter FACT mode.
                // Tier 2 — natural language intent is detected by Clark via the rememberFacts tool
                // (Clark switches itself to AiMode.FACT when it calls the tool from BUILD mode).
                const isRememberCommand = /^\s*\/remember\b/i.test(request.prompt);
                // If the user is responding to a pending fact-approval prompt, preserve
                // the current mode (FACT) so the full fact loop runs in the right mode
                // before anything else. The pendingAppFacts handler below restores the
                // previous mode after approval/rejection.
                const isFactApprovalResponse = (request.responseMetadata?.type === "multi_choice_response" ||
                    request.responseMetadata?.type === "confirm_response") &&
                    clark.context.pendingAppFacts?.messageId ===
                        request.responseMetadata.responseToMessageId;
                const effectiveMode = isRememberCommand
                    ? AiMode.FACT
                    : isFactApprovalResponse
                        ? clark.context.currentMode
                        : request.mode;
                clark.updateContext((context) => ({
                    ...context,
                    currentMode: effectiveMode,
                    modeBeforeFactCapture: isRememberCommand
                        ? previousMode
                        : context.modeBeforeFactCapture,
                    // Clear testing state when user sends a new prompt
                    currentTestRunId: undefined,
                    // Clear per-turn FACT mode state on each new user prompt
                    explicitRememberIntentForTurn: isRememberCommand ? true : undefined,
                    rememberFactsCandidatesForTurn: undefined,
                }));
                if (previousMode !== effectiveMode) {
                    clark.logger.info("[agent-planning] Mode changed", {
                        from: previousMode,
                        to: effectiveMode,
                        isRememberCommand,
                    });
                }
                // Record user message - use recordUserResponse if this is a response to an interactive message
                if (request.responseMetadata) {
                    if (request.responseMetadata.type === "tool_permission_response") {
                        const pendingToolPermissionRequest = clark.context.pendingToolPermissionRequest;
                        if (pendingToolPermissionRequest &&
                            pendingToolPermissionRequest.messageId ===
                                request.responseMetadata.responseToMessageId) {
                            if (request.responseMetadata.approved) {
                                params.entityPermissionStore.approveToolCall(pendingToolPermissionRequest.toolName, pendingToolPermissionRequest.rawInput);
                                request.responseMetadata.alwaysAllowPermissions?.forEach((permission) => {
                                    void params.entityPermissionStore.approve(permission.entityType, permission.entityId, permission.permissionType);
                                });
                            }
                            clearPendingToolPermissionRequest(clark);
                        }
                    }
                    else {
                        // If this is a response to an interactive message, clear it from context
                        clark.updateContext({
                            activeInteractiveMessage: undefined,
                        });
                        const handledFacts = await handleAppFactsApprovalResponse(clark, params, request);
                        if (handledFacts) {
                            return;
                        }
                        // If the user sent a chat message in response to a multi-choice fact
                        // approval (instead of selecting choices), restore the candidates so
                        // the model can see them and decide which facts to keep based on the
                        // user's chat response.
                        if (clark.context.pendingAppFacts) {
                            clark.updateContext((ctx) => ({
                                ...ctx,
                                rememberFactsCandidatesForTurn: ctx.pendingAppFacts?.facts,
                                currentMode: getModeAfterFactCapture(ctx),
                            }));
                        }
                        const metadata = request.responseMetadata.type === "multi_choice_response"
                            ? {
                                responseToMessageId: request.responseMetadata.responseToMessageId,
                                selectedChoiceIndices: request.responseMetadata.selectedChoiceIndices,
                            }
                            : request.responseMetadata.type === "confirm_response"
                                ? {
                                    responseToMessageId: request.responseMetadata.responseToMessageId,
                                    approved: request.responseMetadata.approved,
                                }
                                : {
                                    responseToMessageId: request.responseMetadata.responseToMessageId,
                                    approved: request.responseMetadata.approved,
                                    enableTesting: request.responseMetadata.enableTesting,
                                };
                        const chatmessageId = await params.chatSessionStore.recordUserResponse(request.prompt, request.responseMetadata.type, metadata);
                        if (chatmessageId) {
                            clark.updateContext({
                                chatmessageId,
                            });
                        }
                    }
                }
                else {
                    if (clark.context.activeInteractiveMessage) {
                        clark.updateContext({ activeInteractiveMessage: undefined });
                    }
                    // If the user continues with a new prompt instead of approving pending
                    // app knowledge, treat that as an implicit rejection and clear FACT-mode
                    // pending state so no stale candidates can be saved later.
                    if (clark.context.pendingAppFacts) {
                        clark.updateContext((ctx) => ({
                            ...ctx,
                            pendingAppFacts: undefined,
                            rememberFactsCandidatesForTurn: undefined,
                            explicitRememberIntentForTurn: undefined,
                            modeBeforeFactCapture: undefined,
                            currentMode: getModeAfterFactCapture(ctx),
                        }));
                    }
                    const promptForRecording = clark.context.interruptOriginalPrompt ?? request.prompt;
                    clark.updateContext({ interruptOriginalPrompt: undefined });
                    getLogger().info(`[agent-planning] Recording user message to chat session...`);
                    const chatmessageId = await params.chatSessionStore.recordUser(promptForRecording, {
                        attachments: request.promptContext?.attachments,
                    });
                    getLogger().info(`[agent-planning] User message recorded: ${chatmessageId ?? "no id returned"}`);
                    if (chatmessageId) {
                        clark.updateContext({
                            chatmessageId,
                        });
                    }
                }
                const userId = parseJwt(request.jwt)?.userId;
                if (request.promptContext?.attachments?.length && userId) {
                    try {
                        const context = getActiveContext(clark, {
                            contextManagerV2: params.contextManagerV2,
                            applicationId: params.applicationId,
                        });
                        if (context) {
                            const attachmentsStore = new AttachmentsStore();
                            const storedAttachments = attachmentsStore.appendAttachments(context, request.promptContext.attachments, request.prompt);
                            getLogger().info(`[agent-planning] Persisted ${storedAttachments.length} attachment(s) to context`, storedAttachments.map((a) => ({
                                id: a.id,
                                type: a.type,
                                fileName: a.fileName,
                                label: a.label,
                                insight: a.insight,
                                originalMessage: a.originalMessage,
                            })));
                        }
                        else {
                            clark.updateContext({
                                pendingAttachments: {
                                    attachments: request.promptContext.attachments,
                                    originalMessage: request.prompt,
                                },
                            });
                            getLogger().info(`[agent-planning] Context not loaded yet, buffered ${request.promptContext.attachments.length} attachment(s) for deferred persistence`);
                        }
                    }
                    catch (err) {
                        clark.updateContext({
                            pendingAttachments: {
                                attachments: request.promptContext.attachments,
                                originalMessage: request.prompt,
                            },
                        });
                        getLogger().error(`[agent-planning] Failed to persist attachments`, getErrorMeta(err));
                    }
                }
                // clear pending tool permission request if it exists
                clearPendingToolPermissionRequest(clark);
                if (!request?.jwt) {
                    getLogger().error(`[agent-planning] Missing or invalid JWT token in AI request (applicationId: ${params.applicationId})`);
                    void transitionTo({
                        type: AGENT_NEEDS_USER_INPUT,
                        advice: "Authentication failed. Please refresh the page and try again. If the problem persists, please contact support.",
                    });
                    return;
                }
                const initialElementIds = clark.context.initialElementIds ??
                    Object.keys(params.fileSystemInterface.getElementToFilePathMap() ?? {});
                const taggedEntities = request.promptContext?.entities;
                const availableIntegrations = request.promptContext?.integrations;
                try {
                    await params.integrationStore.fetchIntegrations(peer, availableIntegrations ?? []);
                }
                catch (error) {
                    getLogger().error(`[agent-planning] Failed to fetch integrations`, getErrorMeta(error));
                    void transitionTo({
                        type: AGENT_CANCELED,
                        advice: "Failed to fetch integrations from the browser. Please refresh the page and try again.",
                    });
                    return;
                }
                getLogger().info("Agent planning storing llmConfig in context:", request.llmConfig || {});
                // Log customer data access status
                const disabledTools = request.llmConfig?.disabledTools;
                const customerDataTools = ["queryIntegration"];
                const allCustomerDataToolsDisabled = customerDataTools.every((tool) => disabledTools?.includes(tool));
                if (allCustomerDataToolsDisabled) {
                    getLogger().info(`[agent-planning] Customer data access will be DISABLED (all query tools in disabledTools)`);
                }
                else if (disabledTools &&
                    customerDataTools.some((tool) => disabledTools.includes(tool))) {
                    const disabled = customerDataTools.filter((tool) => disabledTools.includes(tool));
                    getLogger().info(`[agent-planning] Partial customer data access: disabled=[${disabled.join(", ")}]`);
                }
                else {
                    getLogger().info(`[agent-planning] Customer data access will be ENABLED`);
                }
                getLogger().info(`[ai-service] Setting peer: ${peerId}`);
                clark.updateContext((context) => ({
                    ...context,
                    abortController: context.abortController ?? new AbortController(),
                    peer,
                    peerId,
                    initialElementIds,
                    runTimestamp: context.runTimestamp ??
                        new Date().toISOString().replace(/[:.]/g, "-"),
                    jwt: request.jwt,
                    llmConfig: request.llmConfig,
                    availableIntegrations: availableIntegrations ?? context.availableIntegrations,
                    taggedEntities: taggedEntities ?? context.taggedEntities,
                }));
                getLogger().info(`[ai-service] Peer set, sending GENERATING state`);
                void sendUserGenerationState({
                    state: AiGenerationState.GENERATING,
                });
                params.signals.emit("generationStarted");
                const components = params.appContextStore.componentSemanticsById;
                const entities = [
                    ...(request.promptContext?.entities ?? []),
                ].map((entity) => {
                    if (entity.type === AiEntityType.COMPONENT) {
                        return {
                            ...entity,
                            metadata: {
                                name: components[entity.id]?.bindName ??
                                    components[entity.id]?.tagName,
                            },
                        };
                    }
                    return entity;
                });
                if (entities.length) {
                    void sendUserMessage({
                        type: "context",
                        text: JSON.stringify({ entities }),
                    });
                }
                const isFactsEnabled = clark.context.llmConfig?.flags?.isFactsEnabled ?? false;
                const knowledgeMessageId = generateShortId();
                if (isFactsEnabled) {
                    void sendUserMessage({
                        id: knowledgeMessageId,
                        type: "knowledge",
                        role: "assistant",
                        content: "",
                        status: "pending",
                        facts: {
                            org: [],
                            user: [],
                            integrations: {},
                        },
                    });
                }
                else {
                    void sendUserMessage({
                        type: "reasoning",
                        text: "",
                    });
                }
                if (clark.context.hasSuggestions) {
                    // user sent a subsequent prompt on top of a draft
                    // reset debugging flag
                    clark.updateContext({
                        usedDebugging: false,
                    });
                }
                updateMeta({
                    request,
                });
                // speed up incremental linting/typecheck after llm step
                const isTypecheckEnabled = clark.context.llmConfig?.flags?.isTypecheckEnabled ?? true;
                if (isTypecheckEnabled) {
                    void params.appShell.runTypecheck();
                }
                void params.appShell.warmLinterCache();
                void transitionTo({
                    type: AGENT_PLANNED,
                    userPrompt: request.prompt,
                    promptContext: request.promptContext,
                    // Skip safety features if this is a response to an interactive message
                    // (not an original user prompt)
                    isUserPrompt: !request.responseMetadata,
                    knowledgeMessageId,
                });
                break;
            }
            case LLM_ERRORED: {
                const { error } = event;
                const advice = getTerminalErrorMessage(error);
                void transitionTo({
                    type: AGENT_CANCELED,
                    advice,
                });
                break;
            }
            case LLM_FINISHED_UNEXPECTEDLY: {
                const { finishReason } = event;
                // anthropic stop_reason is apparently mapped to ai-sdk finish reason
                // "end_turn": the model reached a natural stopping point
                // "max_tokens": we exceeded the requested max_tokens or the model's maximum
                // "stop_sequence": one of your provided custom stop_sequences was generated
                // "tool_use": the model invoked one or more tools
                switch (finishReason) {
                    case "length":
                    case "content-filter":
                    case "tool-calls":
                    case "error":
                    case "other":
                    case "unknown":
                    default:
                        void transitionTo({
                            type: AGENT_CANCELED,
                            advice: `An unexpected error occurred. Please try again.

If the problem persists, please open a support ticket. Finish reason: \`${finishReason}\``,
                        });
                        break;
                }
                break;
            }
            case "llmRequestedContext": {
                // placeholder for now
                break;
            }
            case POST_PROCESSING_ERRORED: {
                const { error } = event;
                if (error instanceof NpmInstallError) {
                    await debug(`The following error was encountered while installing dependencies:
---
${error.message}
---`, ["package.json"]);
                    break;
                }
                await debug(`The following error occured while processing files generated by the LLM:
---
${error.stack ?? error.message}
---`);
                break;
            }
            case RUNTIME_REVIEW_ERRORED: {
                const { tsErrorsByPath = {}, lintErrorsByPath = {}, integrationErrorsByPath = {}, } = event;
                const filePaths = new Set(Object.keys(tsErrorsByPath)
                    .concat(Object.keys(lintErrorsByPath))
                    .concat(Object.keys(integrationErrorsByPath)));
                for (const filePath of filePaths) {
                    void sendChangeInfo({
                        type: "error",
                        filePath,
                    });
                }
                let debugPrompt = "";
                if (Object.keys(tsErrorsByPath).length > 0) {
                    debugPrompt += `The following TypeScript errors were found in the project:
----
${Object.values(tsErrorsByPath)
                        .map((errors) => errors.join("\n"))
                        .join("\n")}
----`;
                }
                if (Object.keys(lintErrorsByPath).length > 0) {
                    debugPrompt += `The following linting errors were found in the project:
----
${Object.values(lintErrorsByPath)
                        .map((errors) => errors.join("\n"))
                        .join("\n")}
----`;
                }
                if (Object.keys(integrationErrorsByPath).length > 0) {
                    debugPrompt += `The following integration validation errors were found:
----
${Object.values(integrationErrorsByPath)
                        .map((errors) => errors.join("\n"))
                        .join("\n")}
----

Please only use the provided integrations and ensure all integration references match exactly.`;
                }
                await debug(debugPrompt, Array.from(filePaths));
                break;
            }
            case BUILD_SYSTEM_ERRORED: {
                const { error } = event;
                await debug(`The following error occured while building the project:
---
${error.stack ?? error.message}
---`);
                break;
            }
            default:
                throw new Error(`Unhandled event: ${event}`);
        }
    };
};
//# sourceMappingURL=agent-planning.js.map