/**
 * n8n Test Containers Setup
 * This file provides a complete n8n container stack for testing with support for:
 * - Single instances (SQLite or PostgreSQL)
 * - Queue mode with Redis
 * - Multi-main instances with load balancing
 * - Task runner containers for external code execution
 * - Parallel execution (multiple stacks running simultaneously)
 *
 * Key features for parallel execution:
 * - Dynamic port allocation to avoid conflicts (handled by testcontainers or get-port)
 */

import getPort from 'get-port';
import assert from 'node:assert';
import type { StartedNetwork, StartedTestContainer } from 'testcontainers';
import { GenericContainer, Network, Wait } from 'testcontainers';

import { getDockerImageFromEnv } from './docker-image';
import { DockerImageNotFoundError } from './docker-image-not-found-error';
import { N8nImagePullPolicy } from './n8n-image-pull-policy';
import {
	setupPostgres,
	setupRedis,
	setupCaddyLoadBalancer,
	pollContainerHttpEndpoint,
	setupProxyServer,
	setupTaskRunner,
} from './n8n-test-container-dependencies';
import { setupGitea } from './n8n-test-container-gitea';
import {
	setupKeycloak,
	getKeycloakN8nEnvironment,
	waitForKeycloakFromContainer,
	N8N_KEYCLOAK_CERT_PATH,
} from './n8n-test-container-keycloak';
import { setupMailpit, getMailpitEnvironment } from './n8n-test-container-mailpit';
import {
	setupObservabilityStack,
	type ObservabilityStack,
	type ScrapeTarget,
} from './n8n-test-container-observability';
import { setupTracingStack, type TracingStack } from './n8n-test-container-tracing';
import { createElapsedLogger, createSilentLogConsumer } from './n8n-test-container-utils';
import { waitForNetworkQuiet } from './network-stabilization';
import { TEST_CONTAINER_IMAGES } from './test-containers';

// --- Constants ---

const POSTGRES_IMAGE = TEST_CONTAINER_IMAGES.postgres;
const REDIS_IMAGE = TEST_CONTAINER_IMAGES.redis;
const CADDY_IMAGE = TEST_CONTAINER_IMAGES.caddy;
const N8N_E2E_IMAGE = TEST_CONTAINER_IMAGES.n8n;
const MOCKSERVER_IMAGE = TEST_CONTAINER_IMAGES.mockserver;
const GITEA_IMAGE = TEST_CONTAINER_IMAGES.gitea;

// Default n8n image (can be overridden via N8N_DOCKER_IMAGE env var)
const N8N_IMAGE = getDockerImageFromEnv(N8N_E2E_IMAGE);

// Base environment for all n8n instances
const BASE_ENV: Record<string, string> = {
	N8N_LOG_LEVEL: 'debug',
	N8N_ENCRYPTION_KEY: process.env.N8N_ENCRYPTION_KEY ?? 'test-encryption-key',
	E2E_TESTS: 'false',
	QUEUE_HEALTH_CHECK_ACTIVE: 'true',
	N8N_DIAGNOSTICS_ENABLED: 'false',
	N8N_METRICS: 'true',
	NODE_ENV: 'development',
	N8N_LICENSE_TENANT_ID: process.env.N8N_LICENSE_TENANT_ID ?? '1001',
	N8N_LICENSE_ACTIVATION_KEY: process.env.N8N_LICENSE_ACTIVATION_KEY ?? '',
	N8N_LICENSE_CERT: process.env.N8N_LICENSE_CERT ?? '',
	N8N_DYNAMIC_BANNERS_ENABLED: 'false',
};

// Wait strategy for n8n main containers
const N8N_MAIN_WAIT_STRATEGY = Wait.forAll([
	Wait.forListeningPorts(),
	Wait.forHttp('/healthz/readiness', 5678).forStatusCode(200).withStartupTimeout(30000),
	Wait.forLogMessage('Editor is now accessible via').withStartupTimeout(30000),
]);

// Wait strategy for n8n worker containers
const N8N_WORKER_WAIT_STRATEGY = Wait.forAll([
	Wait.forListeningPorts(),
	Wait.forLogMessage('n8n worker is now ready').withStartupTimeout(30000),
]);

// --- Interfaces ---

