import type { User } from '@n8n/db';
import {
	WEBHOOK_NODE_TYPE,
	SCHEDULE_TRIGGER_NODE_TYPE,
	FORM_TRIGGER_NODE_TYPE,
	CHAT_TRIGGER_NODE_TYPE,
	NodeHelpers,
	type INode,
	type INodeType,
	type INodeTypes,
} from 'n8n-workflow';

import type { CredentialsService } from '@/credentials/credentials.service';

import {
	hasHttpHeaderAuthDecryptedData,
	hasJwtPemKeyDecryptedData,
	hasJwtSecretDecryptedData,
} from '../mcp.typeguards';
import type { MCPTriggersMap } from '../mcp.types';
import { getExecuteWorkflowCallExample } from './workflow-inputs';

export type WebhookEndpoints = {
	webhook: string;
	webhookTest: string;
};

type WebhookCredentialRequirement =
	| { type: 'none' }
	| { type: 'basic' }
	| { type: 'header'; headerName: string }
	| { type: 'jwt'; variant: 'secret' | 'pem-key' };

type WebhookNodeDetails = {
	nodeName: string;
	productionUrl: string;
	testUrl: string;
	httpMethod: string;
	responseModeDescription: string;
	credentials: WebhookCredentialRequirement;
};

/**
 * `isFullPath` lives on the node type's webhook description, not on the node's
 * parameters. When it resolves to false, getNodeWebhookUrl prefixes the node's
 * webhookId, producing a URL that does not match the registered webhook.
 */
const resolveIsFullPath = (nodeTypes: INodeTypes, node: INode): boolean => {
	let nodeType: INodeType | undefined;
	try {
		nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
	} catch {
		return false;
	}
	const webhookDescription = nodeType?.description.webhooks?.find(
		(webhook) => webhook.restartWebhook !== true,
	);
	// The value can be an expression, but the Webhook node (the only type routed
	// here) defines it as a static boolean; treat anything else as false.
	return webhookDescription?.isFullPath === true;
};

// Normalizes endpoint segment (strips leading/trailing slashes) and joins with the node path.
const buildEndpointBaseUrl = (baseUrl: string, endpoint: string) => {
	const trimmedBase = baseUrl.replace(/\/+$/, '');
	const trimmedEndpoint = endpoint.replace(/^\/+|\/+$/g, '');
	return trimmedEndpoint ? `${trimmedBase}/${trimmedEndpoint}` : trimmedBase;
};

export const getTriggerDetails = async (
	user: User,
	supportedTriggers: INode[],
	unsupportedTriggers: INode[],
	baseUrl: string,
	credentialsService: CredentialsService,
	nodeTypes: INodeTypes,
	endpoints: WebhookEndpoints,
	workflowId: string,
	testBaseUrl: string = baseUrl,
): Promise<string> => {
	if (supportedTriggers.length === 0) {
		if (unsupportedTriggers.length > 0) {
			const names = unsupportedTriggers.map((trigger) => trigger.name).join(', ');
			return `This workflow's trigger(s) are not supported for direct execution through MCP: ${names}. Only Schedule, Webhook, Form, and Chat triggers can be executed directly through MCP, so this workflow cannot be executed through MCP. Its trigger(s) still run normally when the workflow is active.`;
		}
		return 'This workflow has no production triggers (Schedule, Webhook, Form, or Chat). It can only be executed in manual mode.';
	}

	// Organize triggers by their node type
	const triggersByType: MCPTriggersMap = {
		[SCHEDULE_TRIGGER_NODE_TYPE]: [],
		[WEBHOOK_NODE_TYPE]: [],
		[FORM_TRIGGER_NODE_TYPE]: [],
		[CHAT_TRIGGER_NODE_TYPE]: [],
	};

	// Group triggers by type
	for (const trigger of supportedTriggers) {
		if (trigger.type in triggersByType) {
			triggersByType[trigger.type as keyof MCPTriggersMap].push(trigger);
		}
	}

	// Build response based on trigger types present
	const responses: string[] = ['This workflow has the following trigger(s):\n'];

	// Handle webhook triggers
	if (triggersByType[WEBHOOK_NODE_TYPE].length > 0) {
		const webhookDetails = await getWebhookDetails(
			user,
			triggersByType[WEBHOOK_NODE_TYPE],
			baseUrl,
			credentialsService,
			nodeTypes,
			endpoints,
			workflowId,
			testBaseUrl,
		);
		responses.push(webhookDetails);
	}

	// Handle chat triggers
	if (triggersByType[CHAT_TRIGGER_NODE_TYPE].length > 0) {
		responses.push(getChatTriggerDetails(triggersByType[CHAT_TRIGGER_NODE_TYPE]));
	}

	// Handle schedule triggers
	if (triggersByType[SCHEDULE_TRIGGER_NODE_TYPE].length > 0) {
		responses.push(getScheduleTriggerDetails(triggersByType[SCHEDULE_TRIGGER_NODE_TYPE]));
	}

	// Handle form triggers
	if (triggersByType[FORM_TRIGGER_NODE_TYPE].length > 0) {
		responses.push(getFormTriggerDetails(triggersByType[FORM_TRIGGER_NODE_TYPE]));
	}

	return responses.join('\n\n');
};

