import { TOOL_EXECUTOR_NODE_NAME } from '@n8n/constants';
import type { IExecutionResponse } from '@n8n/db';
import type { ChatNodeMessage, IExecuteData, INode, Workflow } from 'n8n-workflow';
import {
	CHAT_WAIT_USER_REPLY,
	CHAT_NODE_TYPE,
	RESPOND_TO_WEBHOOK_NODE_TYPE,
	SEND_AND_WAIT_OPERATION,
	CHAT_TOOL_NODE_TYPE,
} from 'n8n-workflow';

const AI_TOOL = 'ai_tool';

/**
 * When the parked node is the virtual PartialExecutionToolExecutor, resolves the
 * real tool node it wraps (stored in the executor's `node` parameter). Pure — does
 * not mutate the execution. Returns null when the parked node isn't the executor.
 *
 * Keyed off `executionData.node` (the parked stack entry) rather than
 * `resultData.lastNodeExecuted` so it matches the node the engine resumes.
 */
function getToolExecutorTargetNode(executionData: IExecuteData, workflow: Workflow): INode | null {
	if (executionData.node.name !== TOOL_EXECUTOR_NODE_NAME) return null;

	const toolNodeName = executionData.node.parameters?.node as string | undefined;
	return toolNodeName ? workflow.getNode(toolNodeName) : null;
}

/**
 * Resolves the node a chat message would resume — the parked node, or the tool node
 * behind the virtual PartialExecutionToolExecutor. Pure: does not mutate the
 * execution, so callers deciding *whether* to resume can share it with the resume
 * path without side effects. Returns null when no resumable node can be resolved.
 *
 * Resolves from `executionData.node` — the top of `nodeExecutionStack`, i.e. the
 * node the engine actually resumes — not `resultData.lastNodeExecuted` (a report of
 * what last ran). The two agree in every real waiting state, but reading the
 * authoritative field keeps the resume decision from ever authorizing a different
 * node than the one that runs.
 */
export function findResumeNode(executionData: IExecuteData, workflow: Workflow): INode | null {
	const parkedNode = executionData.node;

	if (parkedNode.name === TOOL_EXECUTOR_NODE_NAME) {
		return getToolExecutorTargetNode(executionData, workflow);
	}

	return workflow.getNode(parkedNode.name);
}

/**
 * Redirects execution from PartialExecutionToolExecutor to the actual tool node
 * it wraps (stored in the executor's `node` parameter).
 *
 * - Clears runNodeFilter/destinationNode so downstream nodes (e.g. AI Agent)
 *   are not blocked by the original partial-execution constraints.
 * - Forces runIndex=0 to merge into the preserveInputOverride placeholder,
 *   keeping inputOverride ($fromAI params) visible on the resumed run.
 *
 * Returns the resolved tool node, or null if not applicable.
 */
export function redirectIfToolExecutor(
	execution: IExecutionResponse,
	executionData: IExecuteData,
	workflow: Workflow,
): INode | null {
	const toolNode = getToolExecutorTargetNode(executionData, workflow);
	if (!toolNode) return null;

	executionData.node = toolNode;
	execution.data.resultData.lastNodeExecuted = toolNode.name;
	executionData.runIndex = 0;

	if (execution.data.startData) {
		execution.data.startData.runNodeFilter = undefined;
		execution.data.startData.destinationNode = undefined;
	}

	return toolNode;
}

/** Normalises originalDestinationNode to a node name (V0 = string, V1 = object). */
function getOriginalDestinationNodeName(
	startData: IExecutionResponse['data']['startData'],
): string | undefined {
	const dest = startData?.originalDestinationNode;
	if (!dest) return undefined;
	return typeof dest === 'string' ? dest : dest.nodeName;
}

/** Returns sendMessage from the most recent ai_tool run of a node, if present. */
function getSendMessageFromToolNode(
	nodeRuns: IExecutionResponse['data']['resultData']['runData'][string],
): ChatNodeMessage | undefined {
	const lastRun = nodeRuns[nodeRuns.length - 1];
	const aiToolBranches = lastRun?.data?.[AI_TOOL];

	if (!Array.isArray(aiToolBranches)) return undefined;

	for (const branch of aiToolBranches) {
		if (Array.isArray(branch) && branch[0]?.sendMessage) {
			return branch[0].sendMessage;
		}
	}

	return undefined;
}