export interface N8NConfig {
	postgres?: boolean;
	queueMode?:
		| boolean
		| {
				mains?: number;
				workers?: number;
		  };
	env?: Record<string, string>;
	projectName?: string;
	resourceQuota?: {
		memory?: number; // in GB
		cpu?: number; // in cores
	};
	proxyServerEnabled?: boolean;
	sourceControl?: boolean;
	taskRunner?: boolean;
	email?: boolean;
	/** Enable OIDC testing with Keycloak. Requires postgres: true for SSO support. */
	oidc?: boolean;
	/** Enable VictoriaObs stack for test observability (VictoriaLogs + VictoriaMetrics) */
	observability?: boolean;
	/** Enable tracing stack for workflow execution visualization (n8n-tracer + Jaeger) */
	tracing?: boolean;
}

export interface N8NStack {
	baseUrl: string;
	stop: () => Promise<void>;
	containers: StartedTestContainer[];
	/** OIDC configuration when oidc is enabled */
	oidc?: {
		/** Discovery URL for OIDC configuration (accessible from host/browser) */
		discoveryUrl: string;
		/** Internal discovery URL for n8n container to access Keycloak via Docker network */
		internalDiscoveryUrl: string;
	};
	/** Observability stack when observability is enabled */
	observability?: ObservabilityStack;
	/** Tracing stack when tracing is enabled */
	tracing?: TracingStack;
}

/**
 * Create an n8n container stack
 *
 * @example
 * // Simple SQLite instance
 * const stack = await createN8NStack();
 *
 * @example
 * // PostgreSQL without queue mode
 * const stack = await createN8NStack({ postgres: true });
 *
 * @example
 * // Queue mode (automatically uses PostgreSQL)
 * const stack = await createN8NStack({ queueMode: true });
 *
 * @example
 * // Custom scaling (uses load balancer for multiple mains)
 * const stack = await createN8NStack({
 * queueMode: { mains: 3, workers: 5 },
 * env: { N8N_ENABLED_MODULES: 'insights' }
 * });
 */
