import {
	Agent,
	createObservationLogObserveFn,
	createObservationLogReflectFn,
	Memory,
} from '@n8n/agents';

import { applyAgentThinking } from './apply-agent-thinking';
import {
	addSafeMcpTools,
	createClaimedToolNames,
	type McpToolNameValidationError,
} from './mcp-tool-name-validation';
import { attachRuntimeWorkspaceCapabilities } from './runtime-workspace';
import { getSystemPrompt } from './system-prompt';
import { listConnectedMcpServices } from '../mcp/connected-mcp-services';
import { hasRuntimeSkills } from '../skills/runtime-skills';
import { createToolRegistry, mergeToolRegistries, toolRegistryValues } from '../tool-registry';
import { createAllTools, createOrchestratorDomainTools, createOrchestrationTools } from '../tools';
import { createToolsFromLocalMcpServer } from '../tools/filesystem/create-tools-from-mcp-server';
import { ALWAYS_LOADED_TOOL_NAMES, CHECKPOINT_FOLLOW_UP_TOOL_NAMES } from '../tools/tool-ids';
import { buildAgentTraceInputs, mergeTraceRunInputs } from '../tracing/langsmith-tracing';
import type {
	CreateInstanceAgentOptions,
	InstanceAiContext,
	InstanceAiToolRegistry,
	ModelConfig,
} from '../types';
import { withModalSession } from '../utils/modal-session';

function resolveModalSessionModelId(
	modelId: ModelConfig,
	context: InstanceAiContext,
	orchestrationThreadId: string | undefined,
): ModelConfig {
	const threadId = [context.threadId, orchestrationThreadId]
		.map((value) => value?.trim())
		.find((value): value is string => value !== undefined && value.length > 0);
	if (!threadId) return modelId;

	const stickyModelId = withModalSession(modelId, threadId);
	if (stickyModelId === modelId) return modelId;

	context.modelId = stickyModelId;
	return stickyModelId;
}

// ── Agent factory ───────────────────────────────────────────────────────────

function splitDeferredTools(
	tools: InstanceAiToolRegistry,
	options: { isCheckpointFollowUp?: boolean } = {},
) {
	const coreTools = createToolRegistry();
	const deferredTools = createToolRegistry();

	for (const [name, tool] of tools) {
		if (
			ALWAYS_LOADED_TOOL_NAMES.has(name) ||
			(options.isCheckpointFollowUp && CHECKPOINT_FOLLOW_UP_TOOL_NAMES.has(name))
		) {
			coreTools.set(name, tool);
		} else {
			deferredTools.set(name, tool);
		}
	}

	return { coreTools, deferredTools };
}

