import z from "zod";
import { AiEntityType, AiPermissionType, } from "@superblocksteam/library-shared/types";
import { ActionResponseType, CosmosDbPlugin, DatabricksPlugin, DynamoDBPlugin, EditorLanguage, ExtendedIntegrationPluginId, GCSPlugin, GraphQLIntegrationPluginMergedFieldNames, MongoDBPlugin, RestApiBodyDataType, RestApiFields, S3ActionType, S3Plugin, SalesforcePlugin, } from "@superblocksteam/shared";
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 S3_READ_ACTIONS = [
    S3ActionType.GENERATE_PRESIGNED_URL,
    S3ActionType.GET_OBJECT,
    S3ActionType.LIST_BUCKET_OBJECTS,
    S3ActionType.LIST_BUCKETS,
];
const DYNAMODB_READ_ACTIONS = [
    "describeTable",
    "getItem",
    "listTables",
    "query",
    "scan",
];
const GCS_READ_ACTIONS = [
    "GENERATE_PRESIGNED_URL",
    "GET_OBJECT",
    "LIST_BUCKETS",
    "LIST_OBJECTS",
];
const COSMOSDB_READ_ACTION_CASES = ["read", "singleton"];
const COSMOSDB_READ_CATEGORY_CASES = ["pointOperation", "sql"];
const MONGODB_READ_ACTIONS = [
    "aggregate",
    "count",
    "distinct",
    "find",
    "findOne",
    "listCollections",
];
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.
`;
// Hoisted out of the factory so `classifyPermissionType` and tests can derive
// their argument types directly from the schema via `z.infer`, instead of
// manually duplicating the discriminated union (which would silently drift
// when plugin types or fields are added).
const inputSchema = z.object({
    integrationId: z.string().min(1).describe("The ID of the integration"),
    operation: z
        .enum(["read", "write"])
        .describe("Declare whether this request only reads data (read) or creates/updates/deletes data (write). " +
        "This is only consulted for ambiguous cases — REST POST requests and 'other' plugin types — " +
        "where intent cannot be determined structurally. For SQL/Salesforce/GraphQL/S3/DynamoDB/GCS/" +
        "CosmosDB/MongoDB, intent is derived from the query or schema and this field is effectively ignored. " +
        "For REST PUT/PATCH/DELETE, it is always treated as a write regardless of this field. " +
        "Still set it accurately: choose 'read' for queries, searches, lists, and fetches; 'write' for " +
        "anything that modifies server state. Write operations require user approval."),
    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({
                httpMethod: z
                    .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
                    .describe("The HTTP method for the request"),
                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"),
                body: z
                    .string()
                    .optional()
                    .refine((val) => {
                    if (val === undefined)
                        return true;
                    try {
                        JSON.parse(val);
                        return true;
                    }
                    catch {
                        return false;
                    }
                }, { message: "body must be a JSON-encoded string" })
                    .describe("Optional JSON-encoded request body (for POST, PUT, PATCH methods). " +
                    "Must be a string containing valid JSON — e.g. '{\"foo\": 1}'. " +
                    "Sent with Content-Type: application/json."),
            }),
        }),
        z.object({
            type: z.literal("s3"),
            action: z.enum(S3_READ_ACTIONS).describe("The S3 action to perform"),
            resource: z
                .string()
                .optional()
                .describe("The S3 bucket name (required for all actions except LIST_BUCKETS)"),
            path: z
                .string()
                .optional()
                .describe("The object key/path (required for GET_OBJECT and GENERATE_PRESIGNED_URL)"),
            listFilesConfig: z
                .object({
                prefix: z
                    .string()
                    .optional()
                    .describe("Prefix filter for listing objects"),
                delimiter: z
                    .string()
                    .optional()
                    .describe("Delimiter for hierarchical listing (e.g. '/' for folder-like grouping)"),
            })
                .optional()
                .describe("Configuration for LIST_BUCKET_OBJECTS action"),
        }),
        z.object({
            type: z.literal("cosmosdb"),
            actionCategory: z
                .enum(COSMOSDB_READ_CATEGORY_CASES)
                .describe("The CosmosDB action category: 'sql' for SQL queries, 'pointOperation' for single object actions"),
            actionCase: z
                .enum(COSMOSDB_READ_ACTION_CASES)
                .describe("The specific action: 'singleton' for SQL queries, 'read' for point reads"),
            containerId: z.string().optional().describe("The CosmosDB container ID"),
            query: z
                .string()
                .optional()
                .describe("The SQL query string (required for sql/singleton action)"),
            partitionKey: z
                .string()
                .optional()
                .describe("The partition key for point operations"),
            id: z
                .string()
                .optional()
                .describe("The item ID (required for pointOperation/read action)"),
        }),
        z.object({
            type: z.literal("dynamodb"),
            action: z
                .enum(DYNAMODB_READ_ACTIONS)
                .describe("The DynamoDB action to perform (read-only)"),
            body: z
                .string()
                .optional()
                .describe("JSON string of DynamoDB request parameters (e.g. TableName, Key, FilterExpression)"),
        }),
        z.object({
            type: z.literal("gcs"),
            action: z
                .enum(GCS_READ_ACTIONS)
                .describe("The GCS action to perform (read-only)"),
            resource: z
                .string()
                .optional()
                .describe("The GCS bucket name (required for all actions except LIST_BUCKETS)"),
            path: z
                .string()
                .optional()
                .describe("The object key/path (required for GET_OBJECT and GENERATE_PRESIGNED_URL)"),
            prefix: z
                .string()
                .optional()
                .describe("Prefix filter for listing objects or buckets"),
        }),
        z.object({
            type: z.literal("mongodb"),
            action: z
                .enum(MONGODB_READ_ACTIONS)
                .describe("The MongoDB operation to perform (read-only)"),
            collection: z
                .string()
                .optional()
                .describe("The collection name (required for all actions except listCollections)"),
            query: z
                .string()
                .optional()
                .describe("JSON query filter for count, distinct, find, findOne operations"),
            pipeline: z
                .string()
                .optional()
                .describe("JSON aggregation pipeline array for aggregate operation"),
            projection: z
                .string()
                .optional()
                .describe("JSON projection document for find/findOne operations"),
            sortby: z
                .string()
                .optional()
                .describe("JSON sort specification for find operation"),
            limit: z
                .string()
                .optional()
                .describe("Maximum number of documents to return for find operation"),
            field: z
                .string()
                .optional()
                .describe("Field name for distinct operation"),
        }),
        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"),
        }),
    ]),
});
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.

      Both read-only and write operations are supported. Write operations (creating, updating, or deleting data) will require user approval before execution.

      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}). Provide HTTP method (GET, POST, PUT, PATCH, DELETE), path, headers, query parameters, and optionally a JSON body.
      - "s3": For Amazon S3 integrations (${S3Plugin.id}). Provide a structured action (LIST_BUCKETS, LIST_BUCKET_OBJECTS, GET_OBJECT, GENERATE_PRESIGNED_URL) with optional resource (bucket name), path, and listFilesConfig.
      - "cosmosdb": For Azure Cosmos DB integrations (${CosmosDbPlugin.id}). Provide a structured action with actionCategory (sql or pointOperation) and actionCase (singleton for SQL queries, read for point reads).
      - "dynamodb": For Amazon DynamoDB integrations (${DynamoDBPlugin.id}). Provide a structured action (getItem, query, scan, listTables, describeTable) with JSON params.
      - "gcs": For Google Cloud Storage integrations (${GCSPlugin.id}). Provide a structured action (LIST_OBJECTS, GET_OBJECT, LIST_BUCKETS, GENERATE_PRESIGNED_URL) with optional resource (bucket), path, and prefix.
      - "mongodb": For MongoDB integrations (${MongoDBPlugin.id}). Provide a structured action (aggregate, count, distinct, find, findOne, listCollections) with collection, query, and other operation-specific fields.
      - "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,
        toModelOutput: ({ output }) => {
            if (output.isPartial) {
                return {
                    type: "text",
                    value: `Output too long. Max output size is ${MAX_QUERY_RESPONSE_TOKENS * 3} characters (~${MAX_QUERY_RESPONSE_TOKENS} tokens). Truncated result:
          <result>
          ${output.data}
          </result>`,
                };
            }
            return {
                type: "json",
                value: output,
            };
        },
        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 "s3": {
                    const request = buildS3Request(params);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "cosmosdb": {
                    const validCombos = [
                        ["sql", "singleton"],
                        ["pointOperation", "read"],
                    ];
                    const isValid = validCombos.some(([cat, act]) => params.actionCategory === cat && params.actionCase === act);
                    if (!isValid) {
                        throw new ToolExecutionError(`Invalid CosmosDB action combination: "${params.actionCategory}/${params.actionCase}". ` +
                            `Valid combinations are: sql/singleton, pointOperation/read.`, {
                            toolName: "executeRequestToIntegration",
                            input: args,
                            context: {},
                            severity: "info",
                        });
                    }
                    const request = buildCosmosDbRequest(params);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "dynamodb": {
                    const request = buildDynamoDBRequest(params);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "gcs": {
                    const request = buildGCSRequest(params);
                    return await executeRequest(clark.context, integrationId, request);
                }
                case "mongodb": {
                    const request = buildMongoDBRequest(params);
                    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) => {
            const permissionType = classifyPermissionType(args);
            return [
                {
                    entityType: AiEntityType.INTEGRATION,
                    entityId: args.integrationId,
                    permissionType,
                },
            ];
        },
        getActionName: (input) => {
            // When the tool is about to perform a write (POST/PUT/PATCH/DELETE,
            // GraphQL mutation, or an "other"/"write"-declared op), use an
            // operation-neutral verb in the approval dialog so users aren't
            // told they're "Querying" an integration right before an update
            // or delete is executed.
            if (input && classifyPermissionType(input) === AiPermissionType.WRITE) {
                return {
                    future: "Execute",
                    present: "Executing",
                    past: "Executed",
                };
            }
            return {
                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 { httpMethod, path, queryParams, body } = params.httpRequest;
                    const queryString = queryParams
                        ? `?${Object.entries(queryParams)
                            .map(([key, val]) => `${key}=${val}`)
                            .join("&")}`
                        : "";
                    const bodySnippet = body ? `\n${body}` : "";
                    return {
                        targets: [integration],
                        details: codeBlock(`${httpMethod} ${path}${queryString}${bodySnippet}`),
                        summary: cleanDescription,
                    };
                }
                case "s3": {
                    const details = [params.action];
                    if (params.resource)
                        details.push(params.resource);
                    if (params.path)
                        details.push(params.path);
                    return {
                        targets: [integration],
                        details: codeBlock(details.join(" ")),
                        summary: cleanDescription,
                    };
                }
                case "cosmosdb": {
                    const details = [`${params.actionCategory}/${params.actionCase}`];
                    if (params.containerId)
                        details.push(params.containerId);
                    if (params.query)
                        details.push(params.query);
                    return {
                        targets: [integration],
                        details: codeBlock(details.join(" ")),
                        summary: cleanDescription,
                    };
                }
                case "dynamodb": {
                    const details = [params.action];
                    if (params.body)
                        details.push(params.body);
                    return {
                        targets: [integration],
                        details: codeBlock(details.join(" ")),
                        summary: cleanDescription,
                    };
                }
                case "gcs": {
                    const details = [params.action];
                    if (params.resource)
                        details.push(params.resource);
                    if (params.path)
                        details.push(params.path);
                    return {
                        targets: [integration],
                        details: codeBlock(details.join(" ")),
                        summary: cleanDescription,
                    };
                }
                case "mongodb": {
                    const details = [params.action];
                    if (params.collection)
                        details.push(params.collection);
                    return {
                        targets: [integration],
                        details: codeBlock(details.join(" ")),
                        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 buildS3Request(params) {
    const request = {
        action: params.action,
    };
    if (params.resource !== undefined) {
        request.resource = params.resource;
    }
    if (params.path !== undefined) {
        request.path = params.path;
    }
    if (params.listFilesConfig !== undefined) {
        request.listFilesConfig = params.listFilesConfig;
    }
    return request;
}
function buildRestApiRequest(httpRequest) {
    const request = {
        [RestApiFields.HTTP_METHOD]: httpRequest.httpMethod,
        [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);
    }
    if (httpRequest.body) {
        request[RestApiFields.BODY] = httpRequest.body;
        request[RestApiFields.BODY_TYPE] = RestApiBodyDataType.JSON;
    }
    return request;
}
function buildDynamoDBRequest(params) {
    const request = {
        action: params.action,
    };
    if (params.body !== undefined) {
        request.body = params.body;
    }
    return request;
}
function buildGCSRequest(params) {
    const request = {
        action: params.action,
    };
    if (params.resource !== undefined) {
        request.resource = params.resource;
    }
    if (params.path !== undefined) {
        request.path = params.path;
    }
    if (params.prefix !== undefined) {
        request.prefix = params.prefix;
    }
    return request;
}
function buildCosmosDbRequest(params) {
    const request = {
        "cosmosdbAction.case": params.actionCategory,
        "cosmosdbAction.value.action.case": params.actionCase,
    };
    if (params.containerId !== undefined) {
        if (params.actionCategory === "pointOperation") {
            request["cosmosdbAction.value.containerId"] = params.containerId;
        }
        else {
            request["cosmosdbAction.value.action.value.containerId"] =
                params.containerId;
        }
    }
    if (params.query !== undefined) {
        request["cosmosdbAction.value.action.value.query"] = params.query;
    }
    if (params.partitionKey !== undefined) {
        request["cosmosdbAction.value.action.value.partitionKey"] =
            params.partitionKey;
    }
    if (params.id !== undefined) {
        request["cosmosdbAction.value.action.value.id"] = params.id;
    }
    return request;
}
function buildMongoDBRequest(params) {
    const request = {
        action: params.action,
    };
    if (params.collection !== undefined) {
        request.resource = params.collection;
    }
    if (params.query !== undefined) {
        request.query = params.query;
    }
    if (params.pipeline !== undefined) {
        request.pipeline = params.pipeline;
    }
    if (params.projection !== undefined) {
        request.projection = params.projection;
    }
    if (params.sortby !== undefined) {
        request.sortby = params.sortby;
    }
    if (params.limit !== undefined) {
        request.limit = params.limit;
    }
    if (params.field !== undefined) {
        request.field = params.field;
    }
    return request;
}
/**
 * Strips GraphQL comments and string literals from a query so that
 * keyword matching only operates on structural tokens.
 */
function stripGraphQLNonCode(query) {
    return query
        .replace(/"""[\s\S]*?"""/g, "") // strip block strings
        .replace(/"(?:[^"\\]|\\.)*"/g, "") // strip double-quoted strings
        .replace(/#.*$/gm, ""); // strip # comments (after strings so # inside strings is safe)
}
/**
 * Classifies a GraphQL query as READ or WRITE by regex-matching the top-level
 * operation type keyword on a stripped copy of the document (comments and
 * string literals removed). This is a structural check, not full AST parsing.
 *
 * Handles documents with fragments before operations and multiple operations.
 * Falls back to WRITE on any error (fail-closed).
 */
function classifyGraphQLOperation(query) {
    try {
        const stripped = stripGraphQLNonCode(query);
        // In valid GraphQL, the `mutation` keyword appears as an operation type at
        // the top level — either at the start of the document or after a closing
        // brace `}` (e.g. after a fragment definition).
        if (/(?:^|})[\s]*mutation\b/i.test(stripped)) {
            return AiPermissionType.WRITE;
        }
        return AiPermissionType.READ;
    }
    catch (err) {
        // Fail-closed: treat as WRITE on error. Log so future regressions in the
        // regex/stripping logic are debuggable instead of silently over-prompting.
        getLogger().warn("classifyGraphQLOperation failed; falling back to WRITE", err);
        return AiPermissionType.WRITE;
    }
}
/**
 * Classifies permission type using a hybrid approach:
 * 1. Deterministic floor (free): GET→READ, DELETE/PUT/PATCH→WRITE,
 *    SQL/Salesforce→READ, GraphQL structural check,
 *    S3/DynamoDB/GCS/CosmosDB/MongoDB→READ.
 * 2. Clark's declared `operation` for the remaining ambiguous cases:
 *    REST POST, and the "other" plugin type.
 *
 * Note on SQL/Salesforce: classification relies on the DML keyword check in
 * `execute()` (search for `dmlKeywords`) to actually block writes. If that
 * enforcement is ever relaxed to allow SQL writes, this function must be
 * updated in lockstep — otherwise writes will silently bypass user approval.
 */
function classifyPermissionType(args) {
    const { params, operation } = args;
    // Deterministic floor: SQL/Salesforce always READ (DML keyword check in
    // execute() — see comment above — blocks writes at request time).
    if (params.type === "sql" || params.type === "salesforce") {
        return AiPermissionType.READ;
    }
    if (params.type === "graphql") {
        return classifyGraphQLOperation(params.graphQLQuery.query);
    }
    // Deterministic floor for REST: GET is always READ; DELETE/PUT/PATCH are
    // semantically mutating and always WRITE. Only POST is truly ambiguous
    // (e.g. search APIs, RPC-style reads) and falls through to Clark's
    // declared operation below.
    if (params.type === "rest") {
        const method = params.httpRequest.httpMethod;
        if (method === "GET") {
            return AiPermissionType.READ;
        }
        if (method === "DELETE" || method === "PUT" || method === "PATCH") {
            return AiPermissionType.WRITE;
        }
    }
    // Schema-constrained read-only types (enum limits to read actions)
    if (params.type === "s3" ||
        params.type === "dynamodb" ||
        params.type === "gcs" ||
        params.type === "cosmosdb" ||
        params.type === "mongodb") {
        return AiPermissionType.READ;
    }
    // For everything else (REST POST, "other"): trust Clark's declaration.
    return operation === "write" ? AiPermissionType.WRITE : AiPermissionType.READ;
}
//# sourceMappingURL=execute-request.js.map