"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLlmMockHandler = createLlmMockHandler;
const backend_common_1 = require("@n8n/backend-common");
const di_1 = require("@n8n/di");
const n8n_workflow_1 = require("n8n-workflow");
const zod_1 = require("zod");
const instance_ai_1 = require("@n8n/instance-ai");
const api_docs_1 = require("./api-docs");
const node_config_1 = require("./node-config");
const request_sanitizer_1 = require("./request-sanitizer");
const MOCK_SYSTEM_PROMPT = `You are an API mock server generating realistic HTTP responses for n8n workflow evaluation.

## Your tools

You have two tools. Call them before generating your response:

"lookup_api_docs" — Fetches real API documentation for a service endpoint. Use this to learn the correct response STRUCTURE (what fields, what nesting, what types the real API returns). Pay special attention to what the real API returns for the exact HTTP method and URL path you're responding to.

"get_node_config" — Returns the n8n node's configuration parameters. This tells you what the node is set up to work with. The configuration contains the values the node expects to find in API responses — resource IDs, field names, column names, etc. Every node type has different parameters, so you need to interpret the config intelligently. Key patterns:
  - Objects with "__rl" are resource selectors — "value" is the selected resource (a document ID, channel, project, etc.)
  - "schema" arrays list the columns/fields the node expects. CRITICAL: use the "id" field as the exact column/field name in your response — NOT "displayName". For example, if schema has {"id": "timestamp", "displayName": "Timestamp"}, the API response must use "timestamp" (lowercase), not "Timestamp"
  - "operation" and "resource" describe what the node does (e.g. "send" a "message", "create" an "issue")
  - Strings starting with "=" are expressions (ignore these) — all other strings are literal values

## How to combine them

The API docs tell you the response SHAPE. The node config tells you the exact DATA VALUES to put in that shape. All names, IDs, and identifiers from the node config are case-sensitive — use them character-for-character.

## Output format

Respond with ONLY a JSON object. No explanation, no markdown, no prose.

{ "type": "json", "body": { ...realistic API response... } }
{ "type": "binary", "contentType": "application/pdf", "filename": "doc.pdf" }
{ "type": "error", "statusCode": 404, "body": { ...service error format... } }

## Rules

- A node may make MULTIPLE sequential HTTP requests in a single execution (e.g., first GET metadata, then GET headers, then POST data). You are responding to ONE specific request. Match your response to the URL + method of THIS request only. A GET to a metadata endpoint must return metadata — not a write result — even if the node's overall purpose is to write data.
- Echo request values faithfully. If the request contains an identifier, name, or reference value (even one that looks like a placeholder such as "YOUR_CHAT_ID" or "YOUR_API_KEY"), echo it back exactly in the corresponding response field. The real API would reflect the same value the client sent.
- Some APIs return empty or minimal responses on success (204 with no body, 202 with empty body). If the API documentation indicates an empty response body, return { "type": "json", "body": {} }. Don't invent additional response fields.
- No pagination — always indicate end of results (has_more=false, nextPageToken=null, etc.)`;
const DEFAULT_MAX_RETRIES = 1;
function createLlmMockHandler(options) {
    const nodeConfigCache = new Map();
    return async (requestOptions, node) => {
        if (!nodeConfigCache.has(node.name)) {
            nodeConfigCache.set(node.name, (0, node_config_1.extractNodeConfig)(node));
        }
        return await generateMockResponse(requestOptions, node, {
            scenarioHints: options?.scenarioHints,
            globalContext: options?.globalContext,
            nodeHint: options?.nodeHints?.[node.name],
            nodeConfig: nodeConfigCache.get(node.name) ?? '',
            maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES,
        });
    };
}
async function generateMockResponse(request, node, context) {
    const serviceName = extractServiceName(request.url);
    const endpoint = extractEndpoint(request.url);
    const sections = [
        '## Request',
        `Service: ${serviceName}`,
        `Node: "${node.name}" (${node.type})`,
        (request.method ?? 'GET') + ' ' + endpoint,
        'Generate the response for this EXACT endpoint and method.',
    ];
    if (request.body) {
        const sanitized = (0, request_sanitizer_1.redactSecretKeys)(request.body);
        const serialized = (0, request_sanitizer_1.truncateForLlm)(JSON.stringify(sanitized));
        sections.push(`Body: ${serialized}`);
    }
    if (request.qs && Object.keys(request.qs).length > 0) {
        const sanitizedQs = (0, request_sanitizer_1.redactSecretKeys)(request.qs);
        sections.push(`Query: ${JSON.stringify(sanitizedQs)}`);
    }
    const isGraphQL = endpoint.includes('/graphql') ||
        (typeof request.body === 'object' && request.body !== null && 'query' in request.body);
    if (isGraphQL) {
        sections.push('', '## GraphQL format requirement');
        sections.push('This is a GraphQL endpoint. ALL responses MUST use GraphQL response format:', '- Success: { "data": { ...fields matching the query... } }', '- Error: { "errors": [{ "message": "...", "extensions": { "code": "..." } }], "data": null }', 'Never return flat REST-style error objects.');
    }
    if (context.nodeConfig) {
        sections.push('', '## Node Configuration', context.nodeConfig);
    }
    if (context.globalContext || context.nodeHint || context.scenarioHints) {
        sections.push('', '## Context');
        if (context.globalContext)
            sections.push(`Data: ${context.globalContext}`);
        if (context.nodeHint)
            sections.push(`Hint: ${context.nodeHint}`);
        if (context.scenarioHints) {
            sections.push(`Scenario: ${context.scenarioHints}`);
            sections.push(isGraphQL
                ? '(For error scenarios, use GraphQL error format with "data": null. Don\'t use "type": "error" wrapper.)'
                : '(Use "error" type with appropriate statusCode for error scenarios.)');
        }
    }
    const userPrompt = sections.join('\n');
    const safeUrl = extractEndpoint(request.url);
    let lastError = '';
    for (let attempt = 0; attempt <= context.maxRetries; attempt++) {
        try {
            const spec = await callLlm(userPrompt, context.nodeConfig);
            return materializeSpec(spec);
        }
        catch (error) {
            lastError = error instanceof Error ? error.message : String(error);
            if (attempt < context.maxRetries) {
                di_1.Container.get(backend_common_1.Logger).warn(`[EvalMock] Mock generation failed for ${request.method ?? 'GET'} ${safeUrl}, retrying (${attempt + 1}/${context.maxRetries}): ${lastError}`);
            }
        }
    }
    di_1.Container.get(backend_common_1.Logger).error(`[EvalMock] Mock generation failed for ${request.method ?? 'GET'} ${safeUrl} after ${context.maxRetries + 1} attempts: ${lastError}`);
    return {
        body: { _evalMockError: true, message: `Mock generation failed: ${lastError}` },
        headers: { 'content-type': 'application/json' },
        statusCode: 200,
    };
}
const apiDocsTool = new instance_ai_1.Tool('lookup_api_docs')
    .description('Look up official API documentation for a specific REST endpoint to understand the exact response format.')
    .input(zod_1.z.object({
    serviceName: zod_1.z
        .string()
        .describe('The API service name (e.g. "Google Sheets", "Gmail", "Slack")'),
    endpointDescription: zod_1.z
        .string()
        .describe('Description of the endpoint (e.g. "GET spreadsheets values response format")'),
}))
    .handler(async (input) => {
    return await (0, api_docs_1.fetchApiDocs)(input.serviceName, input.endpointDescription);
})
    .build();
