import path from "node:path";
import { MCPClient, DOCS_MCP_URL, extractMcpTextContent, } from "@superblocksteam/clark";
import { AiToolExecutionErrorCode } from "@superblocksteam/library-shared/types";
import { getLogger } from "../../util/logger.js";
import { OperationQueue } from "../../util/operation-queue.js";
import { getActiveContext } from "../llm/context-v2/utils/index.js";
import llmobs from "../llmobs/index.js";
import { McpToolAdapter } from "../mcp/adapter/mcp-tool-adapter.js";
import { profileTool, } from "../profiler/tool-profiler.js";
import { createToolRecordingMiddleware } from "../recording/index.js";
import { clearPendingToolPermissionRequest, sendUserMessageChannel, } from "../state-machine/helpers/peer.js";
import { getToolCallSignature } from "../util/tool-signature.js";
import { outputLimiter, secretScanningMiddleware, toolLoggingMiddleware, } from "./middleware.js";
import { buildApiToolFactory } from "./tools/apis/build-api.js";
import { getApiDocsToolFactory } from "./tools/apis/get-api-docs.js";
import { getSdkApiDocsToolFactory } from "./tools/apis/get-sdk-api-docs.js";
import { checkTestApiPermissions, testApiToolFactory, } from "./tools/apis/test-api.js";
import { writeApiToolFactory } from "./tools/apis/write-api.js";
import { buildInstallPackagesToolFactory, buildWriteFileToolFactory, buildEditFileToolFactory, buildMultiEditFileToolFactory, buildReadFileToolFactory, buildListFilesToolFactory, buildDebugToolFactory, clearErrorCacheToolFactory, buildFinalizeToolFactory, buildManageChecklistToolFactory, buildDeleteFileToolFactory, checkDeleteFilePermissions, buildRenameFileToolFactory, buildReloadFileToolFactory, executeRequestToIntegrationToolFactory, openIntegrationSetupToolFactory, openExistingIntegrationToolFactory, updateIntegrationFieldsToolFactory, testIntegrationConnectionToolFactory, saveIntegrationToolFactory, closeIntegrationSetupToolFactory, getIntegrationFormSchemaToolFactory, listAvailableIntegrationTypesToolFactory, grepMetadataToolFactory, searchIntegrationsToolFactory, runCodeToolFactory, exitPlanModeToolFactory, askMultiChoiceToolFactory, askSearchableDropdownToolFactory, listAttachmentsToolFactory, downloadAttachmentsToolFactory, buildCaptureScreenshotToolFactory, buildGetRuntimeErrorsToolFactory, buildGetLogsToolFactory, startTestRunToolFactory, endTestRunToolFactory, updateTestCaseStatusToolFactory, getIntegrationTypesToolFactory, spawnCodingSubagentsToolFactory, } from "./tools/index.js";
import { presets, blockTools, forTools } from "./tools2/access-control.js";
import { summaryInjector, } from "./tools2/registry.js";
import { createDeployService, } from "./tools2/tools/deploy-service.js";
import { globToolFactory, grepToolFactory, lsToolFactory, webSearchToolFactory, webFetchToolFactory, gitToolFactory, gitRawToolFactory, rememberKnowledgeToolFactory, } from "./tools2/tools/index.js";
import { PermissionLevel, ToolCategory, ToolExecutionError, } from "./tools2/types.js";
function toJsonValue(value) {
    if (value === undefined) {
        return null;
    }
    try {
        return JSON.parse(JSON.stringify(value));
    }
    catch {
        return String(value);
    }
}
const fileToolConfig = {
    extractDisplayName: (parameters) => {
        const params = parameters;
        if (params.filePath && typeof params.filePath === "string") {
            const fileName = path.basename(params.filePath);
            return `:${fileName}`;
        }
        return "";
    },
    extractMetadata: (parameters) => {
        const params = parameters;
        return {
            filePath: params.filePath,
            hasOldString: "oldString" in params,
            hasNewString: "newString" in params,
            hasContent: "content" in params,
        };
    },
};
const componentToolConfig = {
    extractDisplayName: (parameters) => {
        const params = parameters;
        if (params.componentName && typeof params.componentName === "string") {
            return `:${params.componentName}`;
        }
        if (params.name && typeof params.name === "string") {
            return `:${params.name}`;
        }
        return "";
    },
    extractMetadata: (parameters) => {
        const params = parameters;
        return {
            componentName: params.componentName,
            name: params.name,
            varName: params.varName,
            eventName: params.eventName,
            timerName: params.timerName,
        };
    },
};
// Module-level cache for the docs MCP client so we avoid a remote HTTP
// roundtrip on every LLM generation turn.  The MCPClient internally caches
// tool definitions after the first successful connect().
// When the feature flag is off we eagerly discard the cached client so it
// does not linger in memory.
let docsMcpClientCache = null;
export const buildTools = async (clark, services) => {
    const scopeUpdateQueue = new OperationQueue();
    const sendUserMessage = sendUserMessageChannel(clark, services.chatSessionStore);
    const disabledTools = clark.context.llmConfig?.disabledTools;
    const registry = services.toolRegistry;
    registry.setLogger(clark.logger);
    const MIDDLEWARE_STACK_STATE_KEY = Symbol("tool.middleware.stack.state");
    const addInstrumentedMiddleware = (middlewareName, middleware) => {
        registry.addMiddleware((tool) => {
            const wrappedTool = middleware(tool);
            return {
                ...wrappedTool,
                execute: async (input, options) => {
                    const execOptions = options;
                    const existingState = execOptions?.[MIDDLEWARE_STACK_STATE_KEY];
                    const middlewareStackState = existingState ??
                        {
                            nextOrder: 0,
                            timings: [],
                            toolName: tool.name,
                        };
                    const createdState = existingState === undefined;
                    if (createdState && execOptions) {
                        execOptions[MIDDLEWARE_STACK_STATE_KEY] = middlewareStackState;
                    }
                    const order = middlewareStackState.nextOrder++;
                    const middlewareStartTime = performance.now();
                    try {
                        return await wrappedTool.execute(input, options);
                    }
                    finally {
                        middlewareStackState.timings.push({
                            middlewareName,
                            order,
                            inclusiveMs: Math.round(performance.now() - middlewareStartTime),
                        });
                        if (createdState) {
                            const ordered = [...middlewareStackState.timings].sort((a, b) => a.order - b.order);
                            const middlewareMetadata = ordered.reduce((acc, timing, i) => {
                                const child = ordered[i + 1];
                                const durationMs = child
                                    ? Math.max(0, timing.inclusiveMs - child.inclusiveMs)
                                    : timing.inclusiveMs;
                                acc[`middleware.${timing.middlewareName}`] = {
                                    durationMs,
                                    order: timing.order,
                                };
                                return acc;
                            }, {});
                            llmobs.annotate({
                                metadata: {
                                    middlewareCount: ordered.length,
                                    middlewareToolName: middlewareStackState.toolName,
                                    middlewareTotalMs: ordered[0]?.inclusiveMs ?? 0,
                                    ...middlewareMetadata,
                                },
                            });
                            if (execOptions) {
                                delete execOptions[MIDDLEWARE_STACK_STATE_KEY];
                            }
                        }
                    }
                },
            };
        });
    };
    // Clear any existing tools and rules
    registry.clear();
    // Set the entity permission store
    registry.setEntityPermissionStore(services.entityPermissionStore);
    // Set the prompt handler
    registry.setPromptHandler(async (tool, input, permissions, _context, options) => {
        const actionName = tool.getActionName?.(input);
        const formattedInput = tool.formatInput?.(input);
        const messageId = options?.toolCallId ?? crypto.randomUUID();
        if (clark.context.pendingToolPermissionRequest ||
            clark.context.activeInteractiveMessage) {
            clark.logger.info("skipping tool permission request as there is already a pending one or active interactive message", messageId);
            return;
        }
        const signature = getToolCallSignature(tool.name, input);
        // Capture timestamp before storing - used to truncate stale "blocked" error when permission is granted
        // The assistant message containing the tool call will have a timestamp <= this value,
        // while the error result will have a timestamp > this value
        const toolCallTimestamp = Date.now();
        // Store pending permission request in context for replay on reconnect
        clark.updateContext({
            pendingToolPermissionRequest: {
                messageId,
                toolName: tool.name,
                actionName,
                formattedInput,
                rawInput: input,
                permissions,
                signature,
                toolCallTimestamp,
            },
        });
        try {
            if (!clark.context.peer) {
                getLogger().warn(`[ai-service] Cannot request tool permission: no peer. tool=${tool.name}`);
            }
            await clark.context.peer?.call.aiRequestToolPermission({
                messageId,
                toolName: tool.name,
                actionName,
                formattedInput,
                rawInput: input,
                permissions,
                signature,
            });
        }
        catch (error) {
            // Clear pending request on error and notify UI
            clearPendingToolPermissionRequest(clark);
            throw error;
        }
    });
    // Add access control rules
    registry.addAccessRule(...presets.base());
    // Testing an API has possibility to prompt if we can statically detect mutations
    registry.addAccessRule(forTools("testApi", async (_tool, _context, input) => {
        return checkTestApiPermissions(services, input, clark);
    }));
    // Deleting files conditionally prompts for pages/APIs (except default Page1)
    registry.addAccessRule(forTools("build_deleteFile", async (_tool, _context, input) => {
        return checkDeleteFilePermissions(input);
    }));
    if (disabledTools && disabledTools.length > 0) {
        registry.addAccessRule(blockTools(disabledTools));
    }
    // Register all factory-based tools
    registry.register(buildInstallPackagesToolFactory, { clark, services });
    registry.register(buildWriteFileToolFactory, { clark, services });
    registry.register(buildEditFileToolFactory, { clark, services });
    registry.register(buildMultiEditFileToolFactory, { clark, services });
    registry.register(buildReadFileToolFactory, { services });
    registry.register(buildListFilesToolFactory, services);
    registry.register(buildDebugToolFactory, { clark, services });
    registry.register(clearErrorCacheToolFactory, { clark, services });
    registry.register(buildCaptureScreenshotToolFactory, { clark, services });
    registry.register(buildFinalizeToolFactory, { clark, services });
    registry.register(buildManageChecklistToolFactory, { clark, services });
    registry.register(spawnCodingSubagentsToolFactory, { clark, services });
    registry.register(buildDeleteFileToolFactory, { clark, services });
    registry.register(buildRenameFileToolFactory, { clark, services });
    registry.register(buildReloadFileToolFactory, { clark, services });
    registry.register(executeRequestToIntegrationToolFactory, {
        clark,
        services,
    });
    registry.register(runCodeToolFactory, clark);
    registry.register(grepMetadataToolFactory, { clark, services });
    registry.register(searchIntegrationsToolFactory, services);
    // Integration setup tools are feature-flagged
    if (services.features.integrationSetupEnabled) {
        registry.register(openIntegrationSetupToolFactory, { clark });
        registry.register(openExistingIntegrationToolFactory, { clark });
        registry.register(updateIntegrationFieldsToolFactory, { clark });
        registry.register(testIntegrationConnectionToolFactory, { clark });
        registry.register(saveIntegrationToolFactory, {
            clark,
            integrationStore: services.integrationStore,
        });
        registry.register(closeIntegrationSetupToolFactory, { clark });
        registry.register(getIntegrationFormSchemaToolFactory, { clark });
        registry.register(listAvailableIntegrationTypesToolFactory, {
            integrationStore: services.integrationStore,
        });
    }
    registry.register(exitPlanModeToolFactory, { clark, services });
    registry.register(askMultiChoiceToolFactory, { clark, services });
    registry.register(askSearchableDropdownToolFactory, { clark, services });
    registry.register(listAttachmentsToolFactory, { clark, services });
    if (services.features.clarkAttachmentDownloadEnabled === true) {
        registry.register(downloadAttachmentsToolFactory, { clark, services });
    }
    const isAppKnowledgeEnabled = clark.context.llmConfig?.flags?.isAppKnowledgeEnabled ?? false;
    if (isAppKnowledgeEnabled) {
        registry.register(rememberKnowledgeToolFactory, { clark, services });
    }
    // YAML API tools - only when SDK API is disabled
    if (!services.features.sdkApiEnabled) {
        registry.register(writeApiToolFactory, services);
        if (services.features.useSystemSkills) {
            registry.register(getIntegrationTypesToolFactory, services);
        }
        else {
            registry.register(getApiDocsToolFactory, services);
        }
        registry.register(buildApiToolFactory, {
            clark,
            services,
            scopeUpdateQueue,
        });
    }
    // SDK API flow uses a dedicated SDK docs tool.
    if (services.features.sdkApiEnabled) {
        registry.register(getSdkApiDocsToolFactory, services);
    }
    // testApi always available (tests existing APIs in either mode)
    registry.register(testApiToolFactory, { clark, services });
    registry.register(globToolFactory, { appShell: services.appShell });
    registry.register(grepToolFactory, {});
    registry.register(lsToolFactory, { appShell: services.appShell });
    registry.register(buildGetRuntimeErrorsToolFactory, { clark, services });
    registry.register(buildGetLogsToolFactory, { clark, services });
    // Web tools are feature-flagged
    if (services.features.webToolsEnabled) {
        registry.register(webSearchToolFactory, {});
        registry.register(webFetchToolFactory, {
            llmProvider: services.llmProvider,
        });
    }
    // Superblocks docs MCP tool is feature-flagged.
    // The MCPClient is cached at module level so the remote HTTP roundtrip
    // (connect → tools/list) only happens once per session instead of on
    // every LLM generation turn.  Subsequent buildTools() calls reuse the
    // already-connected client whose tool definitions are cached in memory.
    // When the flag is off we discard the cached client to free resources.
    if (!services.features.superblocksDocsMcpEnabled) {
        docsMcpClientCache = null;
    }
    else {
        try {
            const logger = clark.logger;
            if (!docsMcpClientCache) {
                docsMcpClientCache = new MCPClient(DOCS_MCP_URL, logger);
            }
            const client = docsMcpClientCache;
            if (!client.isConnected()) {
                await client.connect();
            }
            const mcpAdapter = new McpToolAdapter(registry, logger);
            const registeredTools = await mcpAdapter.addServer({
                name: "superblocks_docs",
                url: DOCS_MCP_URL,
                category: ToolCategory.METADATA,
                defaultPermission: PermissionLevel.ALLOW,
                toolNameTransform: (_serverName, toolName) => toolName,
                toModelOutput: (_toolName) => ({ output }) => {
                    const textContent = extractMcpTextContent(output);
                    if (textContent) {
                        return {
                            type: "text",
                            value: textContent,
                        };
                    }
                    return {
                        type: "json",
                        value: toJsonValue(output),
                    };
                },
            }, client);
            logger.info(`[Docs MCP] ✓ Registered ${registeredTools.length} docs tools: ${registeredTools.join(", ")}`);
        }
        catch (error) {
            docsMcpClientCache = null;
            const errorMessage = String(error);
            services.logger.warn(`[Docs MCP] Error initializing Superblocks docs MCP server: ${errorMessage}`);
        }
    }
    // Git tools — always registered when native git is enabled so they can
    // surface setup-required signals before native git has been connected.
    if (services.features.nativeGitEnabled) {
        const gitService = services.gitService;
        const syncService = services.syncService;
        const gitOperationLock = services.gitOperationLock;
        const onGitOperationComplete = services.onGitOperationComplete;
        const snapshotUploader = services.syncService;
        const deployService = gitService && snapshotUploader
            ? createDeployService({
                applicationId: services.applicationId,
                gitService,
                getJwt: () => clark.context.jwt,
                snapshotUploader,
                superblocksBaseUrl: services.superblocksBaseUrl,
            })
            : undefined;
        const onGitSetupRequired = async () => {
            await sendUserMessage({
                role: "system",
                type: "git_setup_required",
                content: "",
                applicationId: services.applicationId,
                reason: "git_not_connected",
            });
        };
        if (services.features.nativeGitRawEnabled) {
            registry.register(gitRawToolFactory, {
                gitService,
                gitOperationLock,
                onGitOperationComplete,
                onGitSetupRequired,
            });
        }
        else {
            registry.register(gitToolFactory, {
                gitService,
                syncService,
                gitOperationLock,
                onDeploySuccess: (commitId, gitSha) => {
                    clark.context.peer?.call
                        .notifyDeploySuccess({
                        commitId,
                        gitSha,
                    })
                        ?.catch((err) => {
                        getLogger().warn("[git] Failed to notify UI of deploy success", {
                            commitId,
                            gitSha,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    });
                },
                onGitOperationComplete,
                onGitSetupRequired,
                applicationId: services.applicationId,
                deployService,
            });
        }
    }
    // Initialize Playwright MCP server and register browser tools
    try {
        const { mcpServerManager } = services;
        const logger = clark.logger;
        // Get browser context from Clark (contains iframe URL and init/bootstrap data from UI)
        const browserContext = clark.context.browserContext;
        const isTestingEnabled = clark.context.llmConfig?.flags?.isTestingEnabled;
        if (!browserContext || !isTestingEnabled) {
            // Skip Playwright MCP server initialization but continue to register other tools
        }
        else {
            const { iframeBaseUrl, initData, bootstrapData } = browserContext;
            // Start the MCP server with init and bootstrap data for direct iframe access
            // This simulates the parent window sending sb-init and sb-bootstrap-response messages
            const serverUrl = await mcpServerManager.start({
                appUrl: iframeBaseUrl,
                jwt: clark.context.jwt,
                headless: true,
                initData,
                bootstrapData,
                saveArtifact: services.appShell.saveGeneratedArtifact.bind(services.appShell),
                runTimestamp: clark.context.runTimestamp,
            });
            const mcpClient = new MCPClient(serverUrl, logger);
            await mcpClient.connect();
            const mcpAdapter = new McpToolAdapter(registry, logger);
            const registeredTools = await mcpAdapter.addServer({
                name: "playwright",
                url: serverUrl,
                category: ToolCategory.TEST,
                defaultPermission: PermissionLevel.ALLOW,
                toolNameTransform: (_serverName, toolName) => `browser_${toolName}`,
                skipOutputLimit: (_toolName) => (_output) => true,
                toModelOutput: (_toolName) => ({ output }) => {
                    if (output &&
                        typeof output === "object" &&
                        "screenshot" in output &&
                        typeof output.screenshot === "string") {
                        // Determine media type based on format (defaults to png)
                        const format = output.format ?? "png";
                        const mediaType = format === "png"
                            ? "image/png"
                            : format === "jpeg"
                                ? "image/jpeg"
                                : format === "webp"
                                    ? "image/webp"
                                    : "image/png";
                        return {
                            type: "content",
                            value: [
                                {
                                    type: "image-data",
                                    data: output.screenshot,
                                    mediaType,
                                },
                            ],
                        };
                    }
                    else {
                        return {
                            type: "json",
                            value: toJsonValue(output),
                        };
                    }
                },
            }, mcpClient);
            logger.info(`[Playwright] ✓ Registered ${registeredTools.length} browser tools: ${registeredTools.join(", ")}`);
            // Register test subagent tools only if the mcp server is successfully started
            registry.register(startTestRunToolFactory, { clark, services });
            registry.register(endTestRunToolFactory, { clark, services });
            registry.register(updateTestCaseStatusToolFactory, { clark, services });
            // If a user skips a test case, prevent any further Playwright actions from being executed under that skipped testCaseId.
            addInstrumentedMiddleware("playwrightSkipGuard", (tool) => {
                if (tool.name !== "browser_playwright_action") {
                    return tool;
                }
                return {
                    ...tool,
                    execute: async (input, options) => {
                        const testCaseId = input && typeof input === "object" && "testCaseId" in input
                            ? input.testCaseId
                            : undefined;
                        if (typeof testCaseId === "string" && testCaseId.length > 0) {
                            const status = clark.context.testCaseStatuses?.get(testCaseId);
                            if (status === "skipped") {
                                throw new ToolExecutionError(`Test case "${testCaseId}" was skipped by the user. Do NOT run browser actions for it; move to the next test case.`, {
                                    toolName: tool.name,
                                    input,
                                    context: {},
                                    code: AiToolExecutionErrorCode.NOT_ALLOWED,
                                });
                            }
                        }
                        return tool.execute(input, options);
                    },
                };
            });
        }
    }
    catch (error) {
        // Use warn locally to avoid Datadog error alerts (expected when Playwright not configured)
        const isLocal = process.env.SUPERBLOCKS_SERVER_ENV === "local";
        const errorMessage = String(error);
        services.logger[isLocal ? "warn" : "error"](`[Playwright] Error initializing Playwright MCP server: ${errorMessage}`);
    }
    // Add output limiting middleware to all tools
    // This is added first so it becomes the outermost wrapper - truncation happens
    // AFTER inner middleware (like summaryInjector) have processed the original output
    addInstrumentedMiddleware("outputLimiter", outputLimiter(25_000));
    // Add recording middleware if recording is enabled
    // This captures tool inputs and outputs for later replay
    if (services.recordingManager?.isEnabled()) {
        addInstrumentedMiddleware("recording", createToolRecordingMiddleware(services.recordingManager));
    }
    // Add summary injection middleware to capture tool summaries for context compaction
    // The callback looks up the current context at execution time and stores summaries there
    // This is added after outputLimiter so it sees the original (non-truncated) output,
    // allowing getSummary to access structured properties like counts and statuses
    const storeSummary = (toolCallId, summary) => {
        try {
            const context = getActiveContext(clark, services);
            context?.storeSummary(toolCallId, summary);
        }
        catch {
            // Silently ignore - context may not be available in all scenarios
        }
    };
    addInstrumentedMiddleware("summaryInjector", summaryInjector(storeSummary));
    const secretRedactionLevel = services.features.secretRedactionLevel ?? "full";
    if (secretRedactionLevel === "full") {
        addInstrumentedMiddleware("secretScanning", secretScanningMiddleware());
    }
    // Add profiling middleware
    addInstrumentedMiddleware("profileTool", (tool) => {
        let profilingConfig;
        switch (tool.category) {
            case ToolCategory.FILE:
                profilingConfig = fileToolConfig;
                break;
            case ToolCategory.COMPONENT:
                profilingConfig = componentToolConfig;
                break;
        }
        return profileTool(tool.name, tool, profilingConfig);
    });
    // Add logging middleware with safe error handling
    // Secret sanitization is handled by the wrapped logger (regex-based, always-on)
    addInstrumentedMiddleware("toolLogging", toolLoggingMiddleware(clark.logger));
    // Get filtered tools from registry based on mode and plan approval
    const tools = await registry.getToolsRecord({
        ...clark.getAccessControl(services.features),
    });
    const enabledToolNames = Object.keys(tools);
    const totalRegistered = registry.listTools().length;
    const filtered = totalRegistered - enabledToolNames.length;
    clark.logger.info(`[buildTools] ${enabledToolNames.length} tools enabled (${filtered} filtered by access control)`);
    if (disabledTools && disabledTools.length > 0) {
        clark.logger.info(`[buildTools] Explicitly disabled: ${disabledTools.join(", ")}`);
    }
    clark.logger.info(`[buildTools] Enabled tools: ${enabledToolNames.join(", ")}`);
    return tools;
};
//# sourceMappingURL=tools.js.map