import { ModelType, logger } from '@elizaos/core';
import { KEYWORD_EXTRACTION_SYSTEM_PROMPT, WORKFLOW_GENERATION_SYSTEM_PROMPT, DRAFT_INTENT_SYSTEM_PROMPT, ACTION_RESPONSE_SYSTEM_PROMPT, FEASIBILITY_CHECK_PROMPT, FIELD_CORRECTION_SYSTEM_PROMPT, FIELD_CORRECTION_USER_PROMPT, PARAM_CORRECTION_SYSTEM_PROMPT, PARAM_CORRECTION_USER_PROMPT, } from '../prompts/index';
import { WORKFLOW_MATCHING_SYSTEM_PROMPT } from '../prompts/workflowMatching';
import { keywordExtractionSchema, workflowMatchingSchema, draftIntentSchema, feasibilitySchema, } from '../schemas/index';
import { getNodeDefinition, simplifyNodeForLLM } from './catalog';
import { hasOutputSchema, getAvailableResources, getAvailableOperations, loadOutputSchema, formatSchemaForPrompt, } from './outputSchema';
export async function extractKeywords(runtime, userPrompt) {
    let result;
    try {
        result = (await runtime.useModel(ModelType.OBJECT_SMALL, {
            prompt: `${KEYWORD_EXTRACTION_SYSTEM_PROMPT}\n\nUser request: ${userPrompt}`,
            schema: keywordExtractionSchema,
        }));
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        logger.error({ src: 'plugin:n8n-workflow:generation:keywords', error: errMsg }, `Keyword extraction LLM call failed: ${errMsg}`);
        throw new Error(`Keyword extraction failed: ${errMsg}`);
    }
    // Validate structure
    if (!result || !result.keywords || !Array.isArray(result.keywords)) {
        logger.error({ src: 'plugin:n8n-workflow:generation:keywords', result: JSON.stringify(result) }, 'Invalid keyword extraction response structure');
        throw new Error('Invalid keyword extraction response: missing or invalid keywords array');
    }
    // Validate all items are strings
    if (!result.keywords.every((kw) => typeof kw === 'string')) {
        throw new Error('Keywords array contains non-string elements');
    }
    // Limit to 5 keywords max, filter empty strings
    return result.keywords
        .slice(0, 5)
        .map((kw) => kw.trim())
        .filter((kw) => kw.length > 0);
}
export async function matchWorkflow(runtime, userRequest, workflows) {
    if (workflows.length === 0) {
        return {
            matchedWorkflowId: null,
            confidence: 'none',
            matches: [],
            reason: 'No workflows available',
        };
    }
    try {
        // Build workflow list for LLM
        const workflowList = workflows
            .map((wf, index) => `${index + 1}. "${wf.name}" (ID: ${wf.id}, Status: ${wf.active ? 'ACTIVE' : 'INACTIVE'})`)
            .join('\n');
        const userPrompt = `${userRequest}

Available workflows:
${workflowList}`;
        let result;
        try {
            result = (await runtime.useModel(ModelType.OBJECT_SMALL, {
                prompt: `${WORKFLOW_MATCHING_SYSTEM_PROMPT}\n\n${userPrompt}`,
                schema: workflowMatchingSchema,
            }));
        }
        catch (innerError) {
            const errMsg = innerError instanceof Error ? innerError.message : String(innerError);
            logger.error({ src: 'plugin:n8n-workflow:generation:matcher', error: errMsg }, `Workflow matching LLM call failed: ${errMsg}`);
            throw innerError;
        }
        // Validate the returned ID actually exists in the provided list
        if (result.matchedWorkflowId && !workflows.some((wf) => wf.id === result.matchedWorkflowId)) {
            logger.warn({ src: 'plugin:n8n-workflow:generation:matcher' }, `LLM returned non-existent workflow ID "${result.matchedWorkflowId}" — discarding`);
            result.matchedWorkflowId = null;
            result.confidence = 'none';
        }
        logger.debug({ src: 'plugin:n8n-workflow:generation:matcher' }, `Workflow match: ${result.matchedWorkflowId || 'none'} (confidence: ${result.confidence})`);
        return result;
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        logger.error({ src: 'plugin:n8n-workflow:generation:matcher' }, `Workflow matching failed: ${errorMessage}`);
        return {
            matchedWorkflowId: null,
            confidence: 'none',
            matches: [],
            reason: `Workflow matching service unavailable: ${errorMessage}`,
        };
    }
}
export async function classifyDraftIntent(runtime, userMessage, draft) {
    const draftSummary = `Workflow: "${draft.workflow.name}"
Nodes: ${draft.workflow.nodes.map((n) => `${n.name} (${n.type})`).join(', ')}
Original prompt: "${draft.prompt}"`;
    let result;
    try {
        result = (await runtime.useModel(ModelType.OBJECT_SMALL, {
            prompt: `${DRAFT_INTENT_SYSTEM_PROMPT}

## Current Draft

${draftSummary}

## User Message

${userMessage}`,
            schema: draftIntentSchema,
        }));
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        logger.error({ src: 'plugin:n8n-workflow:generation:intent', error: errMsg }, `classifyDraftIntent LLM call failed: ${errMsg}`);
        return {
            intent: 'show_preview',
            reason: `Intent classification failed (${errMsg}) — re-showing preview`,
        };
    }
    const validIntents = ['confirm', 'cancel', 'modify', 'new'];
    if (!result?.intent || !validIntents.includes(result.intent)) {
        logger.warn({ src: 'plugin:n8n-workflow:generation:intent' }, `Invalid intent from LLM: ${JSON.stringify(result)}, re-showing preview`);
        return { intent: 'show_preview', reason: 'Could not classify intent — re-showing preview' };
    }
    return result;
}
function parseWorkflowResponse(response) {
    // Strip markdown code fences (handles ```json, ```, with any whitespace/newlines)
    const cleaned = response
        .replace(/^[\s\S]*?```(?:json)?\s*\n?/i, '') // Remove everything up to and including opening fence
        .replace(/\n?```[\s\S]*$/i, '') // Remove closing fence and everything after
        .trim();
    let workflow;
    try {
        workflow = JSON.parse(cleaned);
    }
    catch (error) {
        throw new Error(`Failed to parse workflow JSON: ${error instanceof Error ? error.message : String(error)}\n\nRaw response: ${response}`);
    }
    if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
        throw new Error('Invalid workflow: missing or invalid nodes array');
    }
    if (!workflow.connections || typeof workflow.connections !== 'object') {
        throw new Error('Invalid workflow: missing or invalid connections object');
    }
    return workflow;
}
/**
 * Build output schema context for relevant nodes so the LLM knows the exact
 * output fields when writing expressions like {{ $json.field }}.
 */
