/**
 * Consolidated executions tool — list, get, run, debug, get-node-output,
 * get-resolved-node-parameters, stop.
 */
import { Tool } from '@n8n/agents';
import { instanceAiConfirmationSeveritySchema } from '@n8n/api-types';
import { nanoid } from 'nanoid';
import { z } from 'zod';

import { sanitizeInputSchema } from '../agent/sanitize-mcp-schemas';
import type { InstanceAiContext } from '../types';

// ── Constants ──────────────────────────────────────────────────────────────

const MAX_TIMEOUT_MS = 600_000;

// ── Action schemas ─────────────────────────────────────────────────────────

const listAction = z.object({
	action: z.literal('list').describe('List recent workflow executions'),
	workflowId: z.string().optional().describe('Workflow ID'),
	status: z
		.string()
		.optional()
		.describe('Filter by status (e.g. "success", "error", "running", "waiting")'),
	limit: z
		.number()
		.int()
		.positive()
		.max(100)
		.optional()
		.describe('Max results to return (default 20)'),
});

const getAction = z.object({
	action: z.literal('get').describe('Get execution status without blocking (poll running ones)'),
	executionId: z.string().describe('Execution ID'),
});

const runAction = z.object({
	action: z.literal('run').describe('Execute a workflow and wait for completion'),
	workflowId: z.string().describe('Workflow ID'),
	inputData: z
		.record(z.unknown())
		.optional()
		.describe(
			'Input data passed to the workflow trigger. Works for ANY trigger type — ' +
				'the system injects inputData as the trigger node output, bypassing the need for a real event. ' +
				'For webhook triggers, inputData is the request body (do NOT wrap in { body: ... }). ' +
				'For event-based triggers (e.g. Linear, GitHub, Slack), pass inputData matching ' +
				'the shape the trigger would emit (e.g. { action: "create", data: { ... } }).',
		),
	timeout: z
		.number()
		.int()
		.min(1000)
		.max(MAX_TIMEOUT_MS)
		.optional()
		.describe('Max wait time in milliseconds (default 300000, max 600000)'),
});

const debugAction = z.object({
	action: z
		.literal('debug')
		.describe(
			'Analyze a failed execution with structured diagnostics. When a node failed, ' +
				"`failedNode.resolvedParameters` includes that node's raw parameters, the same " +
				'tree with expressions substituted, and lists of expressions that threw or resolved to empty values',
		),
	executionId: z.string().describe('Execution ID'),
});

const getNodeOutputAction = z.object({
	action: z
		.literal('get-node-output')
		.describe('Retrieve raw output of a specific node from an execution'),
	executionId: z.string().describe('Execution ID'),
	nodeName: z.string().describe("Name of the node (must exist in the execution's workflow)"),
	startIndex: z.number().int().min(0).optional().describe('Item index to start from (default 0)'),
	maxItems: z
		.number()
		.int()
		.min(1)
		.max(50)
		.optional()
		.describe('Maximum number of items to return (default 10, max 50)'),
});

const getResolvedNodeParametersAction = z.object({
	action: z
		.literal('get-resolved-node-parameters')
		.describe(
			"Replay expression resolution for a node's parameters against a past execution. " +
				'Returns the raw `parameters` (with expressions intact), the `resolved` tree (same ' +
				'shape, expressions substituted), `failedExpressions` (those that threw), and ' +
				'`emptyResolutions` (those that resolved to `null`/`undefined`/`""` — the common ' +
				'silent cause of empty downstream fields). Use this when debugging why a node ' +
				'received an unexpected value or failed because of a parameter — far more precise ' +
				'than guessing from raw expression strings or input data.',
		),
	executionId: z.string().describe('Execution ID'),
	nodeName: z.string().describe("Name of the node (must exist in the execution's workflow)"),
	itemIndex: z
		.number()
		.int()
		.min(0)
		.optional()
		.describe('Input item index to resolve against (default 0)'),
	runIndex: z
		.number()
		.int()
		.min(0)
		.optional()
		.describe('Which run of the node to use, if it ran multiple times (default: last run)'),
});

const stopAction = z.object({
	action: z.literal('stop').describe('Cancel a running workflow execution'),
	executionId: z.string().describe('Execution ID'),
});

const inputSchema = sanitizeInputSchema(
	z.discriminatedUnion('action', [
		listAction,
		getAction,
		runAction,
		debugAction,
		getNodeOutputAction,
		getResolvedNodeParametersAction,
		stopAction,
	]),
);

type Input = z.infer<typeof inputSchema>;

