import * as app from './app.js';
import * as auth from './auth.js';
import * as form from './form.js';
import * as httpd from './httpd.js';

let gProcessIndex = 0;
let gProcesses = {};
let gStatsTimer = false;

const k_mime_types = {
	'css': 'text/css',
	'html': 'text/html',
	'js': 'text/javascript',
	'json': 'text/json',
	'map': 'application/json',
	'svg': 'image/svg+xml',
};

const k_magic_bytes = [
	{bytes: [0xff, 0xd8, 0xff, 0xdb], type: 'image/jpeg'},
	{bytes: [0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01], type: 'image/jpeg'},
	{bytes: [0xff, 0xd8, 0xff, 0xee], type: 'image/jpeg'},
	{bytes: [0xff, 0xd8, 0xff, 0xe1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00], type: 'image/jpeg'},
	{bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], type: 'image/png'},
	{bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], type: 'image/gif'},
	{bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], type: 'image/gif'},
	{bytes: [0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50], type: 'image/webp'},
	{bytes: [0x3c, 0x73, 0x76, 0x67], type: 'image/svg+xml'},
	{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'audio/mpeg'},
	{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d], type: 'video/mp4'},
	{bytes: [null, null, null, null, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32], type: 'video/mp4'},
];

let k_static_files = [
	{uri: '/', path: 'index.html', type: 'text/html; charset=UTF-8'},
	{uri: '/style.css', type: 'text/css; charset=UTF-8'},
	{uri: '/favicon.png', type: 'image/png'},
	{uri: '/client.js', type: 'text/javascript; charset=UTF-8'},
	{uri: '/tfrpc.js', type: 'text/javascript; charset=UTF-8', headers: {'Access-Control-Allow-Origin': 'null'}},
	{uri: '/robots.txt', type: 'text/plain; charset=UTF-8'},
];

const k_global_settings = {
	index: {
		type: 'string',
		default_value: '/~core/apps/',
		description: 'Default path.',
	},
	room: {
		type: 'boolean',
		default_value: true,
		description: 'Whether this instance should behave as a room.',
	},
	room_name: {
		type: 'string',
		default_value: 'tilde friends tunnel',
		description: 'Name of the room.',
	},
	code_of_conduct: {
		type: 'textarea',
		default_value: undefined,
		description: 'Code of conduct presented at sign-in.',
	},
	http_redirect: {
		type: 'string',
		default_value: undefined,
		description: 'If connecting by HTTP and HTTPS is configured, Location header prefix (ie, "https://example.com")',
	},
};

let gGlobalSettings = {
	index: "/~core/apps/",
};

let kPingInterval = 60 * 1000;

function printError(out, error) {
	if (error.stackTrace) {
		out.print(error.fileName + ":" + error.lineNumber + ": " + error.message);
		out.print(error.stackTrace);
	} else {
		for (let [k, v] of Object.entries(error)) {
			out.print(k, v);
		}
		out.print(error.toString());
	}
}

function invoke(handlers, argv) {
	let promises = [];
	if (handlers) {
		for (let i = 0; i < handlers.length; ++i) {
			try {
				promises.push(handlers[i](...argv));
			} catch (error) {
				handlers.splice(i, 1);
				i--;
				promises.push(new Promise(function(resolve, reject) { reject(error); }));
			}
		}
	}
	return Promise.all(promises);
}

function broadcastEvent(eventName, argv) {
	let promises = [];
	for (let i in gProcesses) {
		let process = gProcesses[i];
		if (process.eventHandlers[eventName]) {
			promises.push(invoke(process.eventHandlers[eventName], argv));
		}
	}
	return Promise.all(promises);
}

function broadcast(message) {
	let sender = this;
	let promises = [];
	for (let i in gProcesses) {
		let process = gProcesses[i];
		if (process != sender
			&& process.packageOwner == sender.packageOwner
			&& process.packageName == sender.packageName) {
			let from = getUser(process, sender);
			promises.push(postMessageInternal(from, process, message));
		}
	}
	return Promise.all(promises);
}

function getUser(caller, process) {
	return {
		name: process.userName,
		key: process.key,
		index: process.index,
		packageOwner: process.packageOwner,
		packageName: process.packageName,
		credentials: process.credentials,
		postMessage: postMessageInternal.bind(caller, caller, process),
	};
}

