import { richMessageSchema } from '@n8n/api-types';
import { Service } from '@n8n/di';
import { isRecord } from '@n8n/utils/is-record';
import type { Adapter, SentMessage } from 'chat';
import { z } from 'zod';

import { type AgentChatIntegration, ChatIntegrationRegistry } from './agent-chat-integration';
import type { CallbackMetadata } from './callback-store';
import { ChatIntegrationService, type ChatInstance } from './chat-integration.service';
import {
	ComponentMapper,
	INTERACTIVE_CARD_RESUME_JSON_SCHEMA,
	type ShortenCallback,
} from './component-mapper';
import { INTEGRATION_ERROR_CODES } from './integration-error-codes';
import {
	connectionUnavailable,
	integrationError,
	normalizePlatformId,
	unsupportedAction,
} from './integration-helpers';
import type {
	IntegrationAction,
	IntegrationActionExecutor,
	IntegrationActionResult,
	IntegrationMessageContext,
	IntegrationToolConnectionDescriptor,
} from './integration-tools';

// The shared wire schema from @n8n/api-types — the same definition the tool
// boundary validates against and the editor-ui renderer parses with.
const messageSchema = richMessageSchema;

export const respondInputSchema = z.object({ message: messageSchema });
const sendDmInputSchema = z.object({
	userId: z.string().min(1),
	message: messageSchema,
});
const sendChannelMessageInputSchema = z.object({
	channelId: z.string().min(1),
	message: messageSchema,
});
const editMessageInputSchema = z
	.object({
		messageId: z.string().min(1),
		message: messageSchema,
	})
	.strict();
const addReactionInputSchema = z.object({
	emoji: z.string().min(1),
	threadId: z.string().min(1).optional(),
	messageId: z.string().min(1).optional(),
});

type MessagePayload = z.infer<typeof messageSchema>;

/**
 * Dispatches action invocations between cross-platform actions (`respond`,
 * `send_dm`, `send_channel_message`, `edit_message`, `add_reaction`) and
 * platform-specific actions (`create_issue`, `create_comment`, …) owned by
 * each {@link AgentChatIntegration} subclass.
 */
@Service()
export class ChatIntegrationActionExecutor implements IntegrationActionExecutor {
	private readonly componentMapper = new ComponentMapper();

	constructor(
		private readonly chatIntegrationService: ChatIntegrationService,
		private readonly integrationRegistry: ChatIntegrationRegistry,
	) {}

	async execute(params: {
		descriptor: IntegrationToolConnectionDescriptor;
		action: IntegrationAction;
		input: Record<string, unknown>;
		awaitResponse: boolean;
		runId?: string;
		toolCallId?: string;
		currentMessageContext?: IntegrationMessageContext;
	}): Promise<IntegrationActionResult> {
		if (!params.descriptor.agentId) return connectionUnavailable();

		if (params.action === 'do_not_respond') {
			return this.doNotRespond(params);
		}

		const unsupportedAction = () =>
			integrationError(
				INTEGRATION_ERROR_CODES.UNSUPPORTED_ACTION,
				`The ${params.descriptor.integration.type} integration does not support ${params.action}.`,
			);

		const integrationDef = this.integrationRegistry.get(params.descriptor.integration.type);
		if (integrationDef && !integrationDef.requiresChatInstance) {
			if (!integrationDef.executeAction) {
				return unsupportedAction();
			}
			try {
				const result = await integrationDef.executeAction({
					chat: undefined,
					descriptor: params.descriptor,
					action: params.action,
					input: params.input,
					currentMessageContext: params.currentMessageContext,
				});
				return result ?? unsupportedAction();
			} catch (error) {
				return integrationError(
					INTEGRATION_ERROR_CODES.ACTION_FAILED,
					error instanceof Error ? error.message : String(error),
				);
			}
		}

		const { credentialId } = params.descriptor.integration;
		if (!credentialId) return connectionUnavailable();

		let chat = this.chatIntegrationService.getChatInstance(params.descriptor.agentId, {
			type: params.descriptor.integration.type,
			credentialId,
		});
		chat ??= await this.chatIntegrationService.getChatInstanceForTools(
			params.descriptor.agentId,
			params.descriptor.integration,
		);
		if (!chat) return connectionUnavailable();

		try {
			if (params.action === 'respond') {
				return await this.respondInCurrentThread(chat, params);
			}
			if (params.action === 'send_dm') {
				return await this.sendDirectMessage(chat, params);
			}
			if (params.action === 'edit_message') {
				return await this.editMessageInCurrentThread(chat, params);
			}
			if (params.action === 'add_reaction') {
				return await this.addReactionToMessage(chat, params);
			}

			// Platform-specific actions delegate to the integration implementation.
			if (integrationDef?.executeAction) {
				const result = await integrationDef.executeAction({
					chat,
					descriptor: params.descriptor,
					action: params.action,
					input: params.input,
					currentMessageContext: params.currentMessageContext,
				});
				if (result !== undefined) return result;
			}

			if (params.action === 'send_channel_message') {
				return await this.sendChannelMessage(chat, params);
			}

			return unsupportedAction();
		} catch (error) {
			return integrationError(
				INTEGRATION_ERROR_CODES.ACTION_FAILED,
				error instanceof Error ? error.message : String(error),
			);
		}
	}