export async function createN8NStack(config: N8NConfig = {}): Promise<N8NStack> {
	const log = createElapsedLogger('n8n-stack');

	const {
		postgres = false,
		queueMode = false,
		env = {},
		proxyServerEnabled = false,
		projectName,
		resourceQuota,
		taskRunner,
		sourceControl = false,
		email = false,
		oidc = false,
		observability = false,
		tracing = false,
	} = config;
	const queueConfig = normalizeQueueConfig(queueMode);
	// Task runner is enabled by default for all containerized stacks (mirrors production default)
	const taskRunnerEnabled = taskRunner ?? true;
	const sourceControlEnabled = !!sourceControl;
	const emailEnabled = !!email;
	const oidcEnabled = !!oidc;
	const observabilityEnabled = !!observability;
	const tracingEnabled = !!tracing;
	// OIDC requires PostgreSQL for SSO support
	const usePostgres = postgres || !!queueConfig || oidcEnabled;
	const uniqueProjectName = projectName ?? `n8n-stack-${Math.random().toString(36).substring(7)}`;
	const containers: StartedTestContainer[] = [];

	log(`Starting stack creation: ${uniqueProjectName} (queueMode: ${JSON.stringify(queueConfig)})`);

	const mainCount = queueConfig?.mains ?? 1;
	const needsLoadBalancer = mainCount > 1;
	const needsNetwork =
		usePostgres ||
		!!queueConfig ||
		needsLoadBalancer ||
		proxyServerEnabled ||
		taskRunnerEnabled ||
		sourceControlEnabled ||
		emailEnabled ||
		oidcEnabled ||
		observabilityEnabled ||
		tracingEnabled;

	let network: StartedNetwork | undefined;
	if (needsNetwork) {
		log('Creating network...');
		network = await new Network().start();
		log('Network created');
	}

	let environment: Record<string, string> = {
		...BASE_ENV,
		...env,
	};

	// Add proxy hops only if using load balancer
	if (needsLoadBalancer) {
		environment.N8N_PROXY_HOPS = '1';
	}

	if (usePostgres) {
		assert(network, 'Network should be created for postgres');
		log('Starting PostgreSQL...');
		const postgresContainer = await setupPostgres({
			postgresImage: POSTGRES_IMAGE,
			projectName: uniqueProjectName,
			network,
		});
		containers.push(postgresContainer.container);
		log('PostgreSQL ready');
		environment = {
			...environment,
			DB_TYPE: 'postgresdb',
			DB_POSTGRESDB_HOST: 'postgres',
			DB_POSTGRESDB_PORT: '5432',
			DB_POSTGRESDB_DATABASE: postgresContainer.database,
			DB_POSTGRESDB_USER: postgresContainer.username,
			DB_POSTGRESDB_PASSWORD: postgresContainer.password,
		};
	} else {
		environment.DB_TYPE = 'sqlite';
	}

	if (queueConfig) {
		assert(network, 'Network should be created for queue mode');
		log('Starting Redis...');
		const redis = await setupRedis({
			redisImage: REDIS_IMAGE,
			projectName: uniqueProjectName,
			network,
		});
		containers.push(redis);
		log('Redis ready');
		environment = {
			...environment,
			EXECUTIONS_MODE: 'queue',
			QUEUE_BULL_REDIS_HOST: 'redis',
			QUEUE_BULL_REDIS_PORT: '6379',
			OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: 'true',
		};

		if (queueConfig.mains > 1) {
			if (!process.env.N8N_LICENSE_ACTIVATION_KEY) {
				throw new Error('N8N_LICENSE_ACTIVATION_KEY is required for multi-main instances');
			}
			environment = {
				...environment,
				N8N_MULTI_MAIN_SETUP_ENABLED: 'true',
			};
		}
	}

	if (proxyServerEnabled) {
		assert(network, 'Network should be created for ProxyServer');
		const hostname = 'proxyserver';
		const port = 1080;
		const url = `http://${hostname}:${port}`;
		const proxyServerContainer: StartedTestContainer = await setupProxyServer({
			proxyServerImage: MOCKSERVER_IMAGE,
			projectName: uniqueProjectName,
			network,
			hostname,
			port,
		});

		containers.push(proxyServerContainer);

		environment = {
			...environment,
			// Configure n8n to proxy all HTTP requests through ProxyServer
			HTTP_PROXY: url,
			HTTPS_PROXY: url,
			// Ensure https requests can be proxied without SSL issues
			...(proxyServerEnabled ? { NODE_TLS_REJECT_UNAUTHORIZED: '0' } : {}),
		};
	}

	if (taskRunnerEnabled) {
		environment = {
			...environment,
			N8N_RUNNERS_ENABLED: 'true',
			N8N_RUNNERS_MODE: 'external',
			N8N_RUNNERS_AUTH_TOKEN: 'test',
			N8N_RUNNERS_BROKER_LISTEN_ADDRESS: '0.0.0.0',
		};
	}

	// Set up Mailpit BEFORE creating n8n instances so they have the correct environment
	if (emailEnabled && network) {
		const hostname = 'mailpit';
		const smtpPort = 1025;
		const httpPort = 8025;

		const mailpitContainer = await setupMailpit({
			projectName: uniqueProjectName,
			network,
			hostname,
			smtpPort,
			httpPort,
		});
		containers.push(mailpitContainer);

		environment = {
			...environment,
			...getMailpitEnvironment(hostname, smtpPort),
		};
	}

	let observabilityStack: ObservabilityStack | undefined;
	let tracingStack: TracingStack | undefined;

	let earlyAllocatedPort: number | undefined;
	let earlyAllocatedLoadBalancerPort: number | undefined;
	if (oidcEnabled) {
		if (needsLoadBalancer) {
			earlyAllocatedLoadBalancerPort = await getPort();
		} else {
			earlyAllocatedPort = await getPort();
		}
	}

	let oidcDiscoveryUrl: string | undefined;
	let oidcInternalDiscoveryUrl: string | undefined;
	let keycloakCertPem: string | undefined;

	if (oidcEnabled && network) {
		log('Starting Keycloak for OIDC...');
		const n8nPort = needsLoadBalancer ? earlyAllocatedLoadBalancerPort! : earlyAllocatedPort!;
		const n8nCallbackUrl = `http://localhost:${n8nPort}/rest/sso/oidc/callback`;

		const keycloakResult = await setupKeycloak({
			projectName: uniqueProjectName,
			network,
			n8nCallbackUrl,
		});
		containers.push(keycloakResult.container);
		oidcDiscoveryUrl = keycloakResult.discoveryUrl;
		oidcInternalDiscoveryUrl = keycloakResult.internalDiscoveryUrl;
		keycloakCertPem = keycloakResult.certPem;

		log(`Keycloak ready. Discovery URL: ${oidcDiscoveryUrl}`);

		environment = {
			...environment,
			...getKeycloakN8nEnvironment(),
		};
	}

	// Set up observability stack early to capture startup metrics
	// VictoriaMetrics will retry failed scrapes until n8n containers are ready
	// Vector collects container logs independently (persists even when process exits)
	if (observabilityEnabled && network) {
		log('Setting up observability stack (VictoriaLogs + VictoriaMetrics + Vector)...');

		// Pre-compute scrape targets (hostnames are deterministic)
		const workerCount = queueConfig?.workers ?? 0;
		const scrapeTargets: ScrapeTarget[] = [];

		// Add main targets (all grouped under 'n8n-main' job)
		// Hostname must match the container name: single instance uses '-n8n', multi uses '-n8n-main-{i}'
		for (let i = 1; i <= mainCount; i++) {
			const hostname =
				mainCount > 1 ? `${uniqueProjectName}-n8n-main-${i}` : `${uniqueProjectName}-n8n`;
			scrapeTargets.push({
				job: 'n8n-main',
				instance: `n8n-main-${i}`,
				host: hostname,
				port: 5678,
			});
		}

		// Add worker targets (all grouped under 'n8n-worker' job)
		for (let i = 1; i <= workerCount; i++) {
			scrapeTargets.push({
				job: 'n8n-worker',
				instance: `n8n-worker-${i}`,
				host: `${uniqueProjectName}-n8n-worker-${i}`,
				port: 5678,
			});
		}

		// Start full observability stack (VictoriaLogs, VictoriaMetrics, Vector)
		observabilityStack = await setupObservabilityStack({
			projectName: uniqueProjectName,
			network,
			scrapeTargets,
		});

		containers.push(
			observabilityStack.victoriaLogs.container,
			observabilityStack.victoriaMetrics.container,
		);
		if (observabilityStack.vector) {
			containers.push(observabilityStack.vector.container);
		}
		log('Observability stack ready (logs collected by Vector)');
	}

	if (tracingEnabled && network) {
		log('Setting up tracing stack (n8n-tracer + Jaeger)...');

		tracingStack = await setupTracingStack({
			projectName: uniqueProjectName,
			network,
			deploymentMode: 'scaling',
		});

		containers.push(tracingStack.jaeger.container, tracingStack.n8nTracer.container);
		log('Tracing stack ready');
	}

	let baseUrl: string;

	if (needsLoadBalancer) {
		assert(network, 'Network should be created for load balancer');
		log('Starting Caddy load balancer...');
		const loadBalancerContainer = await setupCaddyLoadBalancer({
			caddyImage: CADDY_IMAGE,
			projectName: uniqueProjectName,
			mainCount,
			network,
			hostPort: earlyAllocatedLoadBalancerPort,
		});
		containers.push(loadBalancerContainer);
		log('Caddy load balancer ready');

		const loadBalancerPort =
			earlyAllocatedLoadBalancerPort ?? loadBalancerContainer.getMappedPort(80);
		baseUrl = `http://localhost:${loadBalancerPort}`;
		environment = {
			...environment,
			WEBHOOK_URL: baseUrl,
		};

		log(`Starting n8n instances (${mainCount} mains, ${queueConfig?.workers ?? 0} workers)...`);
		const instances = await createN8NInstances({
			mainCount,
			workerCount: queueConfig?.workers ?? 0,
			uniqueProjectName,
			environment,
			network,
			resourceQuota,
			keycloakCertPem,
		});
		containers.push(...instances);
		log('All n8n instances started');

		// Wait for all containers to be ready behind the load balancer
		log('Polling load balancer for readiness...');
		await pollContainerHttpEndpoint(loadBalancerContainer, '/healthz/readiness');
		log('Load balancer is ready');
	} else {
		// Use early allocated port if available (OIDC), otherwise allocate new
		const assignedPort = earlyAllocatedPort ?? (await getPort());
		baseUrl = `http://localhost:${assignedPort}`;
		environment = {
			...environment,
			WEBHOOK_URL: baseUrl,
			N8N_PORT: '5678', // Internal port
		};

		const instances = await createN8NInstances({
			mainCount: 1,
			workerCount: queueConfig?.workers ?? 0,
			uniqueProjectName,
			environment,
			network,
			directPort: assignedPort,
			resourceQuota,
			keycloakCertPem,
		});
		containers.push(...instances);
	}

	if (oidcEnabled && oidcInternalDiscoveryUrl) {
		log('Verifying Keycloak connectivity from n8n containers...');
		const n8nContainers = containers.filter((c) => {
			const name = c.getName();
			return name.includes('-n8n-main-') || name.endsWith('-n8n');
		});
		for (const container of n8nContainers) {
			await waitForKeycloakFromContainer(container, oidcInternalDiscoveryUrl);
		}
		log('Keycloak connectivity verified');
	}

	if (taskRunnerEnabled && network) {
		// Determine task broker hostname based on mode:
		// - Queue mode with workers: use first worker
		// - Queue mode without workers (multi-main): use first main (named -n8n-main-1)
		// - Single instance: use main (named -n8n)
		const taskBrokerHost = queueConfig?.workers
			? `${uniqueProjectName}-n8n-worker-1`
			: mainCount > 1
				? `${uniqueProjectName}-n8n-main-1`
				: `${uniqueProjectName}-n8n`;
		const taskBrokerUri = `http://${taskBrokerHost}:5679`;

		const taskRunnerContainer = await setupTaskRunner({
			projectName: uniqueProjectName,
			network,
			taskBrokerUri,
		});
		containers.push(taskRunnerContainer);
	}

	if (sourceControlEnabled && network) {
		const giteaContainer = await setupGitea({
			giteaImage: GITEA_IMAGE,
			projectName: uniqueProjectName,
			network,
		});
		containers.push(giteaContainer);
	}

	log(`Stack ready! baseUrl: ${baseUrl}`);

	// Wait for Docker network changes to settle before proceeding.
	// This prevents ERR_NETWORK_CHANGED errors when containers join the network.
	await waitForNetworkQuiet();

	return {
		baseUrl,
		stop: async () => {
			await stopN8NStack(containers, network, uniqueProjectName);
		},
		containers,
		...(oidcDiscoveryUrl && {
			oidc: {
				discoveryUrl: oidcDiscoveryUrl,
				internalDiscoveryUrl: oidcInternalDiscoveryUrl!,
			},
		}),
		...(observabilityStack && { observability: observabilityStack }),
		...(tracingStack && { tracing: tracingStack }),
	};
}

