import { isEmpty } from "lodash-es";
import { z } from "zod";
import { AiEntityType, AiPermissionType, } from "@superblocksteam/library-shared/types";
import { safeJsonParse, safeJsonStringify, } from "../../../../util/log-sanitizer.js";
import { getErrorMeta, getLogger, getPrefixedLogger, } from "../../../../util/logger.js";
import { createToolFactory, PermissionLevel, ToolCategory, ToolExecutionError, } from "../../tools2/types.js";
import { codeBlock } from "../../tools2/utils.js";
import { MAX_QUERY_RESPONSE_TOKENS, truncateToTokens, } from "../integrations/internal.js";
import { containsMutations, extractIntegrationIdsFromSource, extractMutations, } from "./analysis.js";
import { runApiValidationPipeline } from "./api-validation-orchestrator.js";
import { safeSampleJson } from "./sample-json.js";
import { resolveSdkApiEntryPoint, resolveSdkApiEntry } from "./sdk-registry.js";
async function resolveSdkApiSourcePath(apiName, services, clark) {
    return resolveSdkApiEntryPoint(apiName, {
        readFile: (relativePath) => services.appShell.readFile(relativePath),
        pathExists: (relativePath) => services.appShell.pathExists(relativePath),
    }, clark?.context.sdkApiRegistryEntryPoints);
}
async function resolveSdkApiRegistryEntry(apiName, services) {
    return resolveSdkApiEntry(apiName, {
        readFile: (relativePath) => services.appShell.readFile(relativePath),
        pathExists: (relativePath) => services.appShell.pathExists(relativePath),
    });
}
/**
 * Execute an SDK API via the editor socket (aiExecuteSdkApi).
 * Runs a TypeScript typecheck first to catch errors before execution.
 */