function buildOutputSchemaContext(nodes) {
    const sections = [];
    for (const node of nodes) {
        if (!hasOutputSchema(node.name)) {
            continue;
        }
        const resources = getAvailableResources(node.name);
        for (const resource of resources) {
            const operations = getAvailableOperations(node.name, resource);
            for (const operation of operations) {
                const result = loadOutputSchema(node.name, resource, operation);
                if (!result) {
                    continue;
                }
                const formatted = formatSchemaForPrompt(result.schema);
                sections.push(`### ${node.name} (resource: "${resource}", operation: "${operation}")\n${formatted}`);
            }
        }
    }
    if (sections.length === 0) {
        return '';
    }
    return `\n## Node Output Schemas\n\nWhen referencing output data from a previous node using expressions like \`{{ $json.field }}\`, use ONLY the field paths listed below. Do NOT invent field names from your training data.\n\n${sections.join('\n\n')}`;
}
export async function generateWorkflow(runtime, userPrompt, relevantNodes) {
    const simplifiedNodes = relevantNodes.map(simplifyNodeForLLM);
    const outputSchemaCtx = buildOutputSchemaContext(relevantNodes);
    const fullPrompt = `${WORKFLOW_GENERATION_SYSTEM_PROMPT}

## Relevant Nodes Available

${JSON.stringify(simplifiedNodes, null, 2)}

Use these node definitions to generate the workflow. Each node's "properties" field defines the available parameters.
${outputSchemaCtx}

## User Request

${userPrompt}

Generate a valid n8n workflow JSON that fulfills this request.`;
    const response = await runtime.useModel(ModelType.TEXT_LARGE, {
        prompt: fullPrompt,
        temperature: 0,
        responseFormat: { type: 'json_object' },
    });
    const workflow = parseWorkflowResponse(response);
    if (!workflow.name) {
        workflow.name = `Workflow - ${userPrompt.slice(0, 50).trim()}`;
    }
    return workflow;
}
export async function modifyWorkflow(runtime, existingWorkflow, modificationRequest, relevantNodes) {
    const { _meta, ...workflowForLLM } = existingWorkflow;
    const simplifiedNodes = relevantNodes.map(simplifyNodeForLLM);
    const outputSchemaCtx = buildOutputSchemaContext(relevantNodes);
    const fullPrompt = `${WORKFLOW_GENERATION_SYSTEM_PROMPT}

## Relevant Nodes Available

${JSON.stringify(simplifiedNodes, null, 2)}

Use these node definitions to modify the workflow. Each node's "properties" field defines the available parameters.
${outputSchemaCtx}

## Existing Workflow (modify this)

${JSON.stringify(workflowForLLM, null, 2)}

## Modification Request

${modificationRequest}

Modify the existing workflow according to the request above. Return the COMPLETE modified workflow JSON.
Keep all unchanged nodes and connections intact. Only add, remove, or change what the user asked for.`;
    const response = await runtime.useModel(ModelType.TEXT_LARGE, {
        prompt: fullPrompt,
        temperature: 0,
        responseFormat: { type: 'json_object' },
    });
    const modified = parseWorkflowResponse(response);
    // Preserve the original workflow ID for updates (LLM doesn't return it)
    if (existingWorkflow.id) {
        modified.id = existingWorkflow.id;
    }
    return modified;
}
export function collectExistingNodeDefinitions(workflow) {
    const defs = [];
    const seen = new Set();
    for (const node of workflow.nodes) {
        if (seen.has(node.type)) {
            continue;
        }
        seen.add(node.type);
        const def = getNodeDefinition(node.type);
        if (def) {
            defs.push(def);
        }
        else {
            logger.warn({ src: 'plugin:n8n-workflow:generation:modify' }, `No catalog definition found for node type "${node.type}" — LLM will have limited context for this node`);
        }
    }
    return defs;
}
export async function formatActionResponse(runtime, responseType, data) {
    try {
        const response = await runtime.useModel(ModelType.TEXT_SMALL, {
            prompt: `${ACTION_RESPONSE_SYSTEM_PROMPT}\n\nType: ${responseType}\n\n${JSON.stringify(data)}`,
        });
        return response.trim();
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        logger.error({ src: 'plugin:n8n-workflow:generation:format', error: errMsg, responseType }, `formatActionResponse LLM call failed: ${errMsg}`);
        // Return a fallback message so the action can still communicate with the user
        if (responseType === 'ERROR') {
            return `An error occurred: ${data.error || 'Unknown error'}`;
        }
        return `Operation completed (type: ${responseType})`;
    }
}
export async function assessFeasibility(runtime, userPrompt, removedNodes, remainingNodes) {
    const removedList = removedNodes
        .filter((r) => r.node.credentials?.length)
        .map((r) => r.node.displayName)
        .join(', ');
    const availableList = remainingNodes
        .filter((r) => r.node.credentials?.length)
        .map((r) => r.node.displayName)
        .join(', ');
    const utilityList = remainingNodes
        .filter((r) => !r.node.credentials?.length)
        .map((r) => r.node.displayName)
        .join(', ');
    try {
        const result = (await runtime.useModel(ModelType.OBJECT_SMALL, {
            prompt: `${FEASIBILITY_CHECK_PROMPT}\n\n## User Request\n${userPrompt}` +
                `\n\n## Removed Integrations (unavailable)\n${removedList}` +
                `\n\n## Available Service Integrations\n${availableList}` +
                `\n\n## Available Utility Nodes\n${utilityList}`,
            schema: feasibilitySchema,
        }));
        return result;
    }
    catch (error) {
        const errMsg = error instanceof Error ? error.message : String(error);
        logger.error({ src: 'plugin:n8n-workflow:generation:feasibility', error: errMsg }, `Feasibility assessment LLM call failed: ${errMsg}`);
        return {
            feasible: false,
            reason: `Feasibility check failed: ${errMsg}`,
        };
    }
}
/**
 * Auto-corrects invalid field references in expressions using parallel LLM calls.
 * Returns a new workflow with corrected expressions.
 */
