import type { StartedTestContainer, StartedNetwork } from 'testcontainers';

/** Hostname that containers use to reach the host machine (Docker Desktop built-in) */
export const EXTERNAL_HOST = 'host.docker.internal';

export const SERVICE_NAMES = [
	'postgres',
	'redis',
	'mailpit',
	'gitea',
	'keycloak',
	'victoriaLogs',
	'victoriaMetrics',
	'vector',
	'tracing',
	'proxy',
	'taskRunner',
	'loadBalancer',
	'cloudflared',
	'kafka',
	'ngrok',
	'mysql',
	'localstack',
	'kent',
	'postgresExporter',
	'cadvisor',
	'sandbox',
] as const;

export type ServiceName = (typeof SERVICE_NAMES)[number];

export interface FileToMount {
	content: string;
	target: string;
}

export interface ServiceMeta {
	/**
	 * Files to mount into n8n containers. Use when n8n needs files that can't
	 * be passed via environment (e.g., NODE_EXTRA_CA_CERTS requires a file path).
	 * See keycloak.ts for usage example.
	 */
	n8nFilesToMount?: FileToMount[];
}

export interface ServiceResult<TMeta = unknown> {
	container: StartedTestContainer;
	meta: TMeta;
}

export interface StartContext {
	config: StackConfig;
	projectName: string;
	mains: number;
	workers: number;
	isQueueMode: boolean;
	usePostgres: boolean;
	needsLoadBalancer: boolean;
	/** When true, services should target host.testcontainers.internal instead of Docker-internal hostnames */
	external: boolean;
	environment: Record<string, string>;
	serviceResults: Partial<Record<ServiceName, ServiceResult>>;
	allocatedPorts: { main?: number; loadBalancer?: number };
	baseUrl?: string;
}

export type LoadBalancerPolicy = 'first' | 'round_robin' | 'random' | 'least_conn' | 'ip_hash';

export interface StackConfig {
	mains?: number;
	workers?: number;
	postgres?: boolean;
	env?: Record<string, string>;
	projectName?: string;
	resourceQuota?: { memory?: number; cpu?: number };
	workerResourceQuota?: { memory?: number; cpu?: number };
	services?: readonly ServiceName[];
	/** When true, services target host machine instead of Docker-internal n8n */
	external?: boolean;
	/**
	 * Caddy load-balancer upstream-selection policy. Only applies when `mains > 1`.
	 * Defaults to `'first'` — sticky to main #1, useful for UI debuggability.
	 * Benchmarks should set `'round_robin'` to actually distribute load.
	 */
	lbPolicy?: LoadBalancerPolicy;
}

export interface Service<TResult extends ServiceResult = ServiceResult> {
	/** @example 'Redis' */
	readonly description: string;
	/** @example ['victoriaLogs'] // vector depends on victoriaLogs */
	readonly dependsOn?: readonly ServiceName[];
	/** @example (ctx) => ctx.isQueueMode // redis auto-starts in queue mode */
	shouldStart?(ctx: StartContext): boolean;
	/** @example (ctx) => ({ taskBrokerUri: `http://${ctx.projectName}-n8n:5679` }) */
	getOptions?(ctx: StartContext): unknown;
	/** Starts container, returns connection details for env() */
	start(
		network: StartedNetwork,
		projectName: string,
		options?: unknown,
		ctx?: StartContext,
	): Promise<TResult>;
	/** @param external When true, returns host-compatible values using mapped ports (for local dev) */
	env?(result: TResult, external?: boolean): Record<string, string>;
	/** @param external When true, returns host-compatible values using mapped ports (for local dev) */
	extraEnv?(result: TResult, external?: boolean): Record<string, string>;
	/** Verifies service is reachable from inside n8n containers */
	verifyFromN8n?(result: TResult, n8nContainers: StartedTestContainer[]): Promise<void>;
}

export interface HelperContext {
	containers: StartedTestContainer[];
	findContainer(pattern: RegExp): StartedTestContainer | undefined;
	serviceResults: Partial<Record<ServiceName, ServiceResult>>;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ServiceHelpers {}

export type HelperFactory<T> = (ctx: HelperContext) => T;

export type HelperFactories = {
	[K in keyof ServiceHelpers]: HelperFactory<ServiceHelpers[K]>;
};