export async function createInstanceAgent(
	options: CreateInstanceAgentOptions,
): Promise<{ agent: Agent; mcpConnectionFailures: Array<{ server: string; error: string }> }> {
	const {
		modelId: rawModelId,
		context,
		orchestrationContext,
		mcpServers = [],
		mcpManager,
		memoryConfig,
	} = options;

	const modelId = resolveModalSessionModelId(rawModelId, context, orchestrationContext?.threadId);
	if (orchestrationContext && orchestrationContext.modelId !== modelId) {
		orchestrationContext.modelId = modelId;
	}

	// Build native n8n domain tools (context captured via closures — per-run).
	// Thread the trace handle in so domain tools (e.g. build-workflow) can emit
	// explicit child runs that land on the active trace — orchestration tools
	// (e.g. verify) already get it via OrchestrationContext.
	const domainContext: InstanceAiContext = { ...context, tracing: orchestrationContext?.tracing };
	const domainTools = createAllTools(domainContext);

	// Load MCP tools (cached by config hash inside the manager — only spawns
	// processes / opens connections on first call or config change). The manager
	// returns per-server connection failures alongside the tools so they travel
	// with this call (not shared mutable state) — concurrent runs with different
	// configs can't read each other's failures.
	const requireMcpToolApproval = context.permissions?.executeMcpTool !== 'always_allow';
	const { tools: mcpTools, connectionFailures: managerMcpFailures } =
		await mcpManager.getRegularTools(mcpServers, context.logger, requireMcpToolApproval);
	// Map manager-reported connection failures to the generic SDK event type so
	// the runtime can inject a model-facing note into the orchestrator's system
	// message. The adapter owns the n8n-specific server config → plain SDK event
	// translation; the SDK runtime does the rest.
	const mcpConnectionFailures = managerMcpFailures.map((f) => ({
		server: f.server.name,
		error: f.error,
	}));
	const browserCredentialSetup = context.browserCredentialSetup;
	const rawLocalMcpTools = context.localMcpServer
		? createToolsFromLocalMcpServer({
				server: context.localMcpServer,
				logger: context.logger,
				onCredentialCreateResult: browserCredentialSetup
					? (credentialType, outcome) => {
							if (outcome.ok) browserCredentialSetup.markCreated(credentialType);
							else browserCredentialSetup.markCreateFailed(credentialType, outcome.errorCode);
						}
					: undefined,
			})
		: createToolRegistry();

	const browserToolNames = new Set(
		context.localMcpServer?.getToolsByCategory('browser').map((tool) => tool.name) ?? [],
	);

	const warnSkippedMcpTool = (error: McpToolNameValidationError) => {
		context.logger.warn('Skipped MCP tool with unsafe name', {
			toolName: error.toolName,
			source: error.source,
			reason: error.message,
		});
	};

	// Build orchestration tools — orchestrator-only.
	const orchestrationTools = orchestrationContext
		? createOrchestrationTools(orchestrationContext)
		: createToolRegistry();

	// Keep MCP tools from shadowing domain or orchestration tools.
	const reservedToolNames = new Set([...domainTools.keys(), ...orchestrationTools.keys()]);

	// Store all MCP tools on orchestrationContext for sub-agents.
	const allMcpTools = createToolRegistry();
	const mcpContextToolNames = createClaimedToolNames(reservedToolNames);
	addSafeMcpTools(allMcpTools, rawLocalMcpTools, {
		source: 'local gateway MCP',
		claimedToolNames: mcpContextToolNames,
		warn: warnSkippedMcpTool,
	});
	addSafeMcpTools(allMcpTools, mcpTools, {
		source: 'external MCP',
		claimedToolNames: mcpContextToolNames,
		warn: warnSkippedMcpTool,
	});
	if (orchestrationContext && allMcpTools.size > 0) {
		orchestrationContext.mcpTools = allMcpTools;
	}

	const claimedOrchestratorToolNames = createClaimedToolNames(reservedToolNames);
	const safeLocalMcpTools = createToolRegistry();
	addSafeMcpTools(safeLocalMcpTools, rawLocalMcpTools, {
		source: 'local gateway MCP',
		claimedToolNames: claimedOrchestratorToolNames,
		warn: warnSkippedMcpTool,
	});
	const safeMcpTools = createToolRegistry();
	addSafeMcpTools(safeMcpTools, mcpTools, {
		source: 'external MCP',
		claimedToolNames: claimedOrchestratorToolNames,
		warn: warnSkippedMcpTool,
	});

	const orchestratorDomainTools = createOrchestratorDomainTools({
		...domainContext,
		connectedMcpServices: listConnectedMcpServices(mcpServers, safeMcpTools),
	});

	const allOrchestratorTools = mergeToolRegistries(
		orchestratorDomainTools,
		orchestrationTools,
		safeLocalMcpTools,
		safeMcpTools,
	);
	const tracedOrchestratorTools =
		orchestrationContext?.tracing?.wrapTools(allOrchestratorTools, {
			agentRole: 'orchestrator',
			tags: ['orchestrator'],
		}) ?? allOrchestratorTools;
	const { coreTools, deferredTools } = splitDeferredTools(tracedOrchestratorTools, {
		isCheckpointFollowUp: orchestrationContext?.isCheckpointFollowUp,
	});
	const hasDeferrableTools = !options.disableDeferredTools && deferredTools.size > 0;
	const hasDeferredExternalMcpTools =
		hasDeferrableTools && Array.from(safeMcpTools.keys()).some((name) => deferredTools.has(name));
	const runtimeTools = hasDeferrableTools ? coreTools : tracedOrchestratorTools;
	const systemPrompt = getSystemPrompt({
		webhookBaseUrl: orchestrationContext?.webhookBaseUrl,
		formBaseUrl: orchestrationContext?.formBaseUrl,
		localGateway: context.localGatewayStatus,
		toolSearchEnabled: hasDeferrableTools,
		mcpToolSearchEnabled: hasDeferredExternalMcpTools,
		licenseHints: context.licenseHints,
		browserAvailable: browserToolNames.size > 0,
		branchReadOnly: context.branchReadOnly,
		projectId: context.projectId,
		workspaceRoot:
			orchestrationContext?.workspace && orchestrationContext.workspaceRoot
				? orchestrationContext.workspaceRoot
				: undefined,
	});

	const telemetry = orchestrationContext?.tracing?.getTelemetry?.({
		agentRole: 'orchestrator',
		functionId: 'instance-ai.orchestrator',
		executionMode: 'foreground',
	});
	const agent = new Agent('n8n-instance-agent')
		.model(modelId)
		.instructions(systemPrompt, {
			providerOptions: {
				anthropic: { cacheControl: { type: 'ephemeral' } },
			},
		})
		.tool(toolRegistryValues(runtimeTools))
		.checkpoint(options.checkpointStore ?? 'memory');
	if (mcpConnectionFailures.length > 0) {
		agent.mcpConnectionFailures(mcpConnectionFailures);
	}
	if (options.thinkingEnabled !== false) {
		applyAgentThinking(agent, modelId);
	}
	if (hasDeferrableTools) {
		agent.deferredTool(toolRegistryValues(deferredTools), { search: { topK: 5 } });
	}
	const runtimeSkills = orchestrationContext?.runtimeSkills;
	if (hasRuntimeSkills(runtimeSkills)) {
		agent.skills(runtimeSkills);
	}
	if (telemetry) {
		agent.telemetry(telemetry);
	}
	attachRuntimeWorkspaceCapabilities(agent, {
		workspace: orchestrationContext?.workspace,
		runtimeSkills: orchestrationContext?.runtimeSkills,
	});

	if (options.memory) {
		const mem = new Memory().storage(options.memory);

		if (memoryConfig.observationalMemory) {
			const { observerThresholdTokens, reflectorThresholdTokens, onTaskUsage } =
				memoryConfig.observationalMemory;
			mem.observationalMemory({
				observerThresholdTokens,
				reflectorThresholdTokens,
				...(onTaskUsage
					? {
							observe: createObservationLogObserveFn(modelId, { onUsage: onTaskUsage }),
							reflect: createObservationLogReflectFn(modelId, { onUsage: onTaskUsage }),
						}
					: {}),
			});
		}

		agent.memory(mem);
	}
	if (options.onMemoryTaskEvent) {
		agent.memoryTaskObserver(options.onMemoryTaskEvent);
	}
	mergeTraceRunInputs(
		orchestrationContext?.tracing?.actorRun,
		buildAgentTraceInputs({
			systemPrompt,
			tools: runtimeTools,
			deferredTools: hasDeferrableTools ? deferredTools : undefined,
			modelId,
			memory: options.memory
				? {
						...(memoryConfig.observationalMemory
							? {
									observationalMemory: {
										enabled: true,
										observerThresholdTokens:
											memoryConfig.observationalMemory.observerThresholdTokens,
										reflectorThresholdTokens:
											memoryConfig.observationalMemory.reflectorThresholdTokens,
									},
								}
							: {}),
					}
				: undefined,
			toolSearchEnabled: hasDeferrableTools,
			inputProcessors: hasDeferrableTools ? ['NativeToolSearch'] : undefined,
			runtimeSkills: runtimeSkills?.registry,
		}),
	);

	return { agent, mcpConnectionFailures };
}
