/* eslint-disable @typescript-eslint/unbound-method */
import { BinaryDataManager, UserSettings } from 'n8n-core';
import { Command, flags } from '@oclif/command';

import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import config from '@/config';
import * as ActiveExecutions from '@/ActiveExecutions';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
import { InternalHooksManager } from '@/InternalHooksManager';
import { WebhookServer } from '@/WebhookServer';
import { getLogger } from '@/Logger';
import { initErrorHandling } from '@/ErrorReporting';
import * as CrashJournal from '@/CrashJournal';

const exitWithCrash = async (message: string, error: unknown) => {
	ErrorReporter.error(new Error(message, { cause: error }), { level: 'fatal' });
	await sleep(2000);
	process.exit(1);
};

const exitSuccessFully = async () => {
	try {
		await CrashJournal.cleanup();
	} finally {
		process.exit();
	}
};

export class Webhook extends Command {
	static description = 'Starts n8n webhook process. Intercepts only production URLs.';

	static examples = ['$ n8n webhook'];

	static flags = {
		help: flags.help({ char: 'h' }),
	};

	/**
	 * Stops n8n in a graceful way.
	 * Make for example sure that all the webhooks from third party services
	 * get removed.
	 */
	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	static async stopProcess() {
		LoggerProxy.info('\nStopping n8n...');

		try {
			const externalHooks = ExternalHooks();
			await externalHooks.run('n8n.stop', []);

			setTimeout(async () => {
				// In case that something goes wrong with shutdown we
				// kill after max. 30 seconds no matter what
				await exitSuccessFully();
			}, 30000);

			// Wait for active workflow executions to finish
			const activeExecutionsInstance = ActiveExecutions.getInstance();
			let executingWorkflows = activeExecutionsInstance.getActiveExecutions();

			let count = 0;
			while (executingWorkflows.length !== 0) {
				if (count++ % 4 === 0) {
					LoggerProxy.info(
						`Waiting for ${executingWorkflows.length} active executions to finish...`,
					);
				}
				// eslint-disable-next-line no-await-in-loop
				await sleep(500);
				executingWorkflows = activeExecutionsInstance.getActiveExecutions();
			}
		} catch (error) {
			await exitWithCrash('There was an error shutting down n8n.', error);
		}

		await exitSuccessFully();
	}

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	async run() {
		if (config.getEnv('executions.mode') !== 'queue') {
			/**
			 * It is technically possible to run without queues but
			 * there are 2 known bugs when running in this mode:
			 * - Executions list will be problematic as the main process
			 * is not aware of current executions in the webhook processes
			 * and therefore will display all current executions as error
			 * as it is unable to determine if it is still running or crashed
			 * - You cannot stop currently executing jobs from webhook processes
			 * when running without queues as the main process cannot talk to
			 * the webhook processes to communicate workflow execution interruption.
			 */

			this.error('Webhook processes can only run with execution mode as queue.');
		}

		const logger = getLogger();
		LoggerProxy.init(logger);

		// Make sure that n8n shuts down gracefully if possible
		process.once('SIGTERM', Webhook.stopProcess);
		process.once('SIGINT', Webhook.stopProcess);

		await initErrorHandling();
		await CrashJournal.init();

		try {
			// Start directly with the init of the database to improve startup time
			const startDbInitPromise = Db.init().catch(async (error: Error) =>
				exitWithCrash('There was an error initializing DB', error),
			);

			// Make sure the settings exist
			// eslint-disable-next-line @typescript-eslint/no-unused-vars
			await UserSettings.prepareUserSettings();

			// Load all node and credential types
			const loadNodesAndCredentials = LoadNodesAndCredentials();
			await loadNodesAndCredentials.init();

			// Add the found types to an instance other parts of the application can use
			const nodeTypes = NodeTypes(loadNodesAndCredentials);
			const credentialTypes = CredentialTypes(loadNodesAndCredentials);

			// Load the credentials overwrites if any exist
			CredentialsOverwrites(credentialTypes);

			// Load all external hooks
			const externalHooks = ExternalHooks();
			await externalHooks.init();

			// Wait till the database is ready
			await startDbInitPromise;

			const instanceId = await UserSettings.getInstanceId();
			await InternalHooksManager.init(instanceId, nodeTypes);

			const binaryDataConfig = config.getEnv('binaryDataManager');
			await BinaryDataManager.init(binaryDataConfig);

			const server = new WebhookServer();
			await server.start();

			console.info('Webhook listener waiting for requests.');
		} catch (error) {
			await exitWithCrash('Exiting due to error.', error);
		}
	}
}