	/**
	 * End the turn without posting anything. Allowed only when the latest
	 * inbound message marked the reply as optional.
	 */
	private doNotRespond(params: ExecuteParams): IntegrationActionResult {
		if (params.currentMessageContext?.replyExpectation !== 'optional') {
			return integrationError(
				INTEGRATION_ERROR_CODES.REPLY_REQUIRED,
				'A reply is expected here — this is a direct message, a direct mention, or a conversation you were asked to join. Respond normally instead of staying silent.',
			);
		}

		return {
			ok: true,
			silent: true,
			note: 'No reply will be sent. End your turn now without writing any text.',
		};
	}

	private async addReactionToMessage(
		chat: ChatInstance,
		params: ExecuteParams,
	): Promise<IntegrationActionResult> {
		const adapter = chat.getAdapter(params.descriptor.integration.type);
		if (!supportsAddReaction(adapter)) {
			return unsupportedAction(params.descriptor.integration.type, 'add_reaction');
		}

		const input = addReactionInputSchema.parse(params.input);
		const currentMessageContext = params.currentMessageContext;
		const replyTargetForReaction =
			input.messageId !== undefined &&
			input.messageId === currentMessageContext?.replyMessageId &&
			(input.threadId === undefined ||
				input.threadId === currentMessageContext.replyTarget?.threadId)
				? currentMessageContext.replyTarget
				: undefined;
		const fallbackTarget = replyTargetForReaction ?? currentMessageContext?.target;
		const threadId = input.threadId ?? fallbackTarget?.threadId;
		const messageId = input.messageId ?? currentMessageContext?.messageId;
		if (!threadId || !messageId) {
			const platform = params.descriptor.integration.type;
			const displayLabel = `${platform.charAt(0).toUpperCase()}${platform.slice(1)}`;
			return integrationError(
				INTEGRATION_ERROR_CODES.NO_MESSAGE_CONTEXT,
				`${displayLabel} reactions require a messageId and threadId or current message context.`,
			);
		}

		await adapter.addReaction(threadId, messageId, input.emoji);

		return {
			ok: true,
			reaction: { emoji: input.emoji, threadId, messageId },
			messageContext: {
				integrationConnectionId: params.descriptor.integrationConnectionId,
				platform: params.descriptor.integration.type,
				target:
					replyTargetForReaction && currentMessageContext
						? currentMessageContext.target
						: buildReactionTarget(params.descriptor.integration.type, threadId, fallbackTarget),
				messageId:
					replyTargetForReaction && currentMessageContext
						? currentMessageContext.messageId
						: messageId,
				updatedAt: new Date().toISOString(),
			},
		};
	}