function getApps(user, process) {
	if (process.credentials &&
		process.credentials.session &&
		process.credentials.session.name) {
		if (user && user !== process.credentials.session.name && user !== 'core') {
			return {};
		} else if (!user) {
			user = process.credentials.session.name;
		}
	}
	if (user) {
		let db = new Database(user);
		try {
			let names = JSON.parse(db.get('apps'));
			return Object.fromEntries(names.map(name => [name, db.get('path:' + name)]));
		} catch {
		}
	}
	return {};
}

function postMessageInternal(from, to, message) {
	if (to.eventHandlers['message']) {
		return invoke(to.eventHandlers['message'], [getUser(from, from), message]);
	}
}

async function getSessionProcessBlob(blobId, session, options) {
	let actualOptions = {timeout: kPingInterval};
	if (options) {
		for (let i in options) {
			actualOptions[i] = options[i];
		}
	}
	return getProcessBlob(blobId, 'session_' + session, actualOptions);
}

async function getProcessBlob(blobId, key, options) {
	let process = gProcesses[key];
	if (!process
		&& !(options && "create" in options && !options.create)) {
		let resolveReady;
		let rejectReady;
		try {
			print("Creating task for " + blobId + " " + key);
			process = {};
			process.key = key;
			process.index = gProcessIndex++;
			process.userName = 'user' + process.index;
			process.credentials = options.credentials || {};
			process.task = new Task();
			process.eventHandlers = {};
			if (!options?.script || options?.script === 'app.js') {
				process.app = new app.App();
			}
			process.lastActive = Date.now();
			process.lastPing = null;
			process.timeout = options.timeout;
			process.stats = false;
			process.ready = new Promise(function(resolve, reject) {
				resolveReady = resolve;
				rejectReady = reject;
			});
			gProcesses[key] = process;
			process.task.onExit = function(exitCode, terminationSignal) {
				broadcastEvent('onSessionEnd', [getUser(process, process)]);
				process.task = null;
				delete gProcesses[key];
			};
			let imports = {
				'core': {
					'broadcast': broadcast.bind(process),
					'register': function(eventName, handler) {
						if (!process.eventHandlers[eventName]) {
							process.eventHandlers[eventName] = [];
						}
						process.eventHandlers[eventName].push(handler);
					},
					'unregister': function(eventName, handler) {
						if (process.eventHandlers[eventName]) {
							let index = process.eventHandlers[eventName].indexOf(handler);
							if (index != -1) {
								process.eventHandlers[eventName].splice(index, 1);
							}
							if (process.eventHandlers[eventName].length == 0) {
								delete process.eventHandlers[eventName];
							}
						}
					},
					'user': getUser(process, process),
					'users': function() {
						try {
							return JSON.parse(new Database('auth').get('users'));
						} catch {
							return [];
						}
					},
					'permissionsGranted': function() {
						let user = process?.credentials?.session?.name;
						if (user &&
							options?.packageOwner &&
							options?.packageName &&
							gGlobalSettings.userPermissions &&
							gGlobalSettings.userPermissions[user] &&
							gGlobalSettings.userPermissions[user][options.packageOwner]) {
							return gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName];
						}
					},
					'allPermissionsGranted': function() {
						let user = process?.credentials?.session?.name;
						if (user &&
							options?.packageOwner &&
							options?.packageName &&
							gGlobalSettings.userPermissions &&
							gGlobalSettings.userPermissions[user]) {
							return gGlobalSettings.userPermissions[user];
						}
					},
					'permissionsForUser': function(user) {
						return (gGlobalSettings?.permissions ? gGlobalSettings.permissions[user] : []) ?? [];
					},
					'apps': user => getApps(user, process),
					'getSockets': getSockets,
					'permissionTest': function(permission) {
						let user = process?.credentials?.session?.name;
						if (!user || !options?.packageOwner || !options?.packageName) {
							return;
						} else if (gGlobalSettings.userPermissions &&
							gGlobalSettings.userPermissions[user] &&
							gGlobalSettings.userPermissions[user][options.packageOwner] &&
							gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName] &&
							gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName][permission] !== undefined) {
							if (gGlobalSettings.userPermissions[user][options.packageOwner][options.packageName][permission]) {
								return true;
							} else {
								throw Error(`Permission denied: ${permission}.`);
							}

						} else if (process.app) {
							return process.app.makeFunction(['requestPermission'])(permission).then(function(value) {
								if (value == 'allow') {
									storePermission(user, options.packageOwner, options.packageName, permission, true);
									process.sendPermissions();
									return true;
								} else if (value == 'allow once') {
									return true;
								} else if (value == 'deny') {
									storePermission(user, options.packageOwner, options.packageName, permission, false);
									process.sendPermissions();
									throw Error(`Permission denied: ${permission}.`);
								} else if (value == 'deny once') {
									throw Error(`Permission denied: ${permission}.`);
								}
								throw Error(`Permission denied: ${permission}.`);
							});
						} else {
							throw Error(`Permission denied: ${permission}.`);
						}
					},
				}
			};
			if (process.credentials?.permissions?.administration) {
				imports.core.globalSettingsDescriptions = function() {
					let settings = Object.assign({}, k_global_settings);
					for (let [key, value] of Object.entries(gGlobalSettings)) {
						if (settings[key]) {
							settings[key].value = value;
						}
					}
					return settings;
				};
				imports.core.globalSettingsGet = function(key) {
					return gGlobalSettings[key];
				};
				imports.core.globalSettingsSet = function(key, value) {
					print('Setting', key, value);
					gGlobalSettings[key] = value;
					setGlobalSettings(gGlobalSettings);
					print('Done.');
				};
				imports.core.deleteUser = function(user) {
					return imports.core.permissionTest('delete_user').then(function() {
						let db = new Database('auth');

						db.remove('user:' + user);

						let users = new Set();
						let users_original = db.get('users');
						try {
							users = new Set(JSON.parse(users_original));
						} catch {
						}
						users.delete(user);
						users = JSON.stringify([...users].sort());
						if (users !== users_original) {
							db.set('users', users);
						}
					});
				};
			}
			if (options.api) {
				imports.app = {};
				for (let i in options.api) {
					let api = options.api[i];
					imports.app[api[0]] = process.app.makeFunction(api);
				}
			}
			for (let [name, f] of Object.entries(options?.imports || {})) {
				imports[name] = f;
			}
			process.task.onPrint = function(args) {
				if (imports.app) {
					imports.app.print(...args);
				}
			};
			process.task.onError = function(error) {
				try {
					if (process.app) {
						process.app.makeFunction(['error'])(error);
					} else {
						printError({print: print}, error);
					}
				} catch (e) {
					printError({print: print}, error);
				}
			};
			imports.ssb = Object.fromEntries(Object.keys(ssb).map(key => [key, ssb[key].bind(ssb)]));
			imports.ssb.createIdentity = function() {
				if (process.credentials &&
					process.credentials.session &&
					process.credentials.session.name) {
					return ssb.createIdentity(process.credentials.session.name);
				}
			};
			imports.ssb.getIdentities = function() {
				if (process.credentials &&
					process.credentials.session &&
					process.credentials.session.name) {
					return ssb.getIdentities(process.credentials.session.name);
				}
			};
			imports.ssb.appendMessageWithIdentity = function(id, message) {
				if (process.credentials &&
					process.credentials.session &&
					process.credentials.session.name) {
					return imports.core.permissionTest('ssb_append').then(function() {
						return ssb.appendMessageWithIdentity(process.credentials.session.name, id, message);
					});
				}
			};
			imports.ssb.privateMessageEncrypt = function(id, recipients, message) {
				if (process.credentials &&
					process.credentials.session &&
					process.credentials.session.name) {
					return ssb.privateMessageEncrypt(process.credentials.session.name, id, recipients, message);
				}
			};
			imports.ssb.privateMessageDecrypt = function(id, message) {
				if (process.credentials &&
					process.credentials.session &&
					process.credentials.session.name) {
					return ssb.privateMessageDecrypt(process.credentials.session.name, id, message);
				}
			};

			if (process.credentials &&
				process.credentials.session &&
				process.credentials.session.name) {
				imports.database = function(key) {
					let db = new Database(process.credentials.session.name + ':' + key);
					return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)]));
				};
				imports.my_shared_database = function(packageName, key) {
					let db = new Database(':shared:' + process.credentials.session.name + ':' + packageName + ':' + key);
					return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)]));
				};
				imports.databases = function() {
					return [].concat(databases.list(':shared:' + process.credentials.session.name + ':%'), databases.list(process.credentials.session.name + ':%'));
				};
			}
			if (options.packageOwner && options.packageName) {
				imports.shared_database = function(key) {
					let db = new Database(':shared:' + options.packageOwner + ':' + options.packageName + ':' + key);
					return Object.fromEntries(Object.keys(db).map(x => [x, db[x].bind(db)]));
				}
			}
			process.sendPermissions = function sendPermissions() {
				process.app.send({action: 'permissions', permissions: imports.core.permissionsGranted()});
			}
			process.resetPermission = function resetPermission(permission) {
				let user = process?.credentials?.session?.name;
				storePermission(user, options?.packageOwner, options?.packageName, permission, undefined);
				process.sendPermissions();
			}
			process.task.setImports(imports);
			process.task.activate();
			let source = await getBlobOrContent(blobId);
			let appSourceName = blobId;
			let appSource = utf8Decode(source);
			try {
				let appObject = JSON.parse(appSource);
				if (appObject.type == "tildefriends-app") {
					appSourceName = options?.script ?? 'app.js';
					let id = appObject.files[appSourceName];
					let blob = await getBlobOrContent(id);
					appSource = utf8Decode(blob);
					await process.task.loadFile(['/tfrpc.js', await File.readFile('core/tfrpc.js')]);
					await Promise.all(Object.keys(appObject.files).map(async function(f) {
						await process.task.loadFile([f, await getBlobOrContent(appObject.files[f])]);
					}));
				}
			} catch (e) {
				printError({print: print}, e);
			}
			broadcastEvent('onSessionBegin', [getUser(process, process)]);
			if (process.app) {
				process.app.send({action: "ready", version: version()});
				process.sendPermissions();
			}
			await process.task.execute({name: appSourceName, source: appSource});
			resolveReady(process);
		} catch (error) {
			if (process.app) {
				if (process?.task?.onError) {
					process.task.onError(error);
				} else {
					printError({print: print}, error);
				}
			} else {
				printError({print: print}, error);
			}
			rejectReady(error);
		}
	}
	return process;
}