export async function correctFieldReferences(runtime, workflow, invalidRefs) {
    if (invalidRefs.length === 0) {
        return workflow;
    }
    logger.debug({ src: 'plugin:n8n-workflow:generation:correction' }, `Correcting ${invalidRefs.length} invalid field reference(s)`);
    const corrections = await Promise.all(invalidRefs.map(async (ref) => {
        try {
            const userPrompt = FIELD_CORRECTION_USER_PROMPT.replace('{expression}', ref.expression).replace('{availableFields}', ref.availableFields.join('\n'));
            const corrected = await runtime.useModel(ModelType.TEXT_SMALL, {
                prompt: `${FIELD_CORRECTION_SYSTEM_PROMPT}\n\n${userPrompt}`,
                temperature: 0,
            });
            const cleaned = corrected.trim();
            return { original: ref.expression, corrected: cleaned, nodeName: ref.nodeName };
        }
        catch (error) {
            const errMsg = error instanceof Error ? error.message : String(error);
            logger.warn({ src: 'plugin:n8n-workflow:generation:correction', error: errMsg }, `Failed to correct expression "${ref.expression}": ${errMsg}`);
            return null;
        }
    }));
    const correctedWorkflow = JSON.parse(JSON.stringify(workflow));
    for (const correction of corrections) {
        if (!correction) {
            continue;
        }
        const node = correctedWorkflow.nodes.find((n) => n.name === correction.nodeName);
        if (!node?.parameters) {
            continue;
        }
        replaceInObject(node.parameters, correction.original, correction.corrected);
        logger.debug({ src: 'plugin:n8n-workflow:generation:correction' }, `Corrected "${correction.original}" → "${correction.corrected}" in node "${correction.nodeName}"`);
    }
    return correctedWorkflow;
}
function replaceInObject(obj, original, replacement) {
    for (const key of Object.keys(obj)) {
        const value = obj[key];
        if (typeof value === 'string' && value.includes(original)) {
            obj[key] = value.replaceAll(original, replacement);
        }
        else if (Array.isArray(value)) {
            for (let i = 0; i < value.length; i++) {
                if (typeof value[i] === 'string' && value[i].includes(original)) {
                    value[i] = value[i].replaceAll(original, replacement);
                }
                else if (typeof value[i] === 'object' && value[i] !== null) {
                    replaceInObject(value[i], original, replacement);
                }
            }
        }
        else if (typeof value === 'object' && value !== null) {
            replaceInObject(value, original, replacement);
        }
    }
}
/**
 * Try to deterministically rename an unknown key to a valid property name.
 * Returns the matching property name or null if no confident match.
 */
