import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import KeymapService from '@joplin/lib/services/KeymapService';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import resourceEditWatcherReducer, { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer';
import PluginRunner from './services/plugins/PluginRunner';
import PlatformImplementation from './services/plugins/PlatformImplementation';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import AlarmService from '@joplin/lib/services/AlarmService';
import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode';
import Logger, { TargetType } from '@joplin/utils/Logger';
import Setting from '@joplin/lib/models/Setting';
import actionApi from '@joplin/lib/services/rest/actionApi.desktop';
import BaseApplication, { StartOptions } from '@joplin/lib/BaseApplication';
import DebugService from '@joplin/lib/debug/DebugService';
import { _, setLocale } from '@joplin/lib/locale';
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
import SpellCheckerServiceDriverNative from './services/spellChecker/SpellCheckerServiceDriverNative';
import bridge from './services/bridge';
import menuCommandNames from './gui/menuCommandNames';
import ResourceService from '@joplin/lib/services/ResourceService';
import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
import appReducer, { createAppDefaultState } from './app.reducer';
import Folder from '@joplin/lib/models/Folder';
import Tag from '@joplin/lib/models/Tag';
import { reg } from '@joplin/lib/registry';
const packageInfo: PackageInfo = require('./packageInfo.js');
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import ClipperServer from '@joplin/lib/ClipperServer';
import { ipcRenderer } from 'electron';
const Menu = bridge().Menu;
const PluginManager = require('@joplin/lib/services/PluginManager');
import RevisionService from '@joplin/lib/services/RevisionService';
import MigrationService from '@joplin/lib/services/MigrationService';
import { loadCustomCss } from '@joplin/lib/CssUtils';
import { homedir } from 'os';
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
const electronContextMenu = require('./services/electron-context-menu');
// import  populateDatabase from '@joplin/lib/services/debug/populateDatabase';


// Commands that are not tied to any particular component.
// The runtime for these commands can be loaded when the app starts.

import PerFolderSortOrderService from '@joplin/lib/services/sortOrder/PerFolderSortOrderService';
import ShareService from '@joplin/lib/services/share/ShareService';
import checkForUpdates from './checkForUpdates';
import { AppState } from './app.reducer';
import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
import eventManager, { EventName } from '@joplin/lib/eventManager';
import path = require('path');
import { afterDefaultPluginsLoaded, loadAndRunDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseNotesParent } from '@joplin/lib/reducer';
import OcrService from '@joplin/lib/services/ocr/OcrService';
import OcrDriverTesseract from '@joplin/lib/services/ocr/drivers/OcrDriverTesseract';
import OcrDriverTranscribe from '@joplin/lib/services/ocr/drivers/OcrDriverTranscribe';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import { PackageInfo } from '@joplin/lib/versionInfo';
import { CustomContentProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
import initializeCommandService from './utils/initializeCommandService';
import OcrDriverBase from '@joplin/lib/services/ocr/OcrDriverBase';
import PerformanceLogger from '@joplin/lib/PerformanceLogger';
import Note from '@joplin/lib/models/Note';
import Resource from '@joplin/lib/models/Resource';

const perfLogger = PerformanceLogger.create();

const pluginClasses = [
	require('./plugins/GotoAnything').default,
];

const appDefaultState = createAppDefaultState(resourceEditWatcherDefaultState);

type StartupTask = { label: string; task: ()=> void|Promise<void> };

class Application extends BaseApplication {

	private checkAllPluginStartedIID_: ReturnType<typeof setInterval> = null;
	private initPluginServiceDone_ = false;
	private ocrService_: OcrService;
	private protocolHandler_: CustomContentProtocolHandler;

	public constructor() {
		super();

		this.bridge_nativeThemeUpdated = this.bridge_nativeThemeUpdated.bind(this);
	}

	public hasGui() {
		return true;
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public reducer(state: AppState = appDefaultState, action: any) {
		let newState = appReducer(state, action);
		newState = resourceEditWatcherReducer(newState, action);
		newState = super.reducer(newState, action) as AppState;
		return newState;
	}

	public toggleDevTools(visible: boolean) {
		if (visible) {
			bridge().openDevTools();
		} else {
			bridge().closeDevTools();
		}
	}

	private updateLanguage() {
		setLocale(Setting.value('locale'));
		// The bridge runs within the main process, with its own instance of locale.js
		// so it needs to be set too here.
		bridge().setLocale(Setting.value('locale'));

		const htmlContainer = document.querySelector('html');
		// HTML expects the lang attribute to be in BCP47 format, with a dash rather than
		// an underscore:
		const htmlLang = Setting.value('locale').replace(/_/g, '-');
		htmlContainer.setAttribute('lang', htmlLang);
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	protected async generalMiddleware(store: any, next: any, action: any) {
		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'locale' || action.type === 'SETTING_UPDATE_ALL') {
			this.updateLanguage();
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') {
			bridge().electronApp().getContentProtocolHandler().setMediaAccessEnabled(
				Setting.value('renderer.fileUrls'),
			);
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
			this.updateTray();
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'ocr.enabled' || action.type === 'SETTING_UPDATE_ALL') {
			void this.setupOcrService();
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') {
			bridge().setZoomFactor(Setting.value('windowContentZoomFactor') / 100);
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'linking.extraAllowedExtensions' || action.type === 'SETTING_UPDATE_ALL') {
			bridge().extraAllowedOpenExtensions = Setting.value('linking.extraAllowedExtensions');
		}

		if ((action.type === 'SETTING_UPDATE_ONE' && action.key === 'globalHotkey') || action.type === 'SETTING_UPDATE_ALL') {
			bridge().updateGlobalHotkey(Setting.value('globalHotkey'));
		}

		if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
			await AlarmService.updateNoteNotification(action.id, action.type === 'NOTE_DELETE');
		}

		if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'featureFlag.autoUpdaterServiceEnabled' || action.type === 'SETTING_UPDATE_ALL') {
			if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) this.setupAutoUpdaterService();
		}

		const result = await super.generalMiddleware(store, next, action);
		const newState = store.getState();

		if (['NOTE_VISIBLE_PANES_TOGGLE', 'NOTE_VISIBLE_PANES_SET'].indexOf(action.type) >= 0) {
			Setting.setValue('noteVisiblePanes', newState.noteVisiblePanes);
		}

		if (['NOTE_DEVTOOLS_TOGGLE', 'NOTE_DEVTOOLS_SET'].indexOf(action.type) >= 0) {
			this.toggleDevTools(newState.devToolsVisible);
		}

		if (action.type === 'FOLDER_AND_NOTE_SELECT') {
			await Folder.expandTree(newState.folders, action.folderId);
		}

		if (this.hasGui() && ((action.type === 'SETTING_UPDATE_ONE' && ['themeAutoDetect', 'theme', 'preferredLightTheme', 'preferredDarkTheme'].includes(action.key)) || action.type === 'SETTING_UPDATE_ALL')) {
			this.handleThemeAutoDetect();
		}

		if (action.type === 'PLUGIN_ADD') {
			const plugin = PluginService.instance().pluginById(action.plugin.id);
			this.protocolHandler_.allowReadAccessToDirectory(plugin.baseDir);
			this.protocolHandler_.allowReadAccessToDirectory(plugin.dataDir);
		}

		return result;
	}

	public handleThemeAutoDetect() {
		if (!Setting.value('themeAutoDetect')) return;

		if (bridge().shouldUseDarkColors()) {
			Setting.setValue('theme', Setting.value('preferredDarkTheme'));
		} else {
			Setting.setValue('theme', Setting.value('preferredLightTheme'));
		}
	}

	private bridge_nativeThemeUpdated() {
		this.handleThemeAutoDetect();
	}

	public updateTray() {
		const app = bridge().electronApp();

		if (app.trayShown() === Setting.value('showTrayIcon')) return;

		if (!Setting.value('showTrayIcon')) {
			app.destroyTray();
		} else {
			const contextMenu = Menu.buildFromTemplate([
				{ label: _('Open %s', app.electronApp().name), click: () => { app.mainWindow().show(); } },
				{ type: 'separator' },
				{ label: _('Quit'), click: () => {
					app.quitWithSyncCheck(
						(action: { type: string; [key: string]: unknown }) => this.store().dispatch(action),
						this.store().getState().syncPending,
					);
				} },
			]);
			app.createTray(contextMenu);
		}
	}

	public setupContextMenu() {
		// bridge().setupContextMenu((misspelledWord: string, dictionarySuggestions: string[]) => {
		// 	let output = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions);
		// 	console.info(misspelledWord, dictionarySuggestions);
		// 	console.info(output);
		// 	output = output.map(o => {
		// 		delete o.click;
		// 		return o;
		// 	});
		// 	return output;
		// });


		const MenuItem = bridge().MenuItem;

		// The context menu must be setup in renderer process because that's where
		// the spell checker service lives.
		electronContextMenu({
			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
			shouldShowMenu: (_event: any, params: any) => {
				// params.inputFieldType === 'none' when right-clicking the text editor. This is a bit of a hack to detect it because in this
				// case we don't want to use the built-in context menu but a custom one.
				return params.isEditable && params.inputFieldType !== 'none';
			},

			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
			menu: (actions: any, props: any) => {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(props.misspelledWord, props.dictionarySuggestions).map((item: any) => new MenuItem(item));

				const output = [
					actions.cut(),
					actions.copy(),
					actions.paste(),
					...spellCheckerMenuItems,
				];

				return output;
			},
		});
	}

	private async initPluginService() {
		if (this.initPluginServiceDone_) return;
		this.initPluginServiceDone_ = true;

		const service = PluginService.instance();

		const pluginRunner = new PluginRunner();
		service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
		service.isSafeMode = Setting.value('isSafeMode');

		let pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
		{
			// Users can add and remove plugins from the config screen at any
			// time, however we only effectively uninstall the plugin the next
			// time the app is started. What plugin should be uninstalled is
			// stored in the settings.
			pluginSettings = service.clearUpdateState(await service.uninstallPlugins(pluginSettings));
			Setting.setValue('plugins.states', pluginSettings);
		}

		// As of Joplin 3.5.7, the ABC rendering is part of the app so we automatically disable the plugin
		if (pluginSettings['org.joplinapp.plugins.AbcSheetMusic']) {
			pluginSettings = {
				...pluginSettings,
				['org.joplinapp.plugins.AbcSheetMusic']: {
					enabled: false,
					deleted: false,
					hasBeenUpdated: false,
				},
			};
		}

		try {
			if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
				await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
			}
		} catch (error) {
			this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
		}

		try {
			await service.loadAndRunDevPlugins(pluginSettings);
		} catch (error) {
			this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
		}

		// Load default plugins after loading other plugins -- this allows users
		// to override built-in plugins with development versions with the same
		// ID.
		const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
		try {
			pluginSettings = await loadAndRunDefaultPlugins(service, defaultPluginsDir, getDefaultPluginsInfo(), pluginSettings);
		} catch (error) {
			this.logger().error(`There was an error loading plugins from ${defaultPluginsDir}:`, error);
		}

		{
			// Users can potentially delete files from /plugins or even delete
			// the complete folder. When that happens, we still have the plugin
			// info in the state, which can cause various issues, so to sort it
			// out we remove from the state any plugin that has *not* been loaded
			// above (meaning the file was missing).
			// https://github.com/laurent22/joplin/issues/5253
			const oldSettings = pluginSettings;
			const newSettings: PluginSettings = {};
			for (const pluginId of Object.keys(oldSettings)) {
				if (!service.pluginIds.includes(pluginId)) {
					this.logger().warn('Found a plugin in the state that has not been loaded, which means the plugin might have been deleted outside Joplin - removing it from the state:', pluginId);
					continue;
				}
				newSettings[pluginId] = oldSettings[pluginId];
			}
			Setting.setValue('plugins.states', newSettings);
			pluginSettings = newSettings;
		}

		this.checkAllPluginStartedIID_ = setInterval(() => {
			if (service.allPluginsStarted) {
				clearInterval(this.checkAllPluginStartedIID_);
				this.dispatch({
					type: 'STARTUP_PLUGINS_LOADED',
					value: true,
				});

				// Sends an event to the main process -- this is used by the Playwright
				// tests to wait for plugins to load.
				ipcRenderer.send('startup-plugins-loaded');

				void afterDefaultPluginsLoaded(service.plugins, getDefaultPluginsInfo(), pluginSettings);
			}
		}, 500);
	}

	private async setupOcrService() {
		if (Setting.value('ocr.clearLanguageDataCache')) {
			Setting.setValue('ocr.clearLanguageDataCache', false);
			try {
				await OcrDriverTesseract.clearLanguageDataCache();
			} catch (error) {
				this.logger().warn('OCR: Failed to clear language data cache.', error);
			}
		}

		if (Setting.value('ocr.enabled')) {

			if (!this.ocrService_) {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				const Tesseract = (window as any).Tesseract;

				const drivers: OcrDriverBase[] = [];
				drivers.push(new OcrDriverTesseract(
					{ createWorker: Tesseract.createWorker },
					{
						workerPath: `${bridge().buildDir()}/tesseract.js/worker.min.js`,
						corePath: `${bridge().buildDir()}/tesseract.js-core`,
						languageDataPath: Setting.value('ocr.languageDataPath') || null,
					},
				));

				drivers.push(new OcrDriverTranscribe());

				this.ocrService_ = new OcrService(drivers);
			}

			void this.ocrService_.runInBackground();
		} else {
			if (!this.ocrService_) return;
			void this.ocrService_.stopRunInBackground();
		}

		const handleResourceChange = () => {
			void this.ocrService_.maintenance();
		};

		eventManager.on(EventName.ResourceCreate, handleResourceChange);
		eventManager.on(EventName.ResourceChange, handleResourceChange);
	}

	private setupAutoUpdaterService() {
		this.logger().info('Setting up auto-updater service...');

		if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
			bridge().electronApp().initializeAutoUpdaterService(
				Logger.create('AutoUpdaterService'),
				Setting.value('env') === 'dev',
				Setting.value('autoUpdate.includePreReleases'),
			);
		}
	}

	private async setupCustomCss() {
		const chromeCssPath = Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP);
		if (await shim.fsDriver().exists(chromeCssPath)) {
			this.store().dispatch({
				// Main window custom CSS
				type: 'CUSTOM_CHROME_CSS_ADD',
				filePath: chromeCssPath,
			});
		}

		this.store().dispatch({
			// Markdown preview pane
			type: 'CUSTOM_VIEWER_CSS_APPEND',
			css: await loadCustomCss(Setting.customCssFilePath(Setting.customCssFilenames.RENDERED_MARKDOWN)),
		});
	}

	private async setupIntegrationTestUtils() {
		// Events used by Playwright tests to quickly change the value of a setting.
		ipcRenderer.on('testing--setSetting', (_event, key, value) => {
			this.logger().info('Updating setting using testing API: %s = %s', key, JSON.stringify(value));
			Setting.setValue(key, value);
		});
	}

	private buildStartupTasks_() {
		const tasks: StartupTask[] = [];
		const addTask = (label: string, task: StartupTask['task']) => {
			tasks.push({ label, task });
		};

		addTask('app/set up extra debug logging', () => {
			reg.logger().info('app.start: doing regular boot');
			const dir: string = Setting.value('profileDir');

			syncDebugLog.enabled = false;

			if (dir.endsWith('dev-desktop-2')) {
				syncDebugLog.addTarget(TargetType.File, {
					path: `${homedir()}/synclog.txt`,
				});
				syncDebugLog.enabled = true;
				syncDebugLog.info(`Profile dir: ${dir}`);
			}
		});

		addTask('app/set up registry', () => {
			reg.setDispatch(this.dispatch.bind(this));
			reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); });
		});

		addTask('app/set up auto updater', () => {
			this.setupAutoUpdaterService();
		});

		addTask('app/set up AlarmService', () => {
			AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
			AlarmService.setLogger(reg.logger());
		});

		if (Setting.value('flagOpenDevTools')) {
			addTask('app/openDevTools', () => {
				bridge().openDevTools();
			});
		}

		addTask('app/set up custom protocol handler', async () => {
			this.protocolHandler_ = bridge().electronApp().getContentProtocolHandler();
			this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
			this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
			this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
		});
		// this.protocolHandler_.allowReadAccessTo(Setting.value('tempDir'));
		// For now, this doesn't seem necessary:
		//  this.protocolHandler_.allowReadAccessTo(Setting.value('profileDir'));
		// If it is needed, note that they decrease the security of the protcol
		// handler, and, as such, it may make sense to also limit permissions of
		// allowed pages with a Content Security Policy.

		addTask('app/initialize PluginManager, redux, CommandService, and KeymapService', async () => {
			PluginManager.instance().dispatch_ = this.dispatch.bind(this);
			PluginManager.instance().setLogger(reg.logger());
			PluginManager.instance().register(pluginClasses);

			this.initRedux();

			initializeCommandService(this.store(), Setting.value('env') === 'dev');

			const keymapService = KeymapService.instance();
			// We only add the commands that appear in the menu because only
			// those can have a shortcut associated with them.
			keymapService.initialize(menuCommandNames());

			try {
				await keymapService.loadCustomKeymap(`${Setting.value('profileDir')}/keymap-desktop.json`);
			} catch (error) {
				reg.logger().error(error);
			}
		});

		addTask('app/initialize PerFolderSortOrderService', () => {
			PerFolderSortOrderService.initialize();
		});

		addTask('app/dispatch initial settings', () => {
			// Since the settings need to be loaded before the store is
			// created, it will never receive the SETTING_UPDATE_ALL even,
			// which mean state.settings will not be initialised. So we
			// manually call dispatchUpdateAll() to force an update.
			Setting.dispatchUpdateAll();
		});

		addTask('app/update folders and tags', async () => {
			await refreshFolders((action) => this.dispatch(action), '');

			const tags = await Tag.allWithNotes();
			this.dispatch({
				type: 'TAG_UPDATE_ALL',
				items: tags,
			});
		});

		addTask('app/set up custom CSS', async () => {
			await this.setupCustomCss();
		});

		// const masterKeys = await MasterKey.all();

		// this.dispatch({
		// 	type: 'MASTERKEY_UPDATE_ALL',
		// 	items: masterKeys,
		// });

		addTask('app/send initial selection to redux', async () => {
			const getNotesParent = async () => {
				let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
				if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
					notesParent = {
						type: 'Folder',
						selectedItemId: Setting.value('activeFolderId'),
					};
				}
				return notesParent;
			};

			const notesParent = await getNotesParent();
			if (notesParent.type === 'SmartFilter') {
				this.store().dispatch({
					type: 'SMART_FILTER_SELECT',
					id: notesParent.selectedItemId,
				});
			} else if (notesParent.type === 'Tag') {
				this.store().dispatch({
					type: 'TAG_SELECT',
					id: notesParent.selectedItemId,
				});
			} else {
				this.store().dispatch({
					type: 'FOLDER_SELECT',
					id: notesParent.selectedItemId,
				});
			}

			this.store().dispatch({
				type: 'FOLDER_SET_COLLAPSED_ALL',
				ids: Setting.value('collapsedFolderIds'),
			});

			this.store().dispatch({
				type: 'NOTE_DEVTOOLS_SET',
				value: Setting.value('flagOpenDevTools'),
			});
		});

		addTask('app/initializeUserFetcher', async () => {
			initializeUserFetcher();
			shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
		});

		addTask('app/updateTray', () => this.updateTray());

		addTask('app/set main window state', () => {
			if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) {
				bridge().mainWindow().hide();
			} else {
				bridge().mainWindow().show();
			}
		});

		addTask('app/start maintenance tasks', () => {
			// Always disable on Mac for now - and disable too for the few apps that may have the flag enabled.
			// At present, it only seems to work on Windows.
			if (shim.isMac()) {
				Setting.setValue('featureFlag.autoUpdaterServiceEnabled', false);
			}

			// Note: Auto-update is a misnomer in the code.
			// The code below only checks, if a new version is available.
			// We only allow Windows and macOS users to automatically check for updates
			if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
				if (shim.isWindows() || shim.isMac()) {
					const runAutoUpdateCheck = () => {
						if (Setting.value('autoUpdateEnabled')) {
							void checkForUpdates(true, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
						}
					};

					// Initial check on startup
					shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
					// Then every x hours
					shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
				}
			}

			shim.setTimeout(() => {
				void AlarmService.garbageCollect();
			}, 1000 * 60 * 60);
			void ShareService.instance().maintenance();

			ResourceService.runInBackground();

			if (Setting.value('env') === 'dev') {
				void AlarmService.updateAllNotifications();
				RevisionService.instance().runInBackground();
			} else {
				setTimeout(() => {
					// Schedule sync with a delay of 0 and wrap with the desired timeout, as shim.setTimeout may not fire on first run or after an upgrade
					// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
					void reg.scheduleSync(0).then(() => {
						// Wait for the first sync before updating the notifications, since synchronisation
						// might change the notifications.
						void AlarmService.updateAllNotifications();

						void DecryptionWorker.instance().scheduleStart();

						RevisionService.instance().runInBackground();
					});
				}, 1000);
			}

			this.startRotatingLogMaintenance(Setting.value('profileDir'));
		});

		addTask('app/set up ClipperServer', () => {
			const clipperLogger = new Logger();
			clipperLogger.addTarget(TargetType.File, { path: `${Setting.value('profileDir')}/log-clipper.txt` });
			clipperLogger.addTarget(TargetType.Console);

			ClipperServer.instance().initialize(actionApi);
			ClipperServer.instance().setEnabled(!Setting.value('altInstanceId'));
			ClipperServer.instance().setLogger(clipperLogger);
			ClipperServer.instance().setDispatch(this.store().dispatch);

			if (ClipperServer.instance().enabled() && Setting.value('clipperServer.autoStart')) {
				void ClipperServer.instance().start();
			}
		});

		addTask('app/set up external edit watchers', () => {
			ExternalEditWatcher.instance().setLogger(reg.logger());
			ExternalEditWatcher.instance().initialize(bridge, this.store().dispatch);

			ResourceEditWatcher.instance().initialize(
				reg.logger(),
				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				(action: any) => { this.store().dispatch(action); },
				(path: string) => bridge().openItem(path),
				() => this.store().getState().windowId,
			);

			// Forwards the local event to the global event manager, so that it can
			// be picked up by the plugin manager.
			// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
			ResourceEditWatcher.instance().on('resourceChange', (event: any) => {
				eventManager.emit(EventName.ResourceChange, event);
			});
		});

		// Make it available to the console window - useful to call revisionService.collectRevisions()
		if (Setting.value('env') === 'dev') {
			addTask('app/add debug variables', () => {
				// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
				(window as any).joplin = {
					revisionService: RevisionService.instance(),
					migrationService: MigrationService.instance(),
					decryptionWorker: DecryptionWorker.instance(),
					commandService: CommandService.instance(),
					pluginService: PluginService.instance(),
					bridge: bridge(),
					debug: new DebugService(reg.db()),
					resourceService: ResourceService.instance(),
					searchEngine: SearchEngine.instance(),
					shim,
					Note,
					Folder,
					Resource,
					Setting,
					ocrService: () => this.ocrService_,
				};
			});
		}

		addTask('app/listen for main process events', () => {
			bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
			bridge().setOnAllowedExtensionsChangeListener((newExtensions) => {
				Setting.setValue('linking.extraAllowedExtensions', newExtensions);
			});

			ipcRenderer.on('window-focused', (_event, newWindowId) => {
				const currentWindowId = this.store().getState().windowId;
				if (newWindowId !== currentWindowId) {
					this.dispatch({
						type: 'WINDOW_FOCUS',
						windowId: newWindowId,
						lastWindowId: currentWindowId,
					});
				}
			});

			ipcRenderer.on('secondary-window-closing', (_event, windowId: string) => {
				this.dispatch({ type: 'WINDOW_CLOSE', windowId });
			});
		});

		addTask('app/initPluginService', () => this.initPluginService());

		addTask('app/setupContextMenu', () => {
			this.setupContextMenu();
		});

		addTask('app/set up SpellCheckerService', async () => {
			await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
		});

		addTask('app/listen for resource events', () => {
			eventManager.on(EventName.OcrServiceResourcesProcessed, async () => {
				await ResourceService.instance().indexNoteResources();
			});

			eventManager.on(EventName.NoteResourceIndexed, async () => {
				SearchEngine.instance().scheduleSyncTables();
			});
		});

		addTask('app/setupOcrService', () => this.setupOcrService());

		return tasks;
	}

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
	public async start(argv: string[], startOptions: StartOptions = null): Promise<any> {
		const startupTask = perfLogger.taskStart('app/start');

		// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
		// insert an extra argument so that they can be processed in a consistent way everywhere.
		if (!bridge().electronIsDev()) argv.splice(1, 0, '.');


		argv = await super.start(argv, startOptions);

		await this.setupIntegrationTestUtils();

		bridge().setLogFilePath(Logger.globalLogger.logFilePath());
		await this.applySettingsSideEffects();

		if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) {
			reg.logger().info('app.start: doing upgradeSyncTarget action');
			bridge().mainWindow().show();
			startupTask.onEnd();

			return { action: 'upgradeSyncTarget' };
		}

		const startupTasks = this.buildStartupTasks_();
		for (const task of startupTasks) {
			await perfLogger.track(task.label, async () => task.task());
		}


		// setTimeout(() => {
		// 	void populateDatabase(reg.db(), {
		// 		clearDatabase: true,
		// 		folderCount: 200,
		// 		noteCount: 3000,
		// 		tagCount: 2000,
		// 		tagsPerNote: 10,
		// 		rootFolderCount: 20,
		// 		subFolderDepth: 3,
		// 	});
		// }, 5000);

		// setTimeout(() => {
		// 	console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
		// }, 2000);

		// setTimeout(() => {
		// 	this.dispatch({
		// 		type: 'NAV_GO',
		// 		routeName: 'Config',
		// 		props: {
		// 			defaultSection: 'encryption',
		// 		},
		// 	});
		// }, 2000);

		// setTimeout(() => {
		// 	this.dispatch({
		// 		type: 'DIALOG_OPEN',
		// 		name: 'syncWizard',
		// 	});
		// }, 2000);

		// setTimeout(() => {
		// 	this.dispatch({
		// 		type: 'DIALOG_OPEN',
		// 		name: 'editFolder',
		// 	});
		// }, 3000);

		// setTimeout(() => {
		// 	this.dispatch({
		// 		type: 'NAV_GO',
		// 		routeName: 'Config',
		// 		props: {
		// 			defaultSection: 'plugins',
		// 		},
		// 	});
		// }, 2000);

		// await runIntegrationTests();

		// Used by tests
		ipcRenderer.send('startup-finished');

		startupTask.onEnd();
		return null;
	}

}

let application_: Application = null;

function app() {
	if (!application_) application_ = new Application();
	return application_;
}

export default app;