// ── Suspend / resume schemas (used by `run`) ───────────────────────────────

const suspendSchema = z.object({
	requestId: z.string(),
	message: z.string(),
	severity: instanceAiConfirmationSeveritySchema,
});

const resumeSchema = z.object({
	approved: z.boolean(),
});

// ── Handlers ───────────────────────────────────────────────────────────────

async function handleList(context: InstanceAiContext, input: Extract<Input, { action: 'list' }>) {
	const executions = await context.executionService.list({
		workflowId: input.workflowId,
		status: input.status,
		limit: input.limit,
	});
	return { executions };
}

async function handleGet(context: InstanceAiContext, input: Extract<Input, { action: 'get' }>) {
	return await context.executionService.getStatus(input.executionId);
}

async function handleRun(
	context: InstanceAiContext,
	input: Extract<Input, { action: 'run' }>,
	resumeData: z.infer<typeof resumeSchema> | undefined,
	suspend: (payload: z.infer<typeof suspendSchema>) => Promise<never>,
) {
	if (context.permissions?.runWorkflow === 'blocked') {
		return {
			executionId: '',
			status: 'error' as const,
			denied: true,
			reason: 'Action blocked by admin',
		};
	}

	// `always_allow` is only honored for the workflow IDs the caller pre-authorized
	// (e.g. checkpoint follow-ups scope the override to the workflows the checkpoint
	// is verifying). When the allow-list is unset, `always_allow` applies broadly,
	// matching the legacy behavior.
	const allowList = context.allowedRunWorkflowIds;
	const allowedByScope =
		context.permissions?.runWorkflow === 'always_allow' &&
		(allowList === undefined || allowList.has(input.workflowId));
	const needsApproval = !allowedByScope;

	// If approval is required and this is the first call, suspend for confirmation
	if (needsApproval && (resumeData === undefined || resumeData === null)) {
		const workflowName = await context.workflowService
			.get(input.workflowId)
			.then((wf) => wf.name)
			.catch(() => input.workflowId);
		return await suspend({
			requestId: nanoid(),
			message: `Execute ${workflowName} (ID: ${input.workflowId})`,
			severity: 'warning' as const,
		});
	}

	// If resumed with denial
	if (resumeData !== undefined && resumeData !== null && !resumeData.approved) {
		return {
			executionId: '',
			status: 'error' as const,
			denied: true,
			reason: 'User denied the action',
		};
	}

	// Approved or always_allow — execute
	return await context.executionService.run(input.workflowId, input.inputData, {
		timeout: input.timeout,
	});
}

async function handleDebug(context: InstanceAiContext, input: Extract<Input, { action: 'debug' }>) {
	return await context.executionService.getDebugInfo(input.executionId);
}

async function handleGetNodeOutput(
	context: InstanceAiContext,
	input: Extract<Input, { action: 'get-node-output' }>,
) {
	return await context.executionService.getNodeOutput(input.executionId, input.nodeName, {
		startIndex: input.startIndex,
		maxItems: input.maxItems,
	});
}

async function handleGetResolvedNodeParameters(
	context: InstanceAiContext,
	input: Extract<Input, { action: 'get-resolved-node-parameters' }>,
) {
	return await context.executionService.getResolvedNodeParameters(
		input.executionId,
		input.nodeName,
		{
			itemIndex: input.itemIndex,
			runIndex: input.runIndex,
		},
	);
}

async function handleStop(context: InstanceAiContext, input: Extract<Input, { action: 'stop' }>) {
	return await context.executionService.stop(input.executionId);
}

// ── Tool factory ───────────────────────────────────────────────────────────

export function createExecutionsTool(context: InstanceAiContext) {
	return new Tool('executions')
		.description(
			'Manage workflow executions — list, inspect, run, debug, get node output, ' +
				'get resolved node parameters for a past run, and stop.',
		)
		.input(inputSchema)
		.suspend(suspendSchema)
		.resume(resumeSchema)
		.handler(async (input: Input, ctx) => {
			switch (input.action) {
				case 'list':
					return await handleList(context, input);
				case 'get':
					return await handleGet(context, input);
				case 'run': {
					return await handleRun(context, input, ctx.resumeData, ctx.suspend);
				}
				case 'debug':
					return await handleDebug(context, input);
				case 'get-node-output':
					return await handleGetNodeOutput(context, input);
				case 'get-resolved-node-parameters':
					return await handleGetResolvedNodeParameters(context, input);
				case 'stop':
					return await handleStop(context, input);
			}
		})
		.build();
}