function setGlobalSettings(settings) {
	gGlobalSettings = settings;
	try {
		return new Database('core').set('settings', JSON.stringify(settings));
	} catch (error) {
		print('Error storing settings:', error);
	}
}

function startsWithBytes(data, bytes) {
	if (data.byteLength >= bytes.length) {
		let dataBytes = new Uint8Array(data.slice(0, bytes.length));
		for (let i = 0; i < bytes.length; i++) {
			if (dataBytes[i] !== bytes[i] && bytes[i] !== null) {
				return;
			}
		}
		return true;
	}
}

async function staticFileHandler(request, response, blobId, uri) {
	for (let i in k_static_files) {
		if (uri === k_static_files[i].uri) {
			let path = k_static_files[i].path || uri.substring(1);
			let type = k_static_files[i].type || guessTypeFromName(path);

			let stat = await File.stat('core/' + path);
			let id = `${stat.mtime}_${stat.size}`;

			if (request.headers['if-none-match'] === '"' + id + '"') {
				response.writeHead(304, {'Content-Length': '0'});
				response.end();
			} else {
				let data = await File.readFile('core/' + path);
				response.writeHead(200, Object.assign(
					{
						'Content-Type': type,
						'Content-Length': data.byteLength,
						'etag': '"' + id + '"',
					},
					k_static_files[i].headers || {}));
				response.end(data);
			}
			return;
		}
	}

	response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
	response.end("File not found");
}

