import { parse } from "@babel/parser";
import { generateObject } from "ai";
import { z } from "zod";
import { getFunctionExpressionBody, parseExpression, } from "../../parsing/util.js";
import { getLogger } from "../../util/logger.js";
import { wrapError } from "../llm/stream/errors.js";
import llmobs from "../llmobs/index.js";
import { SKILL_PATH_PREFIXES, SkillLayer } from "../skills/types.js";
import { sendUserMessageChannel } from "../state-machine/helpers/peer.js";
import { getToolCallSignature } from "../util/tool-signature.js";
import { getToolCallArguments, getToolErrorCode, getToolErrorMessage, getToolOutput, } from "./tool-message-utils.js";
import { ToolExecutionError } from "./tools2/types.js";
import { PermissionLevel } from "./tools2/types.js";
/**
 * Extract fact ID from an on-demand fact path.
 * - org: skills/org/{factId}/KNOWLEDGE.md -> { layer: 'org', factId }
 * - user: skills/user/{factId}/KNOWLEDGE.md -> { layer: 'user', factId }
 * - integration: skills/integrations/{integrationId}/{factId}/KNOWLEDGE.md -> { layer: 'integration', integrationId, factId }
 */
function parseOnDemandFactPath(filePath) {
    if (filePath.startsWith(SKILL_PATH_PREFIXES[SkillLayer.ORG])) {
        const relativePath = filePath.slice(SKILL_PATH_PREFIXES[SkillLayer.ORG].length);
        const factId = relativePath.split("/")[0];
        return factId ? { layer: "org", factId } : null;
    }
    if (filePath.startsWith(SKILL_PATH_PREFIXES[SkillLayer.USER])) {
        const relativePath = filePath.slice(SKILL_PATH_PREFIXES[SkillLayer.USER].length);
        const factId = relativePath.split("/")[0];
        return factId ? { layer: "user", factId } : null;
    }
    if (filePath.startsWith(SKILL_PATH_PREFIXES[SkillLayer.INTEGRATION])) {
        const relativePath = filePath.slice(SKILL_PATH_PREFIXES[SkillLayer.INTEGRATION].length);
        const parts = relativePath.split("/");
        const integrationId = parts[0];
        const factId = parts[1];
        return integrationId && factId
            ? { layer: "integration", integrationId, factId }
            : null;
    }
    return null;
}
// Track pending on-demand fact reads by toolCallId
const pendingOnDemandFactReads = new Map();
const ERROR_SUMMARY_TIMEOUT_MS = 5_000;
const ERROR_SUMMARY_MAX_ERROR_CHARS = 2_000;
async function generateErrorSummary(llmProvider, toolName, error) {
    const abortController = new AbortController();
    const timeoutId = setTimeout(() => {
        abortController.abort();
    }, ERROR_SUMMARY_TIMEOUT_MS);
    try {
        const model = llmProvider.modelForTask("fast");
        const clippedError = error.length > ERROR_SUMMARY_MAX_ERROR_CHARS
            ? `${error.slice(0, ERROR_SUMMARY_MAX_ERROR_CHARS)}…`
            : error;
        const result = await generateObject({
            model,
            schema: z.object({
                summary: z
                    .string()
                    .describe("A one-line summary of the error (max 10 words)"),
            }),
            prompt: `Summarize this API error in one short sentence (max 10 words) for a regular person to understand. Be concise and direct.

Tool: ${toolName}
Error: ${clippedError}`,
            abortSignal: abortController.signal,
        });
        return result.object?.summary?.trim();
    }
    catch (err) {
        getLogger().warn("Failed to generate error summary", { toolName, err });
        return undefined;
    }
    finally {
        clearTimeout(timeoutId);
    }
}
export const getFilesChangedInDraftState = async (services) => {
    const { edits, additions } = await services.draftInterface.getDraftChanges();
    return [...edits, ...additions];
};
export const normalizeToBlockStatement = (value) => {
    return llmobs.trace({ name: "normalizeToBlockStatement", kind: "task" }, () => {
        if (!value) {
            return "{ return undefined; }";
        }
        // If it's not a string, convert to JSON and wrap
        if (typeof value !== "string") {
            return `{ return ${JSON.stringify(value)}; }`;
        }
        const trimmedValue = value.trim();
        // If it's already a block statement, return as-is
        if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) {
            return trimmedValue;
        }
        try {
            // Try to parse as an arrow function first
            const arrowFunctionPattern = /^\s*\([^)]*\)\s*=>\s*/;
            if (arrowFunctionPattern.test(trimmedValue)) {
                // Parse the arrow function and extract its body
                const ast = parse(trimmedValue, {
                    sourceType: "module",
                    plugins: ["typescript", "jsx"],
                });
                const expression = ast.program.body[0];
                if (expression?.type === "ExpressionStatement" &&
                    expression.expression.type === "ArrowFunctionExpression") {
                    const functionBody = getFunctionExpressionBody(expression.expression);
                    const trimmedBody = functionBody.trim();
                    // Check if this is a block statement vs an object expression
                    // Block statements typically contain: return, var/let/const, if/for/while, semicolons
                    const blockStatementPatterns = [
                        /\breturn\b/,
                        /\b(var|let|const)\b/,
                        /\b(if|for|while|switch|try)\b/,
                        /;/,
                    ];
                    const hasBlockStatementPatterns = blockStatementPatterns.some((pattern) => pattern.test(trimmedBody));
                    // If it contains block statement patterns, it's likely a block body that was extracted
                    // from an arrow function, so we need to wrap it in braces
                    if (hasBlockStatementPatterns) {
                        // Check if it already has braces (complete block)
                        if (trimmedBody.startsWith("{") && trimmedBody.endsWith("}")) {
                            return trimmedBody;
                        }
                        else {
                            // Block content without braces, wrap it
                            return `{\n${trimmedBody}\n}`;
                        }
                    }
                    // For object expressions or simple expressions, check if it needs braces
                    if (trimmedBody.startsWith("{") && trimmedBody.endsWith("}")) {
                        // It's an object expression, wrap in return
                        return `{ return ${trimmedBody}; }`;
                    }
                    // Otherwise, wrap in return statement
                    return `{ return ${functionBody}; }`;
                }
            }
            // Try to parse as a regular expression and wrap in return
            const parsedExpression = parseExpression(trimmedValue);
            if (parsedExpression) {
                return `{ return ${trimmedValue}; }`;
            }
        }
        catch (_) {
            // If parsing fails, treat as string literal
            return `{ return ${JSON.stringify(trimmedValue)}; }`;
        }
        // Fallback: wrap in return statement
        return `{ return ${trimmedValue}; }`;
    });
};
const getStoreMessageType = (type) => {
    switch (type) {
        case "text-start":
        case "text-delta":
        case "text-end":
            return "text";
        case "reasoning-start":
        case "reasoning-delta":
        case "reasoning-end":
            return "reasoning";
        default:
            return "text";
    }
};
export const processStreamChunk = async (chunk, clark, services, group) => {
    switch (chunk.type) {
        case "text-start":
        case "reasoning-start": {
            services.chatSessionStore.recordInProgressMessage({
                id: chunk.id,
                text: "",
                type: getStoreMessageType(chunk.type),
            });
            break;
        }
        case "text-end":
        case "reasoning-end": {
            const messageId = chunk.id;
            if (messageId !== undefined) {
                await services.chatSessionStore.commitMessage(messageId, getStoreMessageType(chunk.type), group);
            }
            // Reset partial response now that the message has been committed.
            // This prevents already-committed text from being duplicated if
            // the user interrupts during a later step in the same generation.
            clark.updateContext({ partialResponse: undefined });
            break;
        }
        case "text-delta":
            void clark.context.peer?.call.aiPushMessage({
                role: "assistant",
                type: "text-delta",
                content: chunk.text,
                ...(group && { group }),
            });
            // record the text in the chat session store
            services.chatSessionStore.recordInProgressMessage({
                id: chunk.id,
                text: chunk.text,
                type: getStoreMessageType(chunk.type),
            });
            clark.updateContext((ctx) => ({
                ...ctx,
                partialResponse: {
                    text: (ctx.partialResponse?.text ?? "") + chunk.text,
                    lastUpdatedAt: Date.now(),
                },
            }));
            break;
        case "reasoning-delta": {
            void clark.context.peer?.call.aiPushMessage({
                role: "assistant",
                type: "reasoning",
                content: chunk.text,
                ...(group && { group }),
            });
            // record the reasoning in the chat session store
            services.chatSessionStore.recordInProgressMessage({
                id: chunk.id,
                text: chunk.text,
                type: getStoreMessageType(chunk.type),
            });
            break;
        }
        case "tool-call": {
            // Check if this is an on-demand fact read (build_readFile on org/user/integration skill path)
            const input = chunk.input;
            const filePath = chunk.toolName === "build_readFile" &&
                typeof input.filePath === "string"
                ? input.filePath
                : null;
            const factPathInfo = filePath ? parseOnDemandFactPath(filePath) : null;
            if (factPathInfo) {
                // Track this as a pending on-demand fact read - we'll send a knowledge message on result
                pendingOnDemandFactReads.set(chunk.toolCallId, factPathInfo);
                getLogger().info(`[processStreamChunk] Detected on-demand fact read: ${filePath}`, factPathInfo);
                // Don't send the normal tool message for on-demand fact reads
                break;
            }
            const signature = getToolCallSignature(chunk.toolName, chunk.input);
            const args = await getToolCallArguments(chunk.toolName, chunk.input, clark);
            const permission = await services.toolRegistry.checkPermission(chunk.toolName, chunk.input);
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            void sendUserMessage({
                role: "assistant",
                content: "",
                type: "tool",
                tool: chunk.toolName,
                toolCallId: chunk.toolCallId,
                args,
                signature,
                requiresUserPermission: permission !== PermissionLevel.ALLOW,
                ...(group && { group }),
            });
            break;
        }
        case "tool-result": {
            // Check if this was an on-demand fact read
            const pendingFactRead = pendingOnDemandFactReads.get(chunk.toolCallId);
            if (pendingFactRead) {
                pendingOnDemandFactReads.delete(chunk.toolCallId);
                // Get the fact from KnowledgeManager to send a knowledge message
                const fact = services.knowledgeManager.getFactById(pendingFactRead.factId);
                if (fact) {
                    const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
                    // Build the LoadedFacts structure with just this one fact
                    const facts = {
                        org: pendingFactRead.layer === "org" ? [fact] : [],
                        user: pendingFactRead.layer === "user" ? [fact] : [],
                        integrations: pendingFactRead.layer === "integration" &&
                            pendingFactRead.integrationId
                            ? { [pendingFactRead.integrationId]: [fact] }
                            : {},
                    };
                    void sendUserMessage({
                        type: "knowledge",
                        role: "assistant",
                        content: "",
                        status: "validated",
                        facts,
                    });
                    getLogger().info(`[processStreamChunk] Sent knowledge message for on-demand fact: ${fact.name}`);
                }
                else {
                    getLogger().warn(`[processStreamChunk] Could not find fact for on-demand read: ${pendingFactRead.factId}`);
                }
                // Don't send the normal tool-result message for on-demand fact reads
                break;
            }
            const rememberKnowledgeOutput = chunk.toolName === "rememberKnowledge" &&
                chunk.output &&
                typeof chunk.output === "object"
                ? (("candidates" in chunk.output
                    ? chunk.output
                    : "value" in chunk.output &&
                        chunk.output.value &&
                        typeof chunk.output.value === "object" &&
                        "candidates" in chunk.output.value
                        ? chunk.output.value
                        : null) ?? null)
                : null;
            if (rememberKnowledgeOutput &&
                Array.isArray(rememberKnowledgeOutput.candidates)) {
                const candidates = rememberKnowledgeOutput.candidates;
                if (candidates.length > 0) {
                    const duplicateHint = Array.isArray(rememberKnowledgeOutput.recentlySavedMatches) &&
                        rememberKnowledgeOutput.recentlySavedMatches.length > 0
                        ? ` ${rememberKnowledgeOutput.recentlySavedMatches.length} may already exist in recent saves.`
                        : "";
                    const messageId = crypto.randomUUID();
                    const payload = {
                        role: "assistant",
                        id: messageId,
                        type: "remember_knowledge_draft",
                        content: candidates.length > 1
                            ? `I drafted ${candidates.length} knowledge entries for your review.${duplicateHint} Select which ones to save.`
                            : `I drafted this knowledge entry for your review.${duplicateHint}`,
                        candidates,
                    };
                    clark.updateContext((ctx) => ({
                        ...ctx,
                        pendingAppKnowledge: {
                            messageId,
                            knowledgeEntries: candidates,
                        },
                        rememberKnowledgeCandidatesForTurn: undefined,
                    }));
                    const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
                    void sendUserMessage(payload);
                    break;
                }
            }
            const output = getToolOutput(chunk.toolName, chunk.output);
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            void sendUserMessage({
                role: "assistant",
                content: "",
                type: "tool-result",
                toolCallId: chunk.toolCallId,
                toolName: chunk.toolName,
                output,
            });
            break;
        }
        case "tool-error": {
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            const error = chunk.error instanceof ToolExecutionError && chunk.error.cause
                ? getToolErrorMessage(chunk.error.cause)
                : getToolErrorMessage(chunk.error);
            const errorCode = getToolErrorCode(chunk.error);
            const errorSummary = await generateErrorSummary(services.llmProvider, chunk.toolName, error);
            void sendUserMessage({
                role: "assistant",
                content: "",
                type: "tool-error",
                toolCallId: chunk.toolCallId,
                toolName: chunk.toolName,
                error,
                errorCode,
                ...(errorSummary && { errorSummary }),
            });
            break;
        }
        case "error": {
            const wrappedError = wrapError(chunk.error);
            if (wrappedError.isRetryable) {
                return;
            }
            throw chunk.error;
        }
        case "finish": {
            if (chunk.finishReason === "content-filter") {
                const errorMessage = "The response was filtered due to content policy violations.";
                void clark.context.peer?.call.aiPushMessage({
                    role: "assistant",
                    type: "text",
                    content: `❌ ${errorMessage}`,
                });
            }
            else if (chunk.finishReason === "length") {
                const warningMessage = "Response was truncated due to length limits.";
                void clark.context.peer?.call.aiPushMessage({
                    role: "assistant",
                    type: "text",
                    content: `⚠️ ${warningMessage}`,
                });
            }
            break;
        }
        case "abort": {
            const abortMessage = "The request was aborted.";
            getLogger().info(`Stream aborted: ${abortMessage}`);
            break;
        }
    }
};
/**
 * Process stream chunks specifically for the test subagent.
 *
 * Key differences from processStreamChunk:
 * - Text/reasoning messages are suppressed (not sent to UI)
 * - browser_playwright_action results are processed as test steps
 * - endTestRun is processed normally
 * - Other tool calls are logged but not displayed prominently
 */