async function executeSdkApi({ apiName, inputs, clark, services, logger, }) {
    const apiTsPath = await resolveSdkApiSourcePath(apiName, services, clark);
    if (!apiTsPath) {
        throw new Error(`No registry entryPoint found for API "${apiName}". Add it to server/apis/index.ts before testing.`);
    }
    // Run TypeScript check before execution to catch type errors
    logger.info(`Starting TypeScript check for API file: ${apiTsPath}`);
    try {
        const typecheckErrors = await services.appShell.runTypecheck(clark.context.abortController?.signal);
        logger.info(`TypeScript check completed. Has errors: ${!!typecheckErrors}, error file count: ${typecheckErrors ? Object.keys(typecheckErrors).length : 0}`);
        if (typecheckErrors) {
            const apiErrors = [];
            for (const [filePath, errors] of Object.entries(typecheckErrors)) {
                if (filePath.includes(apiTsPath) || filePath.endsWith(apiTsPath)) {
                    apiErrors.push(...errors);
                }
            }
            if (apiErrors.length > 0) {
                const errorPreview = apiErrors.slice(0, 5).join("\n");
                const moreErrors = apiErrors.length > 5
                    ? `\n... and ${apiErrors.length - 5} more error(s)`
                    : "";
                logger.error(`Blocking execution due to TypeScript errors`);
                throw new Error(`TypeScript errors in API "${apiName}" must be fixed before testing:\n\n${errorPreview}${moreErrors}\n\nFix these type errors and try again.`);
            }
        }
        logger.info(`TypeScript check passed for ${apiName}`);
    }
    catch (error) {
        if (error instanceof Error &&
            error.message.includes("TypeScript errors in API")) {
            throw error;
        }
        logger.warn(`TypeScript check failed, proceeding with execution: ${error instanceof Error ? error.message : String(error)}`);
    }
    logger.info(`Executing SDK API ${apiName} with inputs:`, safeJsonStringify(inputs));
    const editorClient = clark.context.peer;
    if (!editorClient) {
        throw new Error("No editor client available");
    }
    // Resolve entry point and export name from the registry so execution
    // works even when browser-side discovery hasn't run yet (e.g. Clark
    // just created the API files and no UI is rendered).
    const registryEntry = await resolveSdkApiRegistryEntry(apiName, services);
    // Extract integration IDs from the API source so the UI handler can
    // run the auth preflight even when the Redux store hasn't registered
    // this API yet (APPS-3288).
    let integrationIds;
    try {
        const apiSource = await services.appShell.readFile(apiTsPath);
        const ids = extractIntegrationIdsFromSource(apiSource);
        if (ids.length > 0) {
            integrationIds = ids;
        }
    }
    catch {
        logger.warn(`Failed to extract integration IDs from ${apiTsPath}, auth preflight will rely on Redux store`);
    }
    try {
        const result = await editorClient.call.aiExecuteSdkApi({
            apiName,
            input: inputs ?? {},
            entryPoint: registryEntry?.entryPoint ?? apiTsPath,
            exportName: registryEntry?.exportName,
            integrationIds,
        });
        logger.info(`aiExecuteSdkApi returned for ${apiName}: success=${result.success}, hasOutput=${!!result.output}, hasError=${!!result.error}`);
        if (result.success === undefined) {
            logger.error(`aiExecuteSdkApi returned malformed result (no success field) for ${apiName}`);
            return {
                message: `API ${apiName} execution returned unexpected result format. This may indicate the API file failed to load or has syntax errors. Check the browser console for details.`,
                result: {
                    success: false,
                    systemError: "Malformed response from API execution",
                    rawResult: result,
                },
                apiName,
            };
        }
        if (result.success === false) {
            logger.warn(`SDK API execution returned error for ${apiName}: ${result.error?.message ?? "Unknown error"}`);
            clark.context.apiTestingState ??= {
                untested: new Set(),
                failed: new Set(),
                passed: new Set(),
                denied: new Set(),
            };
            clark.context.apiTestingState.failed.add(apiName);
            clark.context.apiTestingState.passed.delete(apiName);
            const errorSummary = result.error?.message ?? "";
            return {
                message: `API ${apiName} FAILED with unhandled errors. Do NOT show a success message to the user - investigate and fix the error.

Error summary:
${errorSummary || "No specific error details available."}`,
                result: {
                    success: false,
                    error: result.error,
                },
                apiName,
            };
        }
        // Limit output size for SDK APIs (same token cap as YAML step outputs).
        // The UI (getToolOutput) and model (toModelOutput) apply further limits.
        const truncated = truncateToTokens(result.output, MAX_QUERY_RESPONSE_TOKENS);
        const outputs = safeJsonParse(truncated.result) ?? truncated.result;
        logger.info(`API ${apiName} executed successfully, output truncated=${truncated.truncated}`);
        clark.context.apiTestingState ??= {
            untested: new Set(),
            failed: new Set(),
            passed: new Set(),
            denied: new Set(),
        };
        clark.context.apiTestingState.passed.add(apiName);
        clark.context.apiTestingState.failed.delete(apiName);
        return {
            message: `API ${apiName} executed without throwing exceptions. This does NOT guarantee the outputs are correct - verify the data before showing success to the user.
If the API is meant to be run off a useEffect, call build_reloadFile to reload the appropriate file to propagate the changes to the UI.`,
            result: {
                success: true,
                outputs,
                ...(truncated.truncated && { isPartial: true }),
            },
            apiName,
        };
    }
    catch (error) {
        const errorMeta = getErrorMeta(error);
        logger.warn(`API execution failed for ${apiName}:`, errorMeta);
        const errorMessage = error instanceof Error ? error.message : String(error);
        return {
            message: `API execution failed for ${apiName}. The error details are included in the result below - analyze them to understand and fix the issue.

Error: ${errorMessage}`,
            result: {
                success: false,
                systemError: errorMessage,
            },
            apiName,
        };
    }
}
/**
 * Execute a YAML API via the validation pipeline.
 */