async function staticDirectoryHandler(request, response, directory, uri) {
	let filename = uri || 'index.html';
	if (filename.indexOf('..') != -1) {
		response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
		response.end("File not found");
		return;
	}

	try {
		let stat = await File.stat(directory + filename);
		let id = `${stat.mtime}_${stat.size}`;

		if (request.headers['if-none-match'] === '"' + id + '"') {
			response.writeHead(304, {'Content-Length': '0'});
			response.end();
		} else {
			let data = await File.readFile(directory + filename);
			response.writeHead(200, {
				'Content-Type': k_mime_types[filename.split('.').pop()] || 'text/plain',
				'Content-Length': data.byteLength,
				'etag': '"' + id + '"',
			});
			response.end(data);
		}
	} catch {
		response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length});
		response.end("File not found");
	}
}

async function wellKnownHandler(request, response, path) {
	let data = await File.readFile("data/global/.well-known/" + path);
	if (data) {
		response.writeHead(200, {"Content-Type": "text/plain", "Content-Length": data.length});
		response.end(data);
	} else {
		response.writeHead(404, {"Content-Type": "text/plain", "Content-Length": "File not found".length});
		response.end("File not found");
	}
}

function guessTypeFromName(path) {
	let extension = path.split('.').pop();
	return k_mime_types[extension];
}