export const processTestStreamChunk = async (chunk, clark, services) => {
    const logger = getLogger();
    const testRunId = clark.context.currentTestRunId;
    switch (chunk.type) {
        // Suppress all text output for test subagent
        case "text-start":
        case "text-end":
        case "reasoning-start":
        case "reasoning-end":
            // Silently skip - no UI messages for test subagent text
            break;
        case "text-delta":
            // Log suppressed text for debugging, but don't send to UI
            if (chunk.text.trim()) {
                logger.debug(`[TestSubagent] Suppressed text: ${chunk.text.substring(0, 100)}${chunk.text.length > 100 ? "..." : ""}`);
            }
            break;
        case "reasoning-delta":
            // Log suppressed reasoning for debugging, but don't send to UI
            if (chunk.text.trim()) {
                logger.debug(`[TestSubagent] Suppressed reasoning: ${chunk.text.substring(0, 100)}${chunk.text.length > 100 ? "..." : ""}`);
            }
            break;
        case "tool-call": {
            const args = await getToolCallArguments(chunk.toolName, chunk.input, clark);
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            const isPlaywrightAction = chunk.toolName === "browser_playwright_action";
            const isEndTestRun = chunk.toolName === "endTestRun";
            if (isPlaywrightAction || isEndTestRun) {
                // Send tool call message for test-related tools
                void sendUserMessage({
                    role: "assistant",
                    content: "",
                    type: "tool",
                    tool: chunk.toolName,
                    toolCallId: chunk.toolCallId,
                    args,
                });
            }
            else {
                // Log other tool calls but don't send prominently to UI
                logger.debug(`[TestSubagent] Tool call: ${chunk.toolName}`);
            }
            break;
        }
        case "tool-result": {
            const isPlaywrightAction = chunk.toolName === "browser_playwright_action";
            const isEndTestRun = chunk.toolName === "endTestRun";
            const output = getToolOutput(chunk.toolName, chunk.output);
            if (isPlaywrightAction && testRunId) {
                output.testRunId = testRunId;
            }
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            if (isPlaywrightAction || isEndTestRun) {
                // Send tool result for test-related tools
                void sendUserMessage({
                    role: "assistant",
                    content: "",
                    type: "tool-result",
                    toolCallId: chunk.toolCallId,
                    toolName: chunk.toolName,
                    output,
                });
            }
            else {
                // Log other tool results
                logger.debug(`[TestSubagent] Tool result: ${chunk.toolName}`);
            }
            break;
        }
        case "tool-error": {
            // Always send tool errors so user can see what went wrong
            const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
            void sendUserMessage({
                role: "assistant",
                content: "",
                type: "tool-error",
                toolCallId: chunk.toolCallId,
                toolName: chunk.toolName,
                error: getToolErrorMessage(chunk.error),
            });
            logger.error(`[TestSubagent] Tool error: ${chunk.toolName} - ${getToolErrorMessage(chunk.error)}`);
            break;
        }
        case "error": {
            logger.error(`[TestSubagent] Stream error: ${chunk.error instanceof Error ? chunk.error.message : String(chunk.error)}`);
            throw chunk.error;
        }
        case "finish": {
            if (chunk.finishReason === "content-filter") {
                logger.error("[TestSubagent] Response filtered due to content policy");
            }
            else if (chunk.finishReason === "length") {
                logger.warn("[TestSubagent] Response truncated due to length limits");
            }
            logger.info(`[TestSubagent] Stream finished: ${chunk.finishReason}`);
            break;
        }
        case "abort": {
            logger.info("[TestSubagent] Stream aborted");
            break;
        }
    }
};
//# sourceMappingURL=utils.js.map