async function executeYamlApi({ apiName, inputs, omitStepOutputs, timeout, clark, services, logger, }) {
    try {
        const result = (await runApiValidationPipeline({
            clark,
            services,
            identifier: apiName,
            inputs,
            options: {
                omitStepOutputs,
                timeout,
            },
        })).executionResult;
        if (!result) {
            logger.error("BUG! No execution result returned by api validation pipeline");
            throw new Error("No API execution result received, please try again");
        }
        if (!result.success) {
            logger.warn(`api execution returned errors for ${apiName}:`, result);
            clark.context.apiTestingState ??= {
                untested: new Set(),
                failed: new Set(),
                passed: new Set(),
                denied: new Set(),
            };
            clark.context.apiTestingState.failed.add(apiName);
            clark.context.apiTestingState.passed.delete(apiName);
            const errors = result.errors ?? [];
            const errorSummary = errors
                .map((e) => {
                const parts = [e.message];
                if (e.suggestion) {
                    parts.push(`Suggestion: ${e.suggestion}`);
                }
                return parts.join("\n");
            })
                .join("\n\n");
            return {
                message: `API ${apiName} FAILED with unhandled errors. Do NOT show a success message to the user - investigate and fix the error.

Error summary:
${errorSummary || "No specific error details available."}`,
                result,
                apiName,
            };
        }
        const outputSample = safeSampleJson(result.outputs);
        let stepOutputs = result.stepOutputs;
        if (stepOutputs) {
            stepOutputs = { ...result.stepOutputs };
            for (const [key, value] of Object.entries(stepOutputs)) {
                const stepOutputSample = safeSampleJson(value.output);
                stepOutputs[key] = {
                    ...value,
                    output: stepOutputSample,
                };
            }
        }
        clark.context.apiTestingState ??= {
            untested: new Set(),
            failed: new Set(),
            passed: new Set(),
            denied: new Set(),
        };
        clark.context.apiTestingState.passed.add(apiName);
        clark.context.apiTestingState.failed.delete(apiName);
        return {
            message: `API ${apiName} executed without throwing exceptions. This does NOT guarantee the outputs are correct - verify the data before showing success to the user.
If the API is meant to be run off a useEffect, call build_reloadFile to reload the appropriate file to propagate the changes to the UI.`,
            result: {
                ...result,
                outputs: outputSample,
                stepOutputs,
            },
            apiName,
        };
    }
    catch (error) {
        logger.warn(`API execution failed for ${apiName}:`, getErrorMeta(error));
        const errorMessage = error instanceof Error ? error.message : String(error);
        return {
            message: `API execution failed for ${apiName}. The error details are included in the result below - analyze them to understand and fix the issue.

Error: ${errorMessage}`,
            result: {
                success: false,
                systemError: errorMessage,
            },
            apiName,
        };
    }
}
/**
 * Read the API source file using the appropriate path for the current mode.
 * When a pre-resolved `RegistryEntry` is provided (SDK mode), its `entryPoint`
 * is used directly — avoiding a redundant registry read.
 * Non-SDK mode reads from the YAML-based `apis/<apiName>.ts` path.
 */
async function readApiSource(apiName, services, registryEntry) {
    if (services.features.sdkApiEnabled) {
        const sdkPath = registryEntry?.entryPoint;
        if (!sdkPath) {
            throw new Error(`No SDK registry entry point found for API "${apiName}"`);
        }
        return await services.appShell.readFile(sdkPath);
    }
    return await services.appShell.readFile(`apis/${apiName}.ts`);
}
/**
 * Validates testApi inputs before permission check.
 * Throws if validation fails (e.g., mutations detected but no description provided).
 */
