import { AiEntityType, AiPermissionType, } from "@superblocksteam/library-shared/types";
import { ActionResponseType, DatabricksPlugin, EditorLanguage, ExtendedIntegrationPluginId, GraphQLIntegrationPluginMergedFieldNames, HttpMethod, RestApiFields, SalesforcePlugin, } from "@superblocksteam/shared";
import z from "zod";
import { getLogger } from "../../../../util/logger.js";
import { createToolFactory, ToolCategory, PermissionLevel, ToolExecutionError, } from "../../tools2/types.js";
import { codeBlock } from "../../tools2/utils.js";
import { executeRequest, MAX_QUERY_RESPONSE_TOKENS } from "./internal.js";
// Maximum number of rows that can be returned from a query
const MAX_QUERY_LIMIT = 10;
const DESCRIPTION_BASE_PROMPT = `
A concise plain-English description of WHAT data operation will be performed
Focus on the domain names, the specific number of records if limited, and the table/resource name.
CRITICAL: Always specify which integration is being used with the format @IntegrationName[id=INTEGRATION_ID] where INTEGRATION_ID is the integrationId parameter and IntegrationName is the tagName of the integration.
DO NOT explain why or include context.
`;
export const executeRequestToIntegrationToolFactory = createToolFactory("executeRequestToIntegration", ({ clark, services, }) => {
    return {
        category: ToolCategory.DATA,
        defaultPermission: PermissionLevel.ALLOW,
        description: `
      Use this tool to make requests to integrations configured in the user's application.

      You can use this tool to query data from databases, APIs, or other external services that the user has connected to their application.

      IMPORTANT: Only read-only operations are supported. Do NOT use this tool for any operations that modify data.

      IMPORTANT: SQL queries must include a row limit clause with a maximum of ${MAX_QUERY_LIMIT} rows.
      Supported syntax: LIMIT N (MySQL/PostgreSQL), SELECT TOP N (SQL Server), or FETCH NEXT N ROWS ONLY (ANSI SQL).
      Note: Metadata commands (SHOW, DESCRIBE, EXPLAIN) don't require a row limit.

      IMPORTANT: Limit requests to a maximum of ${MAX_QUERY_LIMIT} entities.

      NOTE: For running Python/JavaScript code, use the 'runCode' tool instead.

      Plugin Types:
      - "sql": For SQL database integrations, including Databricks. Provide a SQL query string.
      - "graphql": For GraphQL integrations (${ExtendedIntegrationPluginId.GRAPHQL}). Provide query, variables, and headers.
      - "rest": For REST API integrations (${ExtendedIntegrationPluginId.REST_API}). HTTP method is fixed to GET. Provide path, headers, and query parameters.
      - "salesforce": For Salesforce integrations (${SalesforcePlugin.id}). Provide SOQL query string.
      - "other": For any other integration types that doesn't fit one of the above. Provide a generic query string.

      Authentication is handled automatically based on the integration configuration.
      Response data is returned as a JSON object.
`,
        inputSchema: z.object({
            integrationId: z.string().min(1).describe("The ID of the integration"),
            description: z.object({
                request: z.string().min(1).describe(`
            ${DESCRIPTION_BASE_PROMPT}
            Phrased as a request for permission.
            Examples: "Ok if I read 20 orders from @DemoOrders[id=abc123]?", "Ok if I read 5 rows from the users table of @Postgres[id=xyz789]?", "Ok if I read 10 issues from @Linear[id=def456]?".
          `),
                present: z.string().min(1).describe(`
            ${DESCRIPTION_BASE_PROMPT}.
            Phrased as a present tense statement.
            Examples: "Reading 20 orders from @DemoOrders[id=abc123]", "Reading 5 rows from the users table of @Postgres[id=xyz789]", "Reading 10 issues from @Linear[id=def456]".
          `),
                past: z.string().min(1).describe(`
            ${DESCRIPTION_BASE_PROMPT}.
            Phrased as a past tense statement.
            Examples: "Read 20 orders from @DemoOrders[id=abc123]", "Read 5 rows from the users table of @Postgres[id=xyz789]", "Read 10 issues from @Linear[id=def456]".
          `),
            }),
            params: z.discriminatedUnion("type", [
                z.object({
                    type: z.literal("sql"),
                    query: z
                        .string()
                        .min(1)
                        .describe("The SQL query string to execute"),
                }),
                z.object({
                    type: z.literal("graphql"),
                    graphQLQuery: z.object({
                        query: z
                            .string()
                            .min(1)
                            .describe("The GraphQL query string to execute"),
                        variables: z
                            .record(z.unknown())
                            .optional()
                            .describe("Optional variables for the GraphQL query"),
                        headers: z
                            .record(z.string())
                            .optional()
                            .describe("Optional HTTP headers to include in the request"),
                    }),
                }),
                z.object({
                    type: z.literal("rest"),
                    httpRequest: z.object({
                        path: z
                            .string()
                            .min(1)
                            .describe("The URL path to the API endpoint, beginning with a '/'"),
                        headers: z
                            .record(z.string())
                            .optional()
                            .describe("Optional HTTP headers to include in the request"),
                        queryParams: z
                            .record(z.string())
                            .optional()
                            .describe("Optional query parameters to include in the request"),
                    }),
                }),
                z.object({
                    type: z.literal("salesforce"),
                    query: z
                        .string()
                        .min(1)
                        .describe("The SOQL query string to execute"),
                }),
                z.object({
                    type: z.literal("other"),
                    body: z
                        .string()
                        .min(1)
                        .describe("The body of the request to execute against the integration"),
                }),
            ]),
        }),
        toModelOutput: (result) => {
            if (result.isPartial) {
                return `Output too long. Max output size is ${MAX_QUERY_RESPONSE_TOKENS * 3} characters (~${MAX_QUERY_RESPONSE_TOKENS} tokens). Truncated result:
          <result>
          ${result.data}
          </result>`;
            }
            return result;
        },
        execute: async (args) => {
            const { params, integrationId } = args;
            switch (params.type) {
                case "salesforce":
                case "sql": {
                    const { query } = params;
                    // Validate query is read-only
                    const normalizedQuery = query.trim().toUpperCase();
                    const dmlKeywords = [
                        "INSERT",
                        "UPDATE",
                        "DELETE",
                        "DROP",
                        "TRUNCATE",
                        "ALTER",
                        "CREATE",
                        "REPLACE",
                        "MERGE",
                        "RENAME",
                        "GRANT",
                        "REVOKE",
                        "USE",
                        "SET",
                        "UNSET",
                        "VACUUM",
                        "COPY",
                    ];
                    const hasDML = dmlKeywords.some((keyword) => 
                    // Match keyword at start or after whitespace/semicolon
                    new RegExp(`(^|\\s|;)${keyword}\\s`, "i").test(normalizedQuery));
                    if (hasDML) {
                        throw new ToolExecutionError("Only read-only SQL queries are allowed. DML operations (INSERT, UPDATE, DELETE, etc.) are blocked.", {
                            toolName: "executeRequestToIntegration",
                            input: args,
                            context: {},
                            severity: "info",
                        });
                    }
                    // Validate row limit clause (supports multiple SQL dialects)
                    // Skip validation for metadata commands that don't support LIMIT
                    const metadataCommands = [
                        "SHOW",
                        "DESCRIBE",
                        "DESC",
                        "EXPLAIN",
                        "USE",
                        "INFORMATION_SCHEMA",
                    ];
                    const isMetadataCommand = metadataCommands.some((cmd) => new RegExp(`^${cmd}\\b`, "i").test(normalizedQuery));
                    // Check if this is an aggregate-only query (returns single row)
                    // Aggregate queries without GROUP BY always return exactly one row
                    const hasGroupBy = /\bGROUP\s+BY\b/i.test(normalizedQuery);
                    const aggregateFunctions = ["COUNT", "SUM", "AVG", "MIN", "MAX"];
                    const hasAggregateFunction = aggregateFunctions.some((fn) => new RegExp(`\\b${fn}\\s*\\(`, "i").test(normalizedQuery));
                    // Check if SELECT clause only contains aggregate functions (no regular columns)
                    // Extract the SELECT clause (between SELECT and FROM)
                    const selectClauseMatch = normalizedQuery.match(/SELECT\s+(.*?)\s+FROM\b/is);
                    const selectClause = selectClauseMatch?.[1] || "";
                    // Remove aggregate functions and check if anything meaningful remains
                    let selectWithoutAggregates = selectClause;
                    for (const fn of aggregateFunctions) {
                        // Remove aggregate function calls like COUNT(*), SUM(column), etc.
                        selectWithoutAggregates = selectWithoutAggregates.replace(new RegExp(`\\b${fn}\\s*\\([^)]*\\)`, "gi"), "");
                    }
                    // Remove whitespace, commas, and AS aliases to check if only aggregates remain
                    const remainingColumns = selectWithoutAggregates
                        .replace(/\bAS\s+\w+/gi, "")
                        .replace(/[,\s]+/g, "");
                    const isAggregateOnlyQuery = hasAggregateFunction && !hasGroupBy && remainingColumns === "";
                    if (!isMetadataCommand && !isAggregateOnlyQuery) {
                        // Check for various SQL dialect row limiting syntaxes:
                        // - MySQL/PostgreSQL/SQLite: LIMIT N
                        // - SQL Server: TOP N (after SELECT)
                        // - ANSI SQL (SQL Server, Oracle, DB2): OFFSET ... FETCH NEXT N ROWS ONLY
                        const limitMatch = normalizedQuery.match(/LIMIT\s+(\d+)/i);
                        const topMatch = normalizedQuery.match(/SELECT\s+TOP\s+(\d+)\b/i);
                        const fetchMatch = normalizedQuery.match(/FETCH\s+(?:FIRST|NEXT)\s+(\d+)\s+ROWS?\s+ONLY/i);
                        const matchedLimit = limitMatch?.[1] || topMatch?.[1] || fetchMatch?.[1];
                        if (!matchedLimit) {
                            throw new ToolExecutionError("SQL queries must include a row limit clause to prevent excessive result sizes. " +
                                "Use LIMIT N (MySQL/PostgreSQL), SELECT TOP N (SQL Server), or FETCH NEXT N ROWS ONLY (ANSI SQL). " +
                                `Maximum allowed limit is ${MAX_QUERY_LIMIT} rows.`, {
                                toolName: "executeRequestToIntegration",
                                input: args,
                                context: {},
                                severity: "info",
                            });
                        }
                        const limit = parseInt(matchedLimit, 10);
                        if (limit > MAX_QUERY_LIMIT) {
                            throw new ToolExecutionError(`Query row limit of ${limit} exceeds maximum allowed limit of ${MAX_QUERY_LIMIT} rows.`, {
                                toolName: "executeRequestToIntegration",
                                input: args,
                                context: {},
                                severity: "info",
                            });
                        }
                    }
                    let request;
                    if (params.type === "salesforce") {
                        request = {
                            soql: {
                                action: "SOQL_ACTION_SOQL",
                                sqlBody: query,
                            },
                        };
                    }
                    else {
                        const queryFieldPath = getQueryFieldPath(integrationId, services.integrationStore);
                        request = buildSqlRequest(query, queryFieldPath);
                    }
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "graphql": {
                    const { graphQLQuery } = params;
                    const request = buildGraphQLRequest(graphQLQuery);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "rest": {
                    const { httpRequest } = params;
                    const request = buildRestApiRequest(httpRequest);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "other": {
                    const { body } = params;
                    const request = { body };
                    return await executeRequest(clark.context, integrationId, request);
                }
                default: {
                    const _exhaustive = params;
                    throw new Error(`Unsupported plugin type: ${_exhaustive}`);
                }
            }
        },
        getRequiredPermissions: (args) => {
            return [
                {
                    entityType: AiEntityType.INTEGRATION,
                    entityId: args.integrationId,
                    permissionType: AiPermissionType.READ,
                },
            ];
        },
        getActionName: () => ({
            future: "Query",
            present: "Querying",
            past: "Queried",
        }),
        formatInput: (args) => {
            const { params, integrationId, description } = args;
            const integration = services.integrationStore.getEntityTag(integrationId) ??
                integrationId;
            let cleanDescription = description.request.trim();
            if (cleanDescription.at(-1) !== "?") {
                cleanDescription = `${cleanDescription}?`;
            }
            switch (params.type) {
                case "salesforce":
                case "sql": {
                    return {
                        targets: [integration],
                        details: codeBlock(params.query, "sql"),
                        summary: cleanDescription,
                    };
                }
                case "graphql": {
                    return {
                        targets: [integration],
                        details: codeBlock(params.graphQLQuery.query, "graphql"),
                        summary: cleanDescription,
                    };
                }
                case "rest": {
                    const { path, queryParams } = params.httpRequest;
                    const queryString = queryParams
                        ? `?${Object.entries(queryParams)
                            .map((key, val) => [key, val].join("="))
                            .join("&")}`
                        : "";
                    return {
                        targets: [integration],
                        details: codeBlock(`GET ${path}${queryString}`),
                        summary: cleanDescription,
                    };
                }
                case "other": {
                    return {
                        targets: [integration],
                        details: codeBlock(params.body),
                        summary: cleanDescription,
                    };
                }
                default: {
                    const _exhaustive = params;
                    return {
                        targets: [integration],
                        summary: cleanDescription,
                    };
                }
            }
        },
        readOnly: true,
        // This tool handles its own response truncation (via truncateToTokens in internal.ts),
        // so skip the generic outputLimiter middleware to avoid double-truncation and preserve
        // the structured { data, isPartial } output format.
        skipOutputLimit: () => true,
        getSummary: (input, output) => {
            const integration = services.integrationStore.getEntityTag(input.integrationId) ??
                input.integrationId;
            const type = input.params.type;
            // Try to extract row count from output
            let rowInfo = "Query executed";
            if (output && typeof output === "object") {
                const data = output;
                if (Array.isArray(data.output)) {
                    rowInfo = `${data.output.length} row${data.output.length === 1 ? "" : "s"}`;
                }
                else if (data.output &&
                    typeof data.output === "object" &&
                    "records" in data.output &&
                    Array.isArray(data.output.records)) {
                    const recordCount = data.output.records.length;
                    rowInfo = `${recordCount} record${recordCount === 1 ? "" : "s"}`;
                }
            }
            return {
                input: `${integration} (${type})`,
                output: rowInfo,
            };
        },
    };
});
function getQueryFieldPath(integrationId, store) {
    const pluginId = store.getHeader(integrationId)?.plugin.id;
    if (!pluginId) {
        throw new Error(`Integration with ID ${integrationId} not found`);
    }
    const plugin = store.getPlugin(pluginId);
    if (!plugin) {
        throw new Error(`Plugin with ID ${pluginId} not found`);
    }
    let queryFieldPath;
    switch (pluginId) {
        case DatabricksPlugin.id:
            queryFieldPath = "runSql.sqlBody";
            break;
        default:
            queryFieldPath =
                extractSqlQueryFieldPathFromTemplate(plugin.actionTemplate) ?? "body";
    }
    return queryFieldPath;
}
function extractSqlQueryFieldPathFromTemplate(actionTemplate) {
    const queryField = actionTemplate.sections
        .flatMap((section) => section.items)
        .flatMap((item) => ("rowItems" in item ? item.rowItems : item))
        .filter((item) => "language" in item)
        .filter((item) => item.language === EditorLanguage.SQL)[0];
    if (queryField) {
        return queryField.name;
    }
    return undefined;
}
function recordToProto(headers) {
    return Object.entries(headers).map(([key, value]) => ({
        key,
        value,
    }));
}
function buildSqlRequest(query, queryFieldPath) {
    if (!queryFieldPath) {
        throw new Error("Invalid query field path");
    }
    const request = {};
    let current = request;
    // sometimes the path to the field in the config where the query should go is nested.
    // in the action template this is represented with dot notation `level1.level2.queryField`.
    // the loop here is taking that field path and building
    // level1: {
    //   level2: {
    //     queryField: <query>
    //   }
    // }
    const queryFieldPathParts = queryFieldPath.split(".");
    for (let i = 0; i < queryFieldPathParts.length - 1; i++) {
        const field = queryFieldPathParts.at(i);
        if (!field || !field.trim()) {
            throw new Error("Invalid query field path: empty segment detected");
        }
        current[field] = {};
        current = current[field];
    }
    const lastField = queryFieldPathParts.at(-1);
    if (!lastField || !lastField.trim()) {
        throw new Error("Invalid query field path: empty segment detected");
    }
    current[lastField] = query;
    return request;
}
function buildGraphQLRequest(graphQLQuery) {
    const headers = {
        ...graphQLQuery.headers,
        "Content-Type": "application/json",
    };
    const headersProto = recordToProto(headers);
    getLogger().debug("GraphQL request headers:", headersProto);
    const request = {
        body: graphQLQuery.query,
        [GraphQLIntegrationPluginMergedFieldNames.HEADERS]: headersProto,
    };
    if (graphQLQuery.variables) {
        request["custom.variables.value"] = JSON.stringify(graphQLQuery.variables);
    }
    return request;
}
function buildRestApiRequest(httpRequest) {
    const request = {
        [RestApiFields.HTTP_METHOD]: HttpMethod.GET,
        [RestApiFields.URL_PATH]: httpRequest.path,
        [RestApiFields.RESPONSE_TYPE]: ActionResponseType.JSON,
    };
    if (httpRequest.headers) {
        request[RestApiFields.HEADERS] = recordToProto(httpRequest.headers);
    }
    if (httpRequest.queryParams) {
        request[RestApiFields.PARAMS] = recordToProto(httpRequest.queryParams);
    }
    return request;
}
//# sourceMappingURL=execute-request.js.map