function guessTypeFromMagicBytes(data) {
	for (let magic of k_magic_bytes) {
		if (startsWithBytes(data, magic.bytes)) {
			return magic.type;
		}
	}
}

function sendData(response, data, type, headers) {
	if (data) {
		response.writeHead(200, Object.assign({"Content-Type": type || guessTypeFromMagicBytes(data) || "application/binary", "Content-Length": data.byteLength}, headers || {}));
		response.end(data);
	} else {
		response.writeHead(404, Object.assign({"Content-Type": "text/plain; charset=utf-8", "Content-Length": "File not found".length}, headers || {}));
		response.end("File not found");
	}
}

async function getBlobOrContent(id) {
	if (!id) {
		return;
	} else if (id.startsWith('&')) {
		return ssb.blobGet(id);
	} else if (id.startsWith('%')) {
		return ssb.messageContentGet(id);
	}
}

let g_handler_index = 0;
async function useAppHandler(response, handler_blob_id, path) {
	let do_resolve;
	let promise = new Promise(async function(resolve, reject) {
		do_resolve = resolve;
	});
	let process;
	let result;
	try {
		process = await getProcessBlob(handler_blob_id, 'handler_' + g_handler_index++, {
			script: 'handler.js',
			imports: {
				request: {
					path: path,
				},
				respond: do_resolve,
			},
		});
		await process.ready;

		result = await promise;
	} finally {
		if (process?.task) {
			await process.task.kill();
		}
	}
	return result;
}

async function blobHandler(request, response, blobId, uri) {
	for (let i in k_static_files) {
		if (uri === k_static_files[i].uri && k_static_files[i].path) {
			let stat = await File.stat('core/' + k_static_files[i].path);
			let id = `${stat.mtime}_${stat.size}`;

			if (request.headers['if-none-match'] === '"' + id + '"') {
				response.writeHead(304, {'Content-Length': '0'});
				response.end();
			} else {
				let data = await File.readFile('core/' + k_static_files[i].path);
				response.writeHead(200, Object.assign(
					{
						'Content-Type': k_static_files[i].type,
						'Content-Length': data.byteLength,
						'etag': '"' + id + '"',
					},
					k_static_files[i].headers || {}));
				response.end(data);
			}
			return;
		}
	}

	if (!uri) {
		response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + blobId + '/', "Content-Length": "0"});
		response.end(data);
		return;
	}

	let process;
	if (uri == "/view") {
		let data;
		let match;
		let query = form.decodeForm(request.query);
		let headers = {
			'Content-Security-Policy': 'sandbox',
		};
		if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) {
			headers['Content-Disposition'] = `attachment; filename=${query.filename}`;
		}
		if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
			let id = await new Database(match[1]).get('path:' + match[2]);
			if (id) {
				if (request.headers['if-none-match'] === '"' + id + '"') {
					headers['Content-Length'] = '0';
					response.writeHead(304, headers);
					response.end();
				} else {
					data = await getBlobOrContent(id);
					if (match[3]) {
						let appObject = JSON.parse(data);
						data = appObject.files[match[3]];
					}
					sendData(response, data, undefined, Object.assign({etag: '"' + id + '"'}, headers));
				}
			} else {
				if (request.headers['if-none-match'] === '"' + blobId + '"') {
					headers['Content-Length'] = '0';
					response.writeHead(304, headers);
					response.end();
				} else {
					sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
				}
			}
		} else {
			if (request.headers['if-none-match'] === '"' + blobId + '"') {
				headers['Content-Length'] = '0';
				response.writeHead(304, headers);
				response.end();
			} else {
				data = await getBlobOrContent(blobId);
				sendData(response, data, undefined, Object.assign({etag: '"' + blobId + '"'}, headers));
			}
		}
	} else if (uri == "/save") {
		let match;
		if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
			let newBlobId = await ssb.blobStore(request.body);
			let user = match[1];
			let appName = match[2];
			let credentials = auth.query(request.headers);
			if (credentials && credentials.session &&
				(credentials.session.name == user ||
				(credentials.permissions.administration && user == 'core'))) {
				let database = new Database(user);
				let apps = new Set();
				let apps_original = database.get('apps');
				try {
					apps = new Set(JSON.parse(apps_original));
				} catch {
				}
				if (!apps.has(appName)) {
					apps.add(appName);
				}
				apps = JSON.stringify([...apps].sort());
				if (apps != apps_original) {
					database.set('apps', apps);
				}
				database.set('path:' + appName, newBlobId);
			} else {
				response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"});
				response.end("401 Unauthorized");
				return;
			}

			response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
			response.end("/" + newBlobId);
		} else if (blobId === '') {
			let newBlobId = await ssb.blobStore(request.body);
			response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
			response.end("/" + newBlobId);
		} else {
			response.writeHead(400, {"Content-Type": "text/plain; charset=utf-8"});
			response.end('Invalid name.');
		}

	} else if (uri == "/delete") {
		let match;
		if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
			let user = match[1];
			let appName = match[2];
			let credentials = auth.query(request.headers);
			if (credentials && credentials.session &&
				(credentials.session.name == user ||
				(credentials.permissions.administration && user == 'core'))) {
				let database = new Database(user);
				let apps = new Set();
				try {
					apps = new Set(JSON.parse(database.get('apps')));
				} catch {
				}
				if (apps.delete(appName)) {
					database.set('apps', JSON.stringify([...apps].sort()));
				}
				database.remove('path:' + appName);
			} else {
				response.writeHead(401, {"Content-Type": "text/plain; charset=utf-8"});
				response.end("401 Unauthorized");
				return;
			}
		}

		response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8"});
		response.end('OK');
	} else {
		let data;
		let match;
		let id;
		let app_id = blobId;
		if (match = /^\/\~(\w+)\/(\w+)$/.exec(blobId)) {
			let db = new Database(match[1]);
			app_id = await db.get('path:' + match[2]);
		}

		let app_object = JSON.parse(utf8Decode(await getBlobOrContent(app_id)));
		id = app_object.files[uri.substring(1)];
		if (!id && app_object.files['handler.js']) {
			let answer;
			try {
				answer = await useAppHandler(response, app_id, uri.substring(1));
			} catch (error) {
				data = utf8Encode(`Internal Server Error\n\n${error?.message}\n${error?.stack}`);
				response.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': data.length});
				response.end(data);
				return;
			}
			if (answer && typeof answer.data == 'string') {
				answer.data = utf8Encode(answer.data);
			}
			sendData(response, answer?.data, answer?.content_type, {
				'Access-Control-Allow-Origin': '*',
				'Content-Security-Policy': 'sandbox',
			});
		} else if (id) {
			if (request.headers['if-none-match'] && request.headers['if-none-match'] == '"' + id + '"') {
				let headers = {
					'Access-Control-Allow-Origin': '*',
					'Content-Security-Policy': 'sandbox',
					'Content-Length': '0',
				};
				response.writeHead(304, headers);
				response.end();
			} else {
				let headers = {
					'ETag': '"' + id + '"',
					'Access-Control-Allow-Origin': '*',
					'Content-Security-Policy': 'sandbox',
				};
				data = await getBlobOrContent(id);
				let type = guessTypeFromName(uri) || guessTypeFromMagicBytes(data);
				sendData(response, data, type, headers);
			}
		} else {
			sendData(response, data, undefined, {});
		}
	}
}

ssb.addEventListener('broadcasts', function() {
	broadcastEvent('onBroadcastsChanged', []);
});

ssb.addEventListener('connections', function() {
	broadcastEvent('onConnectionsChanged', []);
});

async function loadSettings() {
	let data = {};
	try {
		let settings = new Database('core').get('settings');
		if (settings) {
			data = JSON.parse(settings);
		}
	} catch (error) {
		print("Settings not found in database:", error);
	}
	for (let [key, value] of Object.entries(k_global_settings)) {
		if (data[key] === undefined) {
			data[key] = value.default_value;
		}
	}
	gGlobalSettings = data;
}

function sendStats() {
	let apps = Object.values(gProcesses).filter(process => process.app && process.stats).map(process => process.app);
	if (apps.length) {
		let stats = getStats();
		for (let app of apps) {
			app.send({action: 'stats', stats: stats});
		}
		setTimeout(sendStats, 1000);
	} else {
		gStatsTimer = false;
	}
}

function enableStats(process, enabled) {
	process.stats = enabled;
	if (!gStatsTimer) {
		gStatsTimer = true;
		sendStats();
	}
}

function stringResponse(response, data) {
	let bytes = utf8Encode(data);
	response.writeHead(200, {
		"Content-Type": "application/json; charset=utf-8",
		"Content-Length": bytes.byteLength.toString(),
		"Access-Control-Allow-Origin": "*",
	});
	return response.end(bytes);
}

loadSettings().then(function() {
	httpd.all("/login", auth.handler);
	httpd.all("", function(request, response) {
		let match;
		if (request.uri === "/" || request.uri === "") {
			response.writeHead(303, {"Location": (request.client.tls ? 'https://' : 'http://') + request.headers.host + gGlobalSettings.index, "Content-Length": "0"});
			return response.end();
		} else if (match = /^(\/~[^\/]+\/[^\/]+)(\/?.*)$/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/([&\%][^\.]{44}(?:\.\w+)?)(\/?.*)/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/static\/lit\/([\.\w-/]*)$/.exec(request.uri)) {
			return staticDirectoryHandler(request, response, 'deps/lit/', match[1]);
		} else if (match = /^\/codemirror\/([\.\w-/]*)$/.exec(request.uri)) {
			return staticDirectoryHandler(request, response, 'deps/codemirror/', match[1]);
		} else if (match = /^\/speedscope\/([\.\w-/]*)$/.exec(request.uri)) {
			return staticDirectoryHandler(request, response, 'deps/speedscope/', match[1]);
		} else if (match = /^\/split\/([\.\w-/]*)$/.exec(request.uri)) {
			return staticDirectoryHandler(request, response, 'deps/split/', match[1]);
		} else if (match = /^\/smoothie\/([\.\w-/]*)$/.exec(request.uri)) {
			return staticDirectoryHandler(request, response, 'deps/smoothie/', match[1]);
		} else if (match = /^\/static(\/.*)/.exec(request.uri)) {
			return staticFileHandler(request, response, null, match[1]);
		} else if (request.uri == "/robots.txt") {
			return staticFileHandler(request, response, null, request.uri);
		} else if (match = /^(.*)(\/(?:save|delete)?)$/.exec(request.uri)) {
			return blobHandler(request, response, match[1], match[2]);
		} else if (match = /^\/trace$/.exec(request.uri)) {
			return stringResponse(response, trace());
		} else if (match = /^\/disconnections$/.exec(request.uri)) {
			return stringResponse(response, JSON.stringify(disconnectionsDebug(), null, 2));
		} else if (match = /^\/debug$/.exec(request.uri)) {
			return stringResponse(response, JSON.stringify(getDebug(), null, 2));
		} else if (match = /^\/hitches$/.exec(request.uri)) {
			return stringResponse(response, JSON.stringify(getHitches(), null, 2));
		} else if (match = /^\/mem$/.exec(request.uri)) {
			return stringResponse(response, JSON.stringify(getAllocations(), null, 2));
		} else if ((match = /^\/.well-known\/(.*)/.exec(request.uri)) && request.uri.indexOf("..") == -1) {
			return wellKnownHandler(request, response, match[1]);
		} else {
			let data = "File not found.";
			response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Content-Length": data.length.toString()});
			return response.end(data);
		}
	});
	httpd.registerSocketHandler("/app/socket", app.socket);
}).catch(function(error) {
	print('Failed to load settings.');
	printError({print: print}, error);
	exit(1);
});

function storePermission(user, packageOwner, packageName, permission, allow) {
	if (!gGlobalSettings.userPermissions) {
		gGlobalSettings.userPermissions = {};
	}
	if (!gGlobalSettings.userPermissions[user]) {
		gGlobalSettings.userPermissions[user] = {};
	}
	if (!gGlobalSettings.userPermissions[user][packageOwner]) {
		gGlobalSettings.userPermissions[user][packageOwner] = {};
	}
	if (!gGlobalSettings.userPermissions[user][packageOwner][packageName]) {
		gGlobalSettings.userPermissions[user][packageOwner][packageName] = {};
	}
	if (gGlobalSettings.userPermissions[user][packageOwner][packageName][permission] !== allow) {
		if (allow === undefined) {
			delete gGlobalSettings.userPermissions[user][packageOwner][packageName][permission];
		} else {
			gGlobalSettings.userPermissions[user][packageOwner][packageName][permission] = allow;
		}
		setGlobalSettings(gGlobalSettings);
	}
}

export {
	gGlobalSettings as globalSettings,
	setGlobalSettings,
	enableStats,
	invoke,
	getSessionProcessBlob,
};