export const validateTestApiInput = (input = {}, mutations = []) => {
    const hasMutations = mutations?.length > 0;
    const description = input?.description;
    const apiName = input?.apiName;
    if (hasMutations && (!description || description.trim().length === 0)) {
        const mutationsPreview = mutations
            .slice(0, 3)
            .map((m, idx) => {
            const detail = m.details.length > 60 ? `${m.details.slice(0, 60)}...` : m.details;
            return `  ${idx + 1}. ${m.type} "${m.name}": ${detail}`;
        })
            .join("\n");
        throw new Error(`
The API "${apiName}" contains ${mutations.length} mutation(s) that will modify data, but no description was provided.

Mutations detected:
${mutationsPreview}

You MUST provide a 'description' parameter that explains in plain English what data will be modified and why.

Example call:
testApi({
apiName: "${apiName}",
inputs: {...},
description: "Creates a users table with columns for id, name, email, and created_at timestamp"
})`.trim());
    }
};
export const checkTestApiPermissions = async (services, input, _clark) => {
    // we are building our initial set of tools, we don't have any permissions yet
    if (!input || isEmpty(input)) {
        return PermissionLevel.PROMPT;
    }
    if (!input.apiName) {
        throw new ToolExecutionError("API name is required", {
            toolName: "testApi",
            input: input ?? {},
            context: {},
            severity: "warn",
        });
    }
    let mutations = [];
    try {
        const registryEntry = services.features.sdkApiEnabled
            ? await resolveSdkApiRegistryEntry(input.apiName, services)
            : undefined;
        const apiSourceContent = await readApiSource(input.apiName, services, registryEntry);
        mutations = extractMutations(apiSourceContent, registryEntry ? (registryEntry.exportName ?? "default") : undefined);
    }
    catch (error) {
        getLogger().info(`[testApi] Failed to resolve or read API source, fall back to PROMPT: ${input.apiName}`, error instanceof Error ? error.message : String(error));
        validateTestApiInput(input, mutations);
        return PermissionLevel.PROMPT;
    }
    if (mutations.length > 0) {
        validateTestApiInput(input, mutations);
        return PermissionLevel.PROMPT;
    }
    return PermissionLevel.ALLOW;
};
export const testApiToolFactory = createToolFactory("testApi", ({ clark, services, }) => {
    return {
        category: ToolCategory.DEBUG,
        // For SDK APIs, we skip the output limit but will truncate the output we send to the LLM in toModelOutput
        skipOutputLimit: (_output) => !!services.features.sdkApiEnabled,
        description: `
Execute an API to test its behavior. This triggers the API through the normal UI execution path,
so the execution will be visible in the application UI with full trace details.

Use this tool to verify that an API works correctly after building it.

The tool will generate smart default inputs for any bindings the API uses:
- Component values (e.g., Input1.value) will use mock data based on component type
- State variables will use empty objects/arrays
- Global context values are automatically available

CRITICAL: If the API contains mutations (SQL INSERT/UPDATE/DELETE, HTTP POST/PUT/PATCH/DELETE, DynamoDB mutations, Salesforce CRUD operations, etc.),
you MUST provide the 'description' parameter with a clear, plain-English explanation of:
  - What data will be created/modified/deleted
  - Which resources are affected
  - The business purpose of the operation

The description will be shown to the user in an approval prompt, so it must be clear and easy to understand for technical and non-technical users. You can return Markdown.
`,
        inputSchema: z.object({
            apiName: z.string().describe("The name of the API to test"),
            inputs: z
                .record(z.string(), z.any())
                .describe("Test input values for the API bindings. A key must be provided for each input the API expects."),
            description: z
                .string()
                .optional()
                .describe("Plain-English explanation of what this API does, especially any side-effects."),
            omitStepOutputs: z
                .boolean()
                .optional()
                .default(false)
                .describe("If true, step outputs will be omitted from the execution result."),
            timeout: z
                .number()
                .optional()
                .describe("Timeout in milliseconds. If exceeded, the API execution is cancelled. Defaults to 500 seconds."),
        }),
        getActionName: () => ({
            future: "Test API",
            present: "Testing API",
            past: "Tested API",
        }),
        formatInput: (params) => {
            const { apiName, inputs, description } = params;
            return {
                targets: [apiName],
                details: description ??
                    (inputs ? codeBlock(safeJsonStringify(inputs, 2)) : undefined),
                summary: `Ok if I test ${apiName}?`,
            };
        },
        onCall: (_params) => {
            // No-op: We intentionally do NOT modify untested/denied here.
            // The API stays in `untested` until we get a definitive response:
            // - If user approves (or no permission needed): execute() removes from untested
            // - If user denies: handleToolPermission removes from untested and adds to denied
            // This ensures build_debug correctly reports untested APIs while waiting for user response.
        },
        getRequiredPermissions: async (params) => {
            const { apiName } = params;
            if (!apiName) {
                return [];
            }
            try {
                const registryEntry = services.features.sdkApiEnabled
                    ? await resolveSdkApiRegistryEntry(apiName, services)
                    : undefined;
                const apiSourceContent = await readApiSource(apiName, services, registryEntry);
                const hasWrites = containsMutations(apiSourceContent, registryEntry ? (registryEntry.exportName ?? "default") : undefined);
                if (!hasWrites) {
                    return [];
                }
                const integrationIds = extractIntegrationIdsFromSource(apiSourceContent);
                return integrationIds.map((id) => {
                    return {
                        entityType: AiEntityType.INTEGRATION,
                        entityId: id,
                        permissionType: AiPermissionType.WRITE,
                    };
                });
            }
            catch {
                // Source read failed — return empty so the registry's
                // shouldPromptForEntities (which returns true for []) prompts the user.
                return [];
            }
        },
        execute: async ({ apiName, inputs, omitStepOutputs, timeout }) => {
            const logger = getPrefixedLogger(`[testApi-${apiName}]`);
            logger.info(`Executing API ${apiName} with inputs:`, safeJsonStringify(inputs));
            clark.context.apiTestingState?.untested.delete(apiName);
            // User approved execution — remove from denied so future file
            // edits can re-mark the API as untested if needed.
            clark.context.apiTestingState?.denied.delete(apiName);
            if (services.features.sdkApiEnabled) {
                return executeSdkApi({ apiName, inputs, clark, services, logger });
            }
            return executeYamlApi({
                apiName,
                inputs,
                omitStepOutputs,
                timeout,
                clark,
                services,
                logger,
            });
        },
        getSummary: (_input, output) => {
            // Explicitly check for true - undefined/null means failure
            const success = output.result?.success === true;
            const status = success ? "Success" : "Failed";
            return {
                output: status,
            };
        },
    };
});
//# sourceMappingURL=test-api.js.map