import { AgentCredentialIntegrationConfig } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import type { Thread, Author } from 'chat';
import { createHmac } from 'crypto';
import { InstanceSettings } from 'n8n-core';
import { UnexpectedError } from 'n8n-workflow';

import { ConflictError } from '@/errors/response-errors/conflict.error';
import { UrlService } from '@/services/url.service';

import { AgentRepository } from '../../repositories/agent.repository';
import { AgentChatIntegration, type AgentChatIntegrationContext } from '../agent-chat-integration';
import type { SuspendComponent } from '../component-mapper';
import { loadTelegramAdapter } from '../esm-loader';
import type { IntegrationAction } from '../integration-tools';

/**
 * Telegram platform integration.
 *
 * Two capability flags are enabled here because of Telegram constraints:
 * - {@link needsShortCallbackData} — `callback_data` is capped at 64 bytes, so the
 *   bridge stores full payloads in a CallbackStore and emits short 8-char keys as
 *   button IDs.
 * - {@link disableStreaming} — streaming Markdown edits are unstable: intermediate
 *   frames carry half-formed markup that Telegram rejects or renders inconsistently.
 *   The bridge buffers agent output and posts it as a single message per flush,
 *   guaranteeing well-formed Markdown on every post.
 *
 * The adapter runs in webhook mode when a public `WEBHOOK_URL` is configured,
 * otherwise it falls back to polling for local dev.
 */
@Service()
export class TelegramIntegration extends AgentChatIntegration {
	readonly type = 'telegram';

	readonly credentialTypes = ['telegramApi'];

	readonly displayLabel = 'Telegram';

	readonly displayIcon = 'telegram';

	readonly builderGuidance = {
		capabilities: [
			'Receive Telegram messages as agent triggers.',
			'Respond in Telegram conversations and send direct Telegram messages.',
			'Render Telegram-compatible rich interaction cards with buttons.',
		],
		useIntegrationWhen: [
			'The agent should be chatted with from Telegram or act as a Telegram bot.',
			'The agent needs to reply to Telegram users in the same conversation context.',
			'The agent should send Telegram messages as the connected Telegram bot.',
		],
		useNodeToolWhen: [
			'Telegram is only a backend API step and the agent does not need to be connected as a Telegram chat surface.',
			'The request is a one-off Telegram operation from another trigger without ongoing Telegram conversation context.',
		],
	};

	readonly supportedComponents = ['section', 'button', 'divider', 'fields'];

	readonly actions: IntegrationAction[] = ['respond', 'send_dm'];

	readonly needsShortCallbackData = true;

	readonly disableStreaming = true;

	readonly formatThreadId = {
		fromSdk: (thread: Thread<unknown, unknown>) => {
			const adapter = thread.adapter;
			const botUserId = adapter.botUserId;
			if (!botUserId) {
				throw new Error('Telegram bot user ID is not set');
			}
			// thread.id is simply user id, which means connecting agent to another bot user will result in the same thread id
			return `chat:${botUserId}-${thread.id}`;
		},
		toSdk: (threadId: string) => {
			if (!threadId.includes('-')) {
				return threadId;
			}
			return threadId.split('-').slice(1).join('-');
		},
	};

	constructor(
		private readonly logger: Logger,
		private readonly urlService: UrlService,
		private readonly agentRepository: AgentRepository,
		private readonly instanceSettings: InstanceSettings,
	) {
		super();
	}

	async createAdapter(ctx: AgentChatIntegrationContext): Promise<unknown> {
		const botToken = this.extractBotToken(ctx.credential);
		const mode = this.getMode();
		const secretToken = this.deriveSecretToken(ctx.agentId, ctx.credentialId);
		const { createTelegramAdapter } = await loadTelegramAdapter();
		return createTelegramAdapter({ botToken, mode, secretToken });
	}

	/**
	 * In polling mode the Chat SDK adapter long-polls Telegram, which must be
	 * done by exactly one main — otherwise multiple instances race for the same
	 * updates. Webhook mode is safe on every main.
	 */
	override requiresLeader(): boolean {
		return this.getMode() === 'polling';
	}

	/**
	 * Block the connect flow if this Telegram credential is already claimed by
	 * another agent in our DB. We deliberately don't probe Telegram for an
	 * existing webhook here — `onAfterConnect` overwrites whatever URL Telegram
	 * has on file, so a stale webhook from elsewhere isn't a connect blocker.
	 */
	async onBeforeConnect(ctx: AgentChatIntegrationContext): Promise<void> {
		const others = await this.agentRepository.findByIntegrationCredential(
			this.type,
			ctx.credentialId,
			ctx.projectId,
			ctx.agentId,
		);
		if (others.length > 0) {
			throw new ConflictError(
				`Telegram credential is already connected to agent "${others[0].name}"`,
			);
		}
	}

	async onAfterConnect(ctx: AgentChatIntegrationContext): Promise<void> {
		if (this.getMode() !== 'webhook') return;
		const webhookUrl = ctx.webhookUrlFor('telegram');
		const secretToken = this.deriveSecretToken(ctx.agentId, ctx.credentialId);
		const resp = await fetch(this.botApiUrl(ctx.credential, 'setWebhook'), {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ url: webhookUrl, secret_token: secretToken }),
		});
		if (!resp.ok) {
			throw new Error(`Failed to register Telegram webhook: ${await resp.text()}`);
		}
		this.logger.info(`[TelegramIntegration] Webhook registered: ${webhookUrl}`);
	}

	/**
	 * Mirror of `onAfterConnect`: clear the webhook we registered on Telegram so
	 * the bot is free for another agent or another application. Polling-mode
	 * connections never registered a webhook with Telegram, so skip.
	 */
	async onBeforeDisconnect(ctx: AgentChatIntegrationContext): Promise<void> {
		if (this.getMode() !== 'webhook') return;
		const resp = await fetch(this.botApiUrl(ctx.credential, 'deleteWebhook'), {
			method: 'POST',
		});
		if (!resp.ok) {
			throw new Error(`Failed to deregister Telegram webhook: ${await resp.text()}`);
		}
		this.logger.info(
			`[TelegramIntegration] Webhook deregistered for agent ${ctx.agentId}, credential ${ctx.credentialId}`,
		);
	}

	/**
	 * Enforce the Private-mode allowlist. Public mode (or legacy connections
	 * without saved settings) accepts every Telegram user; Private mode only
	 * accepts senders whose numeric user ID or username appears in `allowedUsers`.
	 * Stored values may carry a leading "@" (saved verbatim from user input), so
	 * they are normalized by stripping "@" before comparison. The SDK delivers
	 * both userId and userName without "@".
	 */
	isUserAllowed(
		author: Author,
		integration: AgentCredentialIntegrationConfig | undefined,
	): boolean {
		if (!integration) return true;
		if (integration?.type !== 'telegram') {
			throw new UnexpectedError(
				`TelegramIntegration received settings with type "${integration?.type}"`,
			);
		}
		if (!integration.settings) return true;

		if (integration.settings.accessMode === 'public') return true;
		return integration.settings.allowedUsers.some((allowed) => {
			const normalized = allowed.startsWith('@') ? allowed.slice(1) : allowed;
			return normalized === author.userId || normalized === author.userName;
		});
	}

	normalizeComponents(components: SuspendComponent[]): SuspendComponent[] {
		const normalized: SuspendComponent[] = [];
		for (const c of components) {
			switch (c.type) {
				case 'select':
				case 'radio_select':
					// Convert select options to individual buttons
					for (const opt of c.options ?? []) {
						normalized.push({ type: 'button', label: opt.label, value: opt.value });
					}
					break;
				case 'image':
					// Convert image to a section with a markdown link
					if (c.url) {
						normalized.push({ type: 'section', text: `[${c.altText ?? 'Image'}](${c.url})` });
					}
					break;
				default:
					normalized.push(c);
			}
		}
		return normalized;
	}

	/** Webhook when we have a public URL, polling otherwise (local dev). */
	private getMode(): 'webhook' | 'polling' {
		const baseUrl = this.urlService.getWebhookBaseUrl();
		const isPublic = baseUrl.startsWith('https://') && !baseUrl.includes('localhost');
		return isPublic ? 'webhook' : 'polling';
	}

	/**
	 * Per-integration webhook secret derived deterministically from the cluster
	 * encryption key. In multi-main mode the webhook is registered exactly once by the
	 * leader, but every main must verify incoming POSTs. HMAC over an
	 * `(agentId, credentialId)` lets every main reach the identical secret with
	 * zero coordination.
	 */
	private deriveSecretToken(agentId: string, credentialId: string): string {
		return createHmac('sha256', this.instanceSettings.encryptionKey)
			.update(`telegram:${agentId}:${credentialId}`)
			.digest('hex');
	}

	private extractBotToken(credential: Record<string, unknown>): string {
		const token = credential.accessToken;
		if (typeof token === 'string' && token) {
			return token;
		}
		throw new Error(
			'Could not extract a bot token from the Telegram credential. ' +
				'Please ensure the credential has a valid access token from BotFather.',
		);
	}

	/**
	 * Build a Bot API URL honoring the credential's `baseUrl` (defaults to
	 * `https://api.telegram.org`). Lets users on a self-hosted Bot API server
	 * register/deregister webhooks against their own endpoint.
	 */
	private botApiUrl(credential: Record<string, unknown>, method: string): string {
		const botToken = this.extractBotToken(credential);
		const raw = credential.baseUrl;
		const baseUrl =
			typeof raw === 'string' && raw.trim()
				? raw.trim().replace(/\/+$/, '')
				: 'https://api.telegram.org';
		return `${baseUrl}/bot${botToken}/${method}`;
	}
}