	private async respondInCurrentThread(
		chat: ChatInstance,
		params: ExecuteParams,
	): Promise<IntegrationActionResult> {
		const input = respondInputSchema.parse(params.input);
		const threadId = params.currentMessageContext?.target.threadId;
		if (!threadId) {
			return integrationError(
				INTEGRATION_ERROR_CODES.NO_MESSAGE_CONTEXT,
				'There is no current message context. Use an explicit send action.',
			);
		}

		const replyTargetThreadId = params.currentMessageContext?.replyTarget?.threadId;
		const isAutomaticReplyTarget =
			params.currentMessageContext?.replyExpectation !== undefined &&
			(replyTargetThreadId === undefined || replyTargetThreadId === threadId);
		if (!input.message.card && isAutomaticReplyTarget) {
			return integrationError(
				INTEGRATION_ERROR_CODES.ACTION_FAILED,
				'Plain text is already delivered to this conversation as your normal reply — write the text directly in your reply instead of calling respond. Call respond only with message.card, or use an explicit send action for a different target.',
			);
		}

		const thread = chat.thread(threadId);
		await this.prepareSentThread(params.descriptor, thread);
		const sent = await thread.post(await this.toPostable(params.descriptor, input.message, params));

		return {
			ok: true,
			messageContext: buildMessageContextFromSentMessage({
				descriptor: params.descriptor,
				sent,
				target: params.currentMessageContext!.target,
			}),
		};
	}

	private async sendDirectMessage(
		chat: ChatInstance,
		params: ExecuteParams,
	): Promise<IntegrationActionResult> {
		const input = sendDmInputSchema.parse(params.input);
		const thread = await chat.openDM(input.userId);
		const sent = await thread.post(await this.toPostable(params.descriptor, input.message, params));
		// Re-anchor at the sent message ts on platforms where a top-level DM
		// starts its own thread; otherwise keep the openDM thread.
		const anchoredId = this.sentThreadId(params.descriptor, sent);
		const threadId = anchoredId ?? thread.id;
		const targetThread = anchoredId && anchoredId !== thread.id ? chat.thread(threadId) : thread;
		await this.prepareSentThread(params.descriptor, targetThread);

		return {
			ok: true,
			messageContext: buildMessageContextFromSentMessage({
				descriptor: params.descriptor,
				sent,
				target: { type: 'dm', userId: input.userId, threadId },
			}),
		};
	}

	private async editMessageInCurrentThread(
		chat: ChatInstance,
		params: ExecuteParams,
	): Promise<IntegrationActionResult> {
		const input = editMessageInputSchema.parse(params.input);
		const currentMessageContext = params.currentMessageContext;
		const threadId = currentMessageContext?.target.threadId;
		if (!currentMessageContext || !threadId) {
			return integrationError(
				INTEGRATION_ERROR_CODES.NO_MESSAGE_CONTEXT,
				'There is no current conversation to edit. Send a message first, then try again.',
			);
		}

		const adapter = chat.getAdapter(params.descriptor.integration.type);
		if (!supportsMessageEditing(adapter)) {
			return integrationError(
				INTEGRATION_ERROR_CODES.UNSUPPORTED_ACTION,
				`The ${params.descriptor.integration.type} integration can't edit messages. Use a supported action instead.`,
			);
		}

		const edited = await adapter.editMessage(
			threadId,
			input.messageId,
			await this.toPostable(params.descriptor, input.message, params),
		);

		return {
			ok: true,
			messageContext: {
				...currentMessageContext,
				messageId: edited.id,
				updatedAt: new Date().toISOString(),
			},
		};
	}

	private async sendChannelMessage(
		chat: ChatInstance,
		params: ExecuteParams,
	): Promise<IntegrationActionResult> {
		const input = sendChannelMessageInputSchema.parse(params.input);
		const channelId = normalizePlatformId(params.descriptor.integration.type, input.channelId);
		const channel = chat.channel(channelId);
		const sent = await channel.post(
			await this.toPostable(params.descriptor, input.message, params),
		);
		const threadId = this.sentThreadId(params.descriptor, sent);
		if (threadId) {
			await this.prepareSentThread(params.descriptor, chat.thread(threadId));
		}

		return {
			ok: true,
			messageContext: buildMessageContextFromSentMessage({
				descriptor: params.descriptor,
				sent,
				target: { type: 'channel', channelId, threadId },
			}),
		};
	}