const getScheduleTriggerDetails = (scheduleTriggers: INode[]): string => {
	const header = 'Schedule trigger(s):\n\n';
	const footer = `\n\nScheduled workflows do not take inputs. To execute a specific schedule trigger, pass triggerNodeName only: ${getExecuteWorkflowCallExample(SCHEDULE_TRIGGER_NODE_TYPE)}.`;
	const triggers = scheduleTriggers
		.map(
			(node, index) => `
				<trigger ${index + 1}>
				\t - Node name: ${node.name}
				</trigger ${index + 1}>`,
		)
		.join('\n\n');
	return header + triggers + footer;
};

const getFormTriggerDetails = (formTriggers: INode[]): string => {
	const header = 'Form trigger(s):\n\n';
	const footer = `\n\nTo execute a form trigger, pass triggerNodeName and inputs: ${getExecuteWorkflowCallExample(FORM_TRIGGER_NODE_TYPE)}.`;
	const triggers = formTriggers
		.map(
			(node, index) => `
				<trigger ${index + 1}>
				\t - Node name: ${node.name}
				\t - Form fields: ${JSON.stringify(node.parameters.formFields ?? 'N/A')}
				</trigger ${index + 1}>`,
		)
		.join('\n\n');
	return header + triggers + footer;
};

const getChatTriggerDetails = (chatTriggers: INode[]): string => {
	const header = 'Chat trigger(s):\n\n';
	const footer = `\n\nTo execute a chat trigger, pass triggerNodeName and inputs: ${getExecuteWorkflowCallExample(CHAT_TRIGGER_NODE_TYPE)}.`;
	const triggers = chatTriggers
		.map(
			(node, index) => `
				<trigger ${index + 1}>
				\t - Node name: ${node.name}
				</trigger ${index + 1}>`,
		)
		.join('\n\n');
	return header + triggers + footer;
};

/**
 * Creates a detailed textual description of the webhook triggers in a workflow, including URLs, methods, authentication, and response modes.
 * This helps MCP clients understand how craft a request to trigger the workflow.
 * @returns A detailed string description of the webhook triggers in the workflow.
 */
export const getWebhookDetails = async (
	user: User,
	webhookNodes: INode[],
	baseUrl: string,
	credentialsService: CredentialsService,
	nodeTypes: INodeTypes,
	endpoints: WebhookEndpoints,
	workflowId: string,
	testBaseUrl: string = baseUrl,
): Promise<string> => {
	const nodeDetails = await Promise.all(
		webhookNodes.map(
			async (node) =>
				await collectWebhookNodeDetails(
					user,
					node,
					baseUrl,
					credentialsService,
					nodeTypes,
					endpoints,
					workflowId,
					testBaseUrl,
				),
		),
	);

	return formatWebhookDetails(nodeDetails);
};

const collectWebhookNodeDetails = async (
	user: User,
	node: INode,
	baseUrl: string,
	credentialsService: CredentialsService,
	nodeTypes: INodeTypes,
	endpoints: WebhookEndpoints,
	workflowId: string,
	testBaseUrl: string = baseUrl,
): Promise<WebhookNodeDetails> => {
	const pathParam = typeof node.parameters.path === 'string' ? node.parameters.path : '';
	const isFullPath = resolveIsFullPath(nodeTypes, node);
	const httpMethod =
		typeof node.parameters.httpMethod === 'string' ? node.parameters.httpMethod : 'GET';

	return {
		nodeName: node.name,
		productionUrl: NodeHelpers.getNodeWebhookUrl(
			buildEndpointBaseUrl(baseUrl, endpoints.webhook),
			workflowId,
			node,
			pathParam,
			isFullPath,
		),
		testUrl: NodeHelpers.getNodeWebhookUrl(
			buildEndpointBaseUrl(testBaseUrl, endpoints.webhookTest),
			workflowId,
			node,
			pathParam,
			isFullPath,
		),
		httpMethod,
		responseModeDescription: getResponseModeDescription(node),
		credentials: await resolveCredentialRequirement(user, node, credentialsService),
	};
};