/**
 * Finds sendMessage when PartialExecutionToolExecutor is the last executed node.
 * The message lives in the connected tool node's ai_tool run data, not in the
 * executor's own output. Targets originalDestinationNode first, then scans all
 * ai_tool outputs as a fallback.
 */
function getToolExecutorSendMessage(executionData: IExecutionResponse['data']) {
	const { startData, resultData } = executionData;
	const { runData } = resultData;

	const toolNodeName = getOriginalDestinationNodeName(startData);
	if (toolNodeName && runData[toolNodeName]) {
		return getSendMessageFromToolNode(runData[toolNodeName]);
	}

	// Fallback: scan all nodes with ai_tool output (only tool nodes produce it)
	for (const [nodeName, nodeRuns] of Object.entries(runData)) {
		if (nodeName === TOOL_EXECUTOR_NODE_NAME) continue;
		const message = getSendMessageFromToolNode(nodeRuns);
		if (message) return message;
	}

	return undefined;
}

/**
 * Returns the message to be sent of the last executed node
 */
export function getMessage(execution: IExecutionResponse) {
	const lastNodeExecuted = execution.data.resultData.lastNodeExecuted;
	if (typeof lastNodeExecuted !== 'string') return undefined;

	// PartialExecutionToolExecutor is a virtual node not saved in the workflow,
	// so its sendMessage must be retrieved from the connected tool node's run data.
	if (lastNodeExecuted === TOOL_EXECUTOR_NODE_NAME) {
		return getToolExecutorSendMessage(execution.data);
	}

	const runIndex = execution.data.resultData.runData[lastNodeExecuted].length - 1;
	const data = execution.data.resultData.runData[lastNodeExecuted][runIndex]?.data;
	const outputs = data?.main ?? data?.[AI_TOOL];

	// Check all main output branches for a message
	if (outputs && Array.isArray(outputs)) {
		for (const branch of outputs) {
			if (branch && Array.isArray(branch) && branch.length > 0 && branch[0].sendMessage) {
				return branch[0].sendMessage;
			}
		}
	}

	return undefined;
}

/**
 * Returns the last node executed
 */
export function getLastNodeExecuted(execution: IExecutionResponse) {
	const lastNodeExecuted = execution.data.resultData.lastNodeExecuted;
	if (typeof lastNodeExecuted !== 'string') return undefined;

	const node = execution.workflowData?.nodes?.find((node) => node.name === lastNodeExecuted);
	if (node) return node;

	// For the virtual PartialExecutionToolExecutor node (not saved in workflowData),
	// returns a synthetic node so session.nodeWaitingForChatResponse is set correctly
	// and the message is not re-sent on every poll interval.
	if (lastNodeExecuted === TOOL_EXECUTOR_NODE_NAME) {
		return {
			name: TOOL_EXECUTOR_NODE_NAME,
			type: '@n8n/n8n-nodes-langchain.toolExecutor',
			parameters: {},
			id: '',
			typeVersion: 1,
			position: [0, 0],
			disabled: false,
		} satisfies INode;
	}

	return undefined;
}

/**
 * Check if execution should be resumed immediately after receivng a message
 */
export function shouldResumeImmediately(lastNode: INode) {
	if (lastNode?.type === RESPOND_TO_WEBHOOK_NODE_TYPE) {
		return true;
	}

	if (lastNode?.parameters?.[CHAT_WAIT_USER_REPLY] === false) {
		return true;
	}

	const options = lastNode?.parameters?.options as {
		[CHAT_WAIT_USER_REPLY]?: boolean;
	};

	if (options && options[CHAT_WAIT_USER_REPLY] === false) {
		return true;
	}

	const operation = lastNode?.parameters?.operation;
	const isChatNode = lastNode?.type === CHAT_NODE_TYPE || lastNode?.type === CHAT_TOOL_NODE_TYPE;
	if (isChatNode && operation && operation !== SEND_AND_WAIT_OPERATION) {
		return true;
	}

	return false;
}

export function getLastNodeMessage(execution: IExecutionResponse, lastNode: INode) {
	if (lastNode.type !== CHAT_NODE_TYPE) return '';

	const message =
		execution.data?.resultData?.runData?.[lastNode.name]?.[0]?.data?.main?.[0]?.[0]?.sendMessage;

	return (message as string) ?? '';
}