function createNodeConfigTool(nodeConfig) {
    return new instance_ai_1.Tool('get_node_config')
        .description("Get the n8n node's configuration parameters — resource IDs, field names, settings, etc. Your mock data must match these exact values.")
        .input(zod_1.z.object({}))
        .handler(async () => nodeConfig)
        .build();
}
async function callLlm(userPrompt, nodeConfig) {
    const agent = (0, instance_ai_1.createEvalAgent)('eval-mock-responder', {
        instructions: MOCK_SYSTEM_PROMPT,
    })
        .tool(apiDocsTool)
        .tool(createNodeConfigTool(nodeConfig));
    const result = await agent.generate(userPrompt);
    const text = (0, instance_ai_1.extractText)(result);
    return parseResponseText(text);
}
function parseResponseText(raw) {
    let text = raw.trim();
    const fencedMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/i);
    if (fencedMatch) {
        text = fencedMatch[1].trim();
    }
    text = text
        .replace(/^```(?:json)?\s*\n?/i, '')
        .replace(/\n?\s*```\s*$/i, '')
        .trim();
    if (text.length > 0 && !text.startsWith('{') && !text.startsWith('[')) {
        const extracted = extractJsonObject(text);
        if (extracted) {
            text = extracted;
        }
    }
    const parsed = (0, n8n_workflow_1.jsonParse)(text);
    if (!parsed.type || !['json', 'binary', 'error'].includes(parsed.type)) {
        return { type: 'json', body: parsed };
    }
    return parsed;
}
function materializeSpec(spec) {
    switch (spec.type) {
        case 'json':
            return {
                body: spec.body ?? { ok: true },
                headers: { 'content-type': 'application/json' },
                statusCode: 200,
            };
        case 'binary': {
            const filename = spec.filename ?? 'mock-file.dat';
            const contentType = spec.contentType ?? 'application/octet-stream';
            const content = `[eval-mock] Synthetic file: ${filename} (${contentType})`;
            return {
                body: Buffer.from(content),
                headers: { 'content-type': contentType },
                statusCode: 200,
            };
        }
        case 'error':
            return {
                body: spec.body ?? { error: 'Mock error' },
                headers: { 'content-type': 'application/json' },
                statusCode: spec.statusCode ?? 500,
            };
        default:
            return {
                body: spec.body ?? { ok: true },
                headers: { 'content-type': 'application/json' },
                statusCode: 200,
            };
    }
}
function extractJsonObject(text) {
    const start = text.indexOf('{');
    if (start < 0)
        return undefined;
    let depth = 0;
    let inString = false;
    let escape = false;
    for (let i = start; i < text.length; i++) {
        const ch = text[i];
        if (escape) {
            escape = false;
            continue;
        }
        if (ch === '\\' && inString) {
            escape = true;
            continue;
        }
        if (ch === '"') {
            inString = !inString;
            continue;
        }
        if (inString)
            continue;
        if (ch === '{')
            depth++;
        if (ch === '}') {
            depth--;
            if (depth === 0) {
                return text.slice(start, i + 1);
            }
        }
    }
    return text.slice(start);
}
function extractServiceName(url) {
    try {
        const hostname = new URL(url).hostname;
        const parts = hostname
            .replace(/^api\./, '')
            .replace(/^www\./, '')
            .split('.');
        return parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
    }
    catch {
        return 'Unknown';
    }
}
function extractEndpoint(url) {
    try {
        const parsed = new URL(url);
        return parsed.pathname + (parsed.search ? parsed.search : '');
    }
    catch {
        return url;
    }
}
//# sourceMappingURL=mock-handler.js.map