import { GlobalConfig } from '@n8n/config';
import type { DataSourceOptions, LoggerOptions } from '@n8n/typeorm';
import type { MysqlConnectionOptions } from '@n8n/typeorm/driver/mysql/MysqlConnectionOptions';
import type { PostgresConnectionOptions } from '@n8n/typeorm/driver/postgres/PostgresConnectionOptions';
import type { SqliteConnectionOptions } from '@n8n/typeorm/driver/sqlite/SqliteConnectionOptions';
import type { SqlitePooledConnectionOptions } from '@n8n/typeorm/driver/sqlite-pooled/SqlitePooledConnectionOptions';
import { InstanceSettings } from 'n8n-core';
import { ApplicationError } from 'n8n-workflow';
import path from 'path';
import type { TlsOptions } from 'tls';
import { Container } from 'typedi';

import { entities } from './entities';
import { mysqlMigrations } from './migrations/mysqldb';
import { postgresMigrations } from './migrations/postgresdb';
import { sqliteMigrations } from './migrations/sqlite';
import { subscribers } from './subscribers';

const getCommonOptions = () => {
	const { tablePrefix: entityPrefix, logging: loggingConfig } =
		Container.get(GlobalConfig).database;

	let loggingOption: LoggerOptions = loggingConfig.enabled;
	if (loggingOption) {
		const optionsString = loggingConfig.options.replace(/\s+/g, '');
		if (optionsString === 'all') {
			loggingOption = optionsString;
		} else {
			loggingOption = optionsString.split(',') as LoggerOptions;
		}
	}

	return {
		entityPrefix,
		entities: Object.values(entities),
		subscribers: Object.values(subscribers),
		migrationsTableName: `${entityPrefix}migrations`,
		migrationsRun: false,
		synchronize: false,
		maxQueryExecutionTime: loggingConfig.maxQueryExecutionTime,
		logging: loggingOption,
	};
};

export const getOptionOverrides = (dbType: 'postgresdb' | 'mysqldb') => {
	const globalConfig = Container.get(GlobalConfig);
	const dbConfig = globalConfig.database[dbType];
	return {
		database: dbConfig.database,
		host: dbConfig.host,
		port: dbConfig.port,
		username: dbConfig.user,
		password: dbConfig.password,
	};
};

const getSqliteConnectionOptions = (): SqliteConnectionOptions | SqlitePooledConnectionOptions => {
	const globalConfig = Container.get(GlobalConfig);
	const sqliteConfig = globalConfig.database.sqlite;
	const commonOptions = {
		...getCommonOptions(),
		database: path.resolve(Container.get(InstanceSettings).n8nFolder, sqliteConfig.database),
		migrations: sqliteMigrations,
	};

	if (sqliteConfig.poolSize > 0) {
		return {
			type: 'sqlite-pooled',
			poolSize: sqliteConfig.poolSize,
			enableWAL: true,
			acquireTimeout: 60_000,
			destroyTimeout: 5_000,
			...commonOptions,
		};
	} else {
		return {
			type: 'sqlite',
			enableWAL: sqliteConfig.enableWAL,
			...commonOptions,
		};
	}
};

const getPostgresConnectionOptions = (): PostgresConnectionOptions => {
	const postgresConfig = Container.get(GlobalConfig).database.postgresdb;
	const {
		ssl: { ca: sslCa, cert: sslCert, key: sslKey, rejectUnauthorized: sslRejectUnauthorized },
	} = postgresConfig;

	let ssl: TlsOptions | boolean = postgresConfig.ssl.enabled;
	if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) {
		ssl = {
			ca: sslCa || undefined,
			cert: sslCert || undefined,
			key: sslKey || undefined,
			rejectUnauthorized: sslRejectUnauthorized,
		};
	}

	return {
		type: 'postgres',
		...getCommonOptions(),
		...getOptionOverrides('postgresdb'),
		schema: postgresConfig.schema,
		poolSize: postgresConfig.poolSize,
		migrations: postgresMigrations,
		connectTimeoutMS: postgresConfig.connectionTimeoutMs,
		ssl,
	};
};

const getMysqlConnectionOptions = (dbType: 'mariadb' | 'mysqldb'): MysqlConnectionOptions => ({
	type: dbType === 'mysqldb' ? 'mysql' : 'mariadb',
	...getCommonOptions(),
	...getOptionOverrides('mysqldb'),
	migrations: mysqlMigrations,
	timezone: 'Z', // set UTC as default
});

export function getConnectionOptions(): DataSourceOptions {
	const globalConfig = Container.get(GlobalConfig);
	const { type: dbType } = globalConfig.database;
	switch (dbType) {
		case 'sqlite':
			return getSqliteConnectionOptions();
		case 'postgresdb':
			return getPostgresConnectionOptions();
		case 'mariadb':
		case 'mysqldb':
			return getMysqlConnectionOptions(dbType);
		default:
			throw new ApplicationError('Database type currently not supported', { extra: { dbType } });
	}
}

export function arePostgresOptions(
	options: DataSourceOptions,
): options is PostgresConnectionOptions {
	return options.type === 'postgres';
}