async function stopN8NStack(
	containers: StartedTestContainer[],
	network: StartedNetwork | undefined,
	uniqueProjectName: string,
): Promise<void> {
	const errors: Error[] = [];
	try {
		const stopPromises = containers.reverse().map(async (container) => {
			try {
				await container.stop();
			} catch (error) {
				errors.push(new Error(`Failed to stop container ${container.getId()}: ${error as string}`));
			}
		});
		await Promise.allSettled(stopPromises);

		if (network) {
			try {
				await network.stop();
			} catch (error) {
				errors.push(new Error(`Failed to stop network ${network.getName()}: ${error as string}`));
			}
		}

		if (errors.length > 0) {
			console.warn(
				`Some cleanup operations failed for stack ${uniqueProjectName}:`,
				errors.map((e) => e.message).join(', '),
			);
		}
	} catch (error) {
		console.error(`Critical error during cleanup for stack ${uniqueProjectName}:`, error);
		throw error;
	}
}

function normalizeQueueConfig(
	queueMode: boolean | { mains?: number; workers?: number },
): { mains: number; workers: number } | null {
	if (!queueMode) return null;
	if (typeof queueMode === 'boolean') {
		return { mains: 1, workers: 1 };
	}
	return {
		mains: queueMode.mains ?? 1,
		workers: queueMode.workers ?? 1,
	};
}

interface CreateInstancesOptions {
	mainCount: number;
	workerCount: number;
	uniqueProjectName: string;
	environment: Record<string, string>;
	network?: StartedNetwork;
	directPort?: number;
	resourceQuota?: {
		memory?: number; // in GB
		cpu?: number; // in cores
	};
	keycloakCertPem?: string;
}

async function createN8NInstances({
	mainCount,
	workerCount,
	uniqueProjectName,
	environment,
	network,
	/** The host port to use for the main instance */
	directPort,
	resourceQuota,
	keycloakCertPem,
}: CreateInstancesOptions): Promise<StartedTestContainer[]> {
	const instances: StartedTestContainer[] = [];
	const log = createElapsedLogger('n8n-instances');

	// Create main instances sequentially to avoid database migration conflicts
	for (let i = 1; i <= mainCount; i++) {
		const name = mainCount > 1 ? `${uniqueProjectName}-n8n-main-${i}` : `${uniqueProjectName}-n8n`;
		log(`Starting main ${i}/${mainCount}: ${name}`);
		const container = await createN8NContainer({
			name,
			uniqueProjectName,
			environment,
			network,
			isWorker: false,
			instanceNumber: i,
			networkAlias: name, // Use container name as network alias for VictoriaMetrics scraping
			directPort: i === 1 ? directPort : undefined, // Only first main gets direct port
			resourceQuota,
			keycloakCertPem,
		});
		instances.push(container);
		log(`Main ${i}/${mainCount} ready`);
	}

	// Create worker instances
	for (let i = 1; i <= workerCount; i++) {
		const name = `${uniqueProjectName}-n8n-worker-${i}`;
		log(`Starting worker ${i}/${workerCount}: ${name}`);
		const container = await createN8NContainer({
			name,
			uniqueProjectName,
			environment,
			network,
			isWorker: true,
			instanceNumber: i,
			resourceQuota,
			keycloakCertPem,
		});
		instances.push(container);
		log(`Worker ${i}/${workerCount} ready`);
	}

	return instances;
}