	private async toPostable(
		descriptor: IntegrationToolConnectionDescriptor,
		message: MessagePayload,
		params: { awaitResponse: boolean; runId?: string; toolCallId?: string },
	) {
		const cardPayload = message.card;
		if (!cardPayload) return message.text ?? '';

		if (params.awaitResponse && (!params.runId || !params.toolCallId)) {
			throw new Error('Interactive integration actions require runId and toolCallId.');
		}

		const card = await this.componentMapper.toCard(
			{
				title: cardPayload.title ?? message.text,
				message: cardPayload.message,
				components: cardPayload.components,
			},
			params.runId ?? '',
			params.toolCallId ?? '',
			INTERACTIVE_CARD_RESUME_JSON_SCHEMA,
			this.getShortenCallback(
				descriptor,
				params.runId && params.toolCallId
					? { groupId: JSON.stringify([params.runId, params.toolCallId]) }
					: undefined,
			),
			descriptor.integration.type,
		);

		return { card };
	}

	private getShortenCallback(
		descriptor: IntegrationToolConnectionDescriptor,
		metadata?: CallbackMetadata,
	): ShortenCallback | undefined {
		const { agentId, integration } = descriptor;
		if (!agentId) return undefined;
		const { credentialId } = integration;
		if (!credentialId) return undefined;
		return this.chatIntegrationService.getShortenCallback(
			agentId,
			{ type: integration.type, credentialId },
			metadata,
		);
	}

	private async prepareSentThread(
		descriptor: IntegrationToolConnectionDescriptor,
		thread: Parameters<NonNullable<AgentChatIntegration['prepareSentThread']>>[0],
	): Promise<void> {
		if (descriptor.integration.credentialId === undefined) return;
		await this.integrationRegistry
			.get(descriptor.integration.type)
			?.prepareSentThread?.(thread, descriptor.integration);
	}

	/**
	 * Thread id where follow-ups to an outbound sent message will arrive.
	 * Platforms where a top-level post starts its own thread (Slack) re-anchor
	 * the id at the sent message; others keep the posting thread. Returns
	 * undefined when the send produced no thread id (e.g. a non-threaded post).
	 */
	private sentThreadId(
		descriptor: IntegrationToolConnectionDescriptor,
		sent: SentMessage,
	): string | undefined {
		if (!sent.threadId) return undefined;
		const integration = this.integrationRegistry.get(descriptor.integration.type);
		return (
			integration?.messageThreadId?.({ id: sent.id, threadId: sent.threadId }) ?? sent.threadId
		);
	}
}

function supportsMessageEditing(adapter: unknown): adapter is Pick<Adapter, 'editMessage'> {
	return isRecord(adapter) && typeof adapter.editMessage === 'function';
}

function supportsAddReaction(adapter: unknown): adapter is Pick<Adapter, 'addReaction'> {
	return isRecord(adapter) && typeof adapter.addReaction === 'function';
}

interface ExecuteParams {
	descriptor: IntegrationToolConnectionDescriptor;
	action: IntegrationAction;
	input: Record<string, unknown>;
	awaitResponse: boolean;
	runId?: string;
	toolCallId?: string;
	currentMessageContext?: IntegrationMessageContext;
}

function buildMessageContextFromSentMessage(params: {
	descriptor: IntegrationToolConnectionDescriptor;
	sent: SentMessage;
	target: IntegrationMessageContext['target'];
}): IntegrationMessageContext {
	return {
		integrationConnectionId: params.descriptor.integrationConnectionId,
		platform: params.descriptor.integration.type,
		target: params.target,
		messageId: params.sent.id,
		updatedAt: new Date().toISOString(),
	};
}

function buildReactionTarget(
	platform: string,
	threadId: string,
	fallbackTarget: IntegrationMessageContext['target'] | undefined,
): IntegrationMessageContext['target'] {
	if (fallbackTarget?.threadId === threadId) return fallbackTarget;

	const [threadPlatform, channel] = threadId.split(':');
	const channelId =
		platform === 'slack' && threadPlatform === 'slack' && channel
			? `${threadPlatform}:${channel}`
			: undefined;
	return { type: 'thread', threadId, ...(channelId ? { channelId } : {}) };
}