const formatWebhookDetails = (details: WebhookNodeDetails[]): string => {
	const header = 'Webhook trigger(s):\n\n';
	const triggers = details
		.map((detail, index) => formatTriggerDescription(detail, index))
		.join('\n\n');
	const footer = `\n\nTo execute a webhook trigger, pass triggerNodeName and inputs: ${getExecuteWorkflowCallExample(WEBHOOK_NODE_TYPE)}.`;
	return header + triggers + footer;
};

const formatTriggerDescription = (detail: WebhookNodeDetails, index: number): string => `
				<trigger ${index + 1}>
				\t - Node name: ${detail.nodeName}
				\t - Production URL: ${detail.productionUrl}
				\t - Test URL: ${detail.testUrl}
				\t - HTTP Method: ${detail.httpMethod}
				\t - Response Mode: ${detail.responseModeDescription}
				${formatCredentialRequirement(detail.credentials)}
				</trigger ${index + 1}>`;

const formatCredentialRequirement = (requirement: WebhookCredentialRequirement): string => {
	switch (requirement.type) {
		case 'basic':
			return '\t - Credentials: \n\t - This webhook requires basic authentication with a username and password that should be provided by the user.';
		case 'header':
			return `\t - Credentials: \n\t - This webhook requires a header with name "${requirement.headerName}" and a value that should be provided by the user.`;
		case 'jwt':
			if (requirement.variant === 'secret') {
				return '\t - Credentials: \n\t - This webhook requires a JWT secret that should be provided by the user.';
			}
			return '\t - Credentials: \n\t - This webhook requires JWT private and public keys that should be provided by the user.';
		default:
			return '\t - No credentials required for this webhook.';
	}
};

const resolveCredentialRequirement = async (
	user: User,
	node: INode,
	credentialsService: CredentialsService,
): Promise<WebhookCredentialRequirement> => {
	const authType =
		typeof node.parameters.authentication === 'string' ? node.parameters.authentication : undefined;

	switch (authType) {
		case 'basicAuth':
			return { type: 'basic' };
		case 'headerAuth': {
			const headerName = await getHeaderAuthName(user, node, credentialsService);
			if (headerName) {
				return { type: 'header', headerName };
			}
			break;
		}
		case 'jwtAuth': {
			const variant = await getJWTAuthVariant(user, node, credentialsService);
			if (variant) {
				return { type: 'jwt', variant };
			}
			break;
		}
	}

	return { type: 'none' };
};

const getHeaderAuthName = async (
	user: User,
	node: INode,
	credentialsService: CredentialsService,
): Promise<string | null> => {
	const id = node.credentials?.httpHeaderAuth?.id;
	if (!id) return null;

	const creds = await credentialsService.getOne(user, id, true);
	if (hasHttpHeaderAuthDecryptedData(creds)) {
		return creds.data.name;
	}
	return null;
};

const getJWTAuthVariant = async (
	user: User,
	node: INode,
	credentialsService: CredentialsService,
): Promise<'secret' | 'pem-key' | null> => {
	const id = node.credentials?.jwtAuth?.id;
	if (!id) return null;
	try {
		const creds = await credentialsService.getOne(user, id, true);
		if (hasJwtSecretDecryptedData(creds)) {
			return 'secret';
		} else if (hasJwtPemKeyDecryptedData(creds)) {
			return 'pem-key';
		}
	} catch {
		return null;
	}
	return null;
};

const getResponseModeDescription = (node: INode): string => {
	const responseMode =
		typeof node.parameters.responseMode === 'string' ? node.parameters.responseMode : undefined;

	if (responseMode === 'responseNode') {
		return 'Webhook is configured to respond using "Respond to Webhook" node.';
	}

	if (responseMode === 'lastNode') {
		const responseData =
			typeof node.parameters.responseData === 'string' ? node.parameters.responseData : undefined;
		const base = 'Webhook is configured to respond when the last node is executed. ';
		switch (responseData) {
			case 'allEntries':
				return base + 'Returns all the entries of the last node. Always returns an array.';
			case 'firstEntryBinary':
				return (
					base +
					'Returns the binary data of the first entry of the last node. Always returns a binary file.'
				);
			case 'noData':
				return base + 'Returns without a body.';
			default:
				return (
					base +
					'Returns the JSON data of the first entry of the last node. Always returns a JSON object.'
				);
		}
	}

	return 'Webhook is configured to respond immediately with the message "Workflow got started."';
};