interface CreateContainerOptions {
	name: string;
	uniqueProjectName: string;
	environment: Record<string, string>;
	network?: StartedNetwork;
	isWorker: boolean;
	instanceNumber: number;
	networkAlias?: string;
	directPort?: number;
	resourceQuota?: {
		memory?: number; // in GB
		cpu?: number; // in cores
	};
	keycloakCertPem?: string;
}

async function createN8NContainer({
	name,
	uniqueProjectName,
	environment,
	network,
	isWorker,
	instanceNumber,
	networkAlias,
	directPort,
	resourceQuota,
	keycloakCertPem,
}: CreateContainerOptions): Promise<StartedTestContainer> {
	const taskRunnerEnabled = environment.N8N_RUNNERS_ENABLED === 'true';

	// Use silent log consumer - Vector handles log collection when observability is enabled
	const { consumer, throwWithLogs } = createSilentLogConsumer();

	let container = new GenericContainer(N8N_IMAGE);

	container = container
		.withEnvironment(environment)
		.withLabels({
			'com.docker.compose.project': uniqueProjectName,
			'com.docker.compose.service': isWorker ? 'n8n-worker' : 'n8n-main',
			instance: instanceNumber.toString(),
		})
		.withPullPolicy(new N8nImagePullPolicy(N8N_IMAGE))
		.withName(name)
		.withLogConsumer(consumer)
		.withReuse();

	if (keycloakCertPem) {
		container = container.withCopyContentToContainer([
			{
				content: keycloakCertPem,
				target: N8N_KEYCLOAK_CERT_PATH,
			},
		]);
	}

	if (resourceQuota) {
		container = container.withResourcesQuota({
			memory: resourceQuota.memory,
			cpu: resourceQuota.cpu,
		});
	}

	if (network) {
		container = container.withNetwork(network);
		if (networkAlias) {
			container = container.withNetworkAliases(networkAlias);
		}
	}

	if (isWorker) {
		// Workers expose task broker port if task runners are enabled
		const workerPorts = taskRunnerEnabled ? [5678, 5679] : [5678];
		container = container
			.withCommand(['worker'])
			.withExposedPorts(...workerPorts)
			.withWaitStrategy(N8N_WORKER_WAIT_STRATEGY);
	} else {
		// Mains always expose both ports (5678 for web, 5679 for task broker when enabled)
		const mainPorts = taskRunnerEnabled ? [5678, 5679] : [5678];
		container = container.withExposedPorts(...mainPorts).withWaitStrategy(N8N_MAIN_WAIT_STRATEGY);

		if (directPort) {
			const portMappings = taskRunnerEnabled
				? [{ container: 5678, host: directPort }, 5679]
				: [{ container: 5678, host: directPort }];
			container = container
				.withExposedPorts(...portMappings)
				.withWaitStrategy(N8N_MAIN_WAIT_STRATEGY);
		}
	}

	try {
		return await container.start();
	} catch (error: unknown) {
		if (
			error instanceof Error &&
			'statusCode' in error &&
			(error as Error & { statusCode: number }).statusCode === 404
		) {
			throw new DockerImageNotFoundError(name, error);
		}

		console.error(`Container "${name}" failed to start!`);
		console.error('Original error:', error instanceof Error ? error.message : String(error));

		return throwWithLogs(error);
	}
}