function fuzzyMatchParam(unknownKey, validProps) {
    const lower = unknownKey.toLowerCase();
    // Substring match: one valid prop contains the unknown key or vice-versa
    // Guard: shorter string must be ≥ 60% of the longer to avoid false positives (e.g. "url" in "curl")
    const substringMatches = validProps.filter((p) => {
        const pLower = p.name.toLowerCase();
        if (!(pLower.includes(lower) || lower.includes(pLower))) {
            return false;
        }
        const ratio = Math.min(lower.length, pLower.length) / Math.max(lower.length, pLower.length);
        return ratio >= 0.6;
    });
    if (substringMatches.length === 1) {
        return substringMatches[0].name;
    }
    return null;
}
/** Deterministic fast path + LLM fallback for parameter name correction. */
export async function correctParameterNames(runtime, workflow, detections) {
    if (detections.length === 0) {
        return workflow;
    }
    const correctedWorkflow = JSON.parse(JSON.stringify(workflow));
    // Phase 1: deterministic renames (no LLM cost)
    const needsLLM = [];
    for (const detection of detections) {
        const node = correctedWorkflow.nodes.find((n) => n.name === detection.nodeName);
        if (!node) {
            continue;
        }
        const remainingUnknowns = [];
        for (const key of detection.unknownKeys) {
            const match = fuzzyMatchParam(key, detection.propertyDefs);
            if (match) {
                logger.debug({ src: 'plugin:n8n-workflow:generation:paramCorrection' }, `Node "${detection.nodeName}": ${key} → ${match} (deterministic)`);
                node.parameters[match] = node.parameters[key];
                delete node.parameters[key];
            }
            else {
                remainingUnknowns.push(key);
            }
        }
        if (remainingUnknowns.length > 0) {
            needsLLM.push({
                ...detection,
                unknownKeys: remainingUnknowns,
                currentParams: node.parameters,
            });
        }
    }
    if (needsLLM.length === 0) {
        return correctedWorkflow;
    }
    // Phase 2: LLM correction for complex cases (restructuring)
    logger.debug({ src: 'plugin:n8n-workflow:generation:paramCorrection' }, `LLM correction needed for ${needsLLM.length} node(s): ${needsLLM.map((d) => `"${d.nodeName}" (${d.unknownKeys.join(', ')})`).join('; ')}`);
    const corrections = await Promise.all(needsLLM.map(async (detection) => {
        try {
            const userPrompt = PARAM_CORRECTION_USER_PROMPT.replace('{nodeType}', detection.nodeType)
                .replace('{currentParams}', JSON.stringify(detection.currentParams, null, 2))
                .replace('{propertyDefs}', JSON.stringify(detection.propertyDefs, null, 2));
            const response = await runtime.useModel(ModelType.TEXT_SMALL, {
                prompt: `${PARAM_CORRECTION_SYSTEM_PROMPT}\n\n${userPrompt}`,
                temperature: 0,
            });
            const cleaned = response
                .replace(/^[\s\S]*?```(?:json)?\s*\n?/i, '')
                .replace(/\n?```[\s\S]*$/i, '')
                .trim();
            const correctedParams = JSON.parse(cleaned);
            // Validate: corrected params should only contain valid property names
            const validNames = new Set(detection.propertyDefs.map((p) => p.name));
            validNames.add('resource');
            validNames.add('operation');
            const invalidKeys = Object.keys(correctedParams).filter((k) => !validNames.has(k));
            if (invalidKeys.length > 0) {
                logger.warn({ src: 'plugin:n8n-workflow:generation:paramCorrection' }, `LLM returned invalid keys for "${detection.nodeName}": ${invalidKeys.join(', ')} — dropping them`);
                for (const k of invalidKeys) {
                    delete correctedParams[k];
                }
            }
            return { nodeName: detection.nodeName, correctedParams };
        }
        catch (error) {
            const errMsg = error instanceof Error ? error.message : String(error);
            logger.warn({ src: 'plugin:n8n-workflow:generation:paramCorrection', error: errMsg }, `Failed to correct parameters for node "${detection.nodeName}": ${errMsg}`);
            return null;
        }
    }));
    for (const correction of corrections) {
        if (!correction) {
            continue;
        }
        const node = correctedWorkflow.nodes.find((n) => n.name === correction.nodeName);
        if (!node) {
            continue;
        }
        // Preserve resource/operation (already corrected by correctOptionParameters)
        if (node.parameters.resource !== undefined) {
            correction.correctedParams.resource = node.parameters.resource;
        }
        if (node.parameters.operation !== undefined) {
            correction.correctedParams.operation = node.parameters.operation;
        }
        logger.debug({ src: 'plugin:n8n-workflow:generation:paramCorrection' }, `Node "${correction.nodeName}": params corrected via LLM — keys: ${Object.keys(correction.correctedParams).join(', ')}`);
        node.parameters = correction.correctedParams;
    }
    return correctedWorkflow;
}
//# sourceMappingURL=generation.js.map