import assert from 'node:assert';
import AuditSource from './auditsource.js';
import constants from './constants.js';
import logger from './logger.js';
import eventlog from './eventlog.js';
import express from 'express';
import http from 'node:http';
import { HttpError } from '@cloudron/connect-lastmile';
import middleware from './middleware/index.js';
import platform from './platform.js';
import routes from './routes/index.js';
import safe from 'safetydance';
import users from './users.js';
import util from 'node:util';
import { WebSocketServer } from 'ws';

const { log } = logger('server');


let gHttpServer = null;

function notFoundHandler(req, res, next) {
    const cleanUrl = req.url.replace(/(access_token=)[^&]+/, '$1' + '<redacted>');
    log(`no such route: ${req.method} ${cleanUrl}`);
    return next(new HttpError(404, 'No such route'));
}

async function initializeExpressSync() {
    const app = express();
    // disable slowloris prevention: https://github.com/nodejs/node/issues/47421 . duplicate headers are discarded for the standard headers (https://nodejs.org/api/http.html#messageheaders)
    const httpServer = http.createServer({ headersTimeout: 0, requestTimeout: 0, joinDuplicateHeaders: false }, app); // see also nginx client_header_timeout (30s)

    const wsServer = new WebSocketServer({ noServer: true }); // in noServer mode, we have to handle 'upgrade' and call handleUpgrade

    const REQUEST_TIMEOUT = 60000; // timeout for all requests (see also setTimeout on the httpServer)

    // NOTE: routes that use multi-part have to be whitelisted in the reverse proxy
    const FILE_SIZE_LIMIT = '256mb', // max file size that can be uploaded (see also client_max_body_size in nginx)
        FILE_TIMEOUT = 60 * 1000; // increased timeout for file uploads (1 min)

    const QUERY_LIMIT = '2mb', // max size for json queries (see also client_max_body_size in nginx)
        FIELD_LIMIT = 2 * 1024 * 1024; // max fields that can appear in multipart

    const multipart = middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT }, true); // forces multipart content-type
    const json = middleware.json({ strict: true, limit: QUERY_LIMIT }, true); // forces json content-type
    const jsonOrMultipart = [
        middleware.json({ strict: true, limit: QUERY_LIMIT }, false),
        middleware.multipart({ maxFieldsSize: FIELD_LIMIT, limit: FILE_SIZE_LIMIT, timeout: FILE_TIMEOUT }, false)
    ];

    app.set('json spaces', 2); // pretty json
    app.enable('trust proxy'); // trust the X-Forwarded-* headers
    app.set('query parser', 'simple'); // uses the built-in querystring module for query parsing. req.query always has strings or array of strings

    const router = new express.Router();
    router.del = router.delete; // amend router.del for readability further on

    app
        // the timeout middleware will respond with a 503. the request itself cannot be 'aborted' and will continue
        // search for req.clearTimeout in route handlers to see places where this timeout is reset
        .use(middleware.timeout(REQUEST_TIMEOUT, { respond: true }))
        .use((req, res , next) => {
            // we store our route resources, like app,volumes,... in req.resources. Those are added in the load() routes
            req.resources = {};
            next();
        })
        .use(router)
        .use(notFoundHandler)
        .use(middleware.lastMile());

    // authentication
    const password = routes.accesscontrol.passwordAuth;
    const token = routes.accesscontrol.tokenAuth;

    // authorization
    const authorizeOwner = routes.accesscontrol.authorize(users.ROLE_OWNER);
    const authorizeAdmin = routes.accesscontrol.authorize(users.ROLE_ADMIN);
    const authorizeOperator = routes.accesscontrol.authorizeOperator;
    const authorizeUserManager = routes.accesscontrol.authorize(users.ROLE_USER_MANAGER);
    const authorizeMailManager = routes.accesscontrol.authorize(users.ROLE_MAIL_MANAGER);
    const authorizeUser = routes.accesscontrol.authorize(users.ROLE_USER);

    // boostrapping/provision routes
    const verifyUnprovisioned = routes.provision.verifyUnprovisioned;
    router.post('/api/v1/provision/setup',    json, verifyUnprovisioned, routes.provision.providerTokenAuth, routes.provision.setup);
    router.post('/api/v1/provision/restore',  json, verifyUnprovisioned, routes.provision.restore);
    router.post('/api/v1/provision/activate', json, verifyUnprovisioned, routes.provision.activate);
    router.get ('/api/v1/provision/block_devices',  verifyUnprovisioned, routes.provision.getBlockDevices);
    router.post('/api/v1/provision/detect_ip',      verifyUnprovisioned, routes.provision.detectIP);
    router.get ('/api/v1/provision/status',         routes.provision.getStatus);

    // auth routes
    router.post('/api/v1/auth/login',                  json, password, routes.auth.login);
    router.get ('/api/v1/auth/logout',                       token,    routes.auth.logout); // this will invalidate the token if any and redirect to / always
    router.post('/api/v1/auth/password_reset_request', json,           routes.auth.passwordResetRequest);
    router.post('/api/v1/auth/password_reset',         json,           routes.auth.passwordReset);
    router.post('/api/v1/auth/setup_account',          json,           routes.auth.setupAccount);

    router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus); // healthcheck
    router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // public route for dashboard, email
    router.get ('/api/v1/cloudron/background', routes.branding.getCloudronBackground); // public route for login

    // cloudron routes
    router.get ('/api/v1/cloudron/languages',                                 routes.cloudron.listLanguages);
    router.get ('/api/v1/cloudron/language',           token, authorizeAdmin, routes.cloudron.getLanguage);
    router.post('/api/v1/cloudron/language',     json, token, authorizeAdmin, routes.cloudron.setLanguage);
    router.get ('/api/v1/cloudron/time_zone',          token, authorizeAdmin, routes.cloudron.getTimeZone);
    router.post('/api/v1/cloudron/time_zone',    json, token, authorizeAdmin, routes.cloudron.setTimeZone);

    // config route for dashboard that any auth user (not just admin) can access
    router.get ('/api/v1/dashboard/config',                 token, authorizeUser, routes.dashboard.getConfig);
    router.post('/api/v1/dashboard/prepare_location', json, token, authorizeAdmin, routes.dashboard.startPrepareLocation);
    router.post('/api/v1/dashboard/location',         json, token, authorizeAdmin, routes.dashboard.changeLocation);

    // system (vm/server)
    router.get ('/api/v1/system/info',                           token, authorizeAdmin, routes.system.getInfo); // vendor, product name etc
    router.post('/api/v1/system/reboot',                   json, token, authorizeAdmin, routes.system.reboot);
    router.get ('/api/v1/system/metrics',                        token, authorizeAdmin, routes.system.getMetrics);
    router.get ('/api/v1/system/metricstream',                   token, authorizeAdmin, routes.system.getMetricStream);
    router.get ('/api/v1/system/block_devices',                  token, authorizeAdmin, routes.system.getBlockDevices);
    router.get ('/api/v1/system/filesystems',                    token, authorizeAdmin, routes.system.getFilesystems);
    router.get ('/api/v1/system/filesystem_usage',               token, authorizeAdmin, routes.system.getFilesystemUsage);
    router.get ('/api/v1/system/logs/:unit',                     token, authorizeAdmin, routes.system.getLogs);
    router.get ('/api/v1/system/logstream/:unit',                token, authorizeAdmin, routes.system.getLogStream);
    // app operators require cpu and memory info for the Resources UI
    router.get ('/api/v1/system/cpus',                           token, authorizeUser, routes.system.getCpus);
    router.get ('/api/v1/system/memory',                         token, authorizeUser, routes.system.getMemory);

    // eventlog
    router.get ('/api/v1/eventlog',                                token, authorizeAdmin, routes.eventlog.list);
    router.get ('/api/v1/eventlog/:eventId',                       token, authorizeAdmin, routes.eventlog.get);

    // updater
    router.get ('/api/v1/updater/box_update',                     token,  authorizeUser, routes.updater.getBoxUpdate); // allowed for normal users to make it work for app operators
    router.post('/api/v1/updater/box_update',               json, token, authorizeAdmin, routes.updater.updateBox);
    router.post('/api/v1/updater/check_box_update',     json, token, authorizeAdmin, routes.updater.checkBoxUpdate);
    router.get ('/api/v1/updater/autoupdate_config',              token, authorizeAdmin, routes.updater.getAutoupdateConfig);
    router.post('/api/v1/updater/autoupdate_config',        json, token, authorizeAdmin, routes.updater.setAutoupdateConfig);

    // task routes
    router.get ('/api/v1/tasks',                         token, authorizeAdmin, routes.tasks.list);
    router.get ('/api/v1/tasks/:taskId',                 token, authorizeAdmin, routes.tasks.load, routes.tasks.get);
    router.get ('/api/v1/tasks/:taskId/logs',            token, authorizeAdmin, routes.tasks.load, routes.tasks.getLogs);
    router.get ('/api/v1/tasks/:taskId/logstream',       token, authorizeAdmin, routes.tasks.load, routes.tasks.getLogStream);
    router.post('/api/v1/tasks/:taskId/stop',      json, token, authorizeAdmin, routes.tasks.load, routes.tasks.stopTask);

    // notification routes (these are server level)
    router.get ('/api/v1/notifications',                       token, authorizeAdmin, routes.notifications.list);
    router.get ('/api/v1/notifications/:notificationId',       token, authorizeAdmin, routes.notifications.load, routes.notifications.get);
    router.post('/api/v1/notifications/:notificationId', json, token, authorizeAdmin, routes.notifications.load, routes.notifications.update);

    // backup routes
    router.get ('/api/v1/backups',                           token, authorizeAdmin, routes.backups.list);
    router.get ('/api/v1/backups/:id',                       token, authorizeAdmin, routes.backups.load, routes.backups.get);
    router.post('/api/v1/backups/:id',                 json, token, authorizeAdmin, routes.backups.load, routes.backups.update);
    router.post('/api/v1/backups/:id/start_integrity_check', json, token, authorizeAdmin, routes.backups.load, routes.backups.startIntegrityCheck);
    router.post('/api/v1/backups/:id/stop_integrity_check',  json, token, authorizeAdmin, routes.backups.load, routes.backups.stopIntegrityCheck);

    // backup site (destination) routes
    router.get ('/api/v1/backup_sites/',                                token, authorizeAdmin, routes.backupSites.list);
    router.get ('/api/v1/backup_sites/:id',                             token, authorizeAdmin, routes.backupSites.load, routes.backupSites.get);
    router.post('/api/v1/backup_sites',                           json, token, authorizeOwner, routes.backupSites.add);
    router.del ('/api/v1/backup_sites/:id',                             token, authorizeOwner, routes.backupSites.load, routes.backupSites.del);
    router.get ('/api/v1/backup_sites/:id/status',                      token, authorizeAdmin, routes.backupSites.load, routes.backupSites.getStatus);
    router.get ('/api/v1/backup_sites/:id/backups',                     token, authorizeAdmin, routes.backupSites.load, routes.backupSites.listBackups);
    router.post('/api/v1/backup_sites/:id/create_backup',               token, authorizeAdmin, routes.backupSites.load, routes.backupSites.createBackup);
    router.post('/api/v1/backup_sites/:id/cleanup',               json, token, authorizeAdmin, routes.backupSites.load, routes.backupSites.cleanup);
    router.post('/api/v1/backup_sites/:id/remount',               json, token, authorizeAdmin, routes.backupSites.load, routes.backupSites.remount);
    router.post('/api/v1/backup_sites/:id/configure/name',        json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setName);
    router.post('/api/v1/backup_sites/:id/configure/config',      json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setConfig);
    router.post('/api/v1/backup_sites/:id/configure/limits',      json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setLimits);
    router.post('/api/v1/backup_sites/:id/configure/schedule',    json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setSchedule);
    router.post('/api/v1/backup_sites/:id/configure/retention',   json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setRetention);
    router.post('/api/v1/backup_sites/:id/configure/encryption',  json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setEncryption);
    router.post('/api/v1/backup_sites/:id/configure/contents',    json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setContents);
    router.post('/api/v1/backup_sites/:id/configure/enable_for_updates', json, token, authorizeOwner, routes.backupSites.load, routes.backupSites.setEnabledForUpdates);

    // app archive routes
    router.get ('/api/v1/archives',                     token, authorizeAdmin, routes.archives.list);
    router.get ('/api/v1/archives/:id',                 token, authorizeAdmin, routes.archives.load, routes.archives.get);
    router.del ('/api/v1/archives/:id',                 token, authorizeAdmin, routes.archives.load, routes.archives.del);
    router.post('/api/v1/archives/:id/unarchive', json, token, authorizeAdmin, routes.archives.load, routes.archives.unarchive);
    router.get ('/api/v1/archives/:id/icon',                                   routes.archives.load, routes.archives.getIcon);

    // working off the user behind the provided token
    router.get ('/api/v1/profile',                                       token, authorizeUser, routes.profile.get);
    router.post('/api/v1/profile/display_name',                    json, token, authorizeUser, routes.profile.canEditProfile, routes.profile.setDisplayName);
    router.post('/api/v1/profile/email',                           json, token, authorizeUser, routes.profile.canEditProfile, routes.users.verifyPassword, routes.profile.setEmail);
    router.post('/api/v1/profile/fallback_email',                  json, token, authorizeUser, routes.profile.canEditProfile, routes.users.verifyPassword, routes.profile.setFallbackEmail);
    router.post('/api/v1/profile/language',                        json, token, authorizeUser, routes.profile.setLanguage);
    router.get ('/api/v1/profile/avatar/:identifier',                                          routes.profile.getAvatarById); // this is not scoped so it can used directly in img tag
    router.post('/api/v1/profile/avatar',                                token, authorizeUser, routes.profile.canEditProfile, multipart, routes.profile.setAvatar);
    router.del ('/api/v1/profile/avatar',                                token, authorizeUser, routes.profile.canEditProfile, routes.profile.unsetAvatar);
    router.get ('/api/v1/profile/background_image',                      token, authorizeUser, routes.profile.getBackgroundImage);
    router.post('/api/v1/profile/background_image',                      token, authorizeUser, multipart, routes.profile.setBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked
    router.del ('/api/v1/profile/background_image',                      token, authorizeUser,            routes.profile.unsetBackgroundImage); // backgroundImage is not exposed in LDAP. so it's personal and not locked
    router.post('/api/v1/profile/password',                        json, token, authorizeUser, routes.users.verifyPassword, routes.profile.setPassword);
    router.post('/api/v1/profile/totp_secret',                     json, token, authorizeUser, routes.profile.setTotpSecret);
    router.post('/api/v1/profile/totp_enable',                     json, token, authorizeUser, routes.profile.enableTotp);
    router.post('/api/v1/profile/totp_disable',                    json, token, authorizeUser, routes.users.verifyPassword, routes.profile.disableTotp);
    router.post('/api/v1/profile/notification_config',             json, token, authorizeUser, routes.profile.setNotificationConfig); // non-admins cannot get notifications anyway
    router.del ('/api/v1/profile/sessions',                              token, authorizeUser, routes.profile.destroyUserSession);

    // passkey routes (singular - only one passkey per user)
    router.post('/api/v1/profile/passkey/register/options',       json, token, authorizeUser, routes.profile.getPasskeyRegistrationOptions);
    router.post('/api/v1/profile/passkey/register',               json, token, authorizeUser, routes.profile.registerPasskey);
    router.post('/api/v1/profile/passkey/disable',                json, token, authorizeUser, routes.users.verifyPassword, routes.profile.deletePasskey);

    // app password routes
    router.get ('/api/v1/app_passwords',           token, authorizeUser, routes.appPasswords.list);
    router.post('/api/v1/app_passwords',     json, token, authorizeUser, routes.appPasswords.add);
    router.get ('/api/v1/app_passwords/:id',       token, authorizeUser, routes.appPasswords.get);
    router.del ('/api/v1/app_passwords/:id',       token, authorizeUser, routes.appPasswords.del);

    // access tokens
    router.get ('/api/v1/tokens',           token, authorizeUser, routes.tokens.list);
    router.post('/api/v1/tokens',     json, token, authorizeUser, routes.tokens.add);
    router.get ('/api/v1/tokens/:id',       token, authorizeUser, routes.tokens.verifyOwnership, routes.tokens.get);
    router.del ('/api/v1/tokens/:id',       token, authorizeUser, routes.tokens.verifyOwnership, routes.tokens.del);

    // user routes
    router.get ('/api/v1/users',                             token, authorizeUserManager, routes.users.list);
    router.post('/api/v1/users',                       json, token, authorizeUserManager, routes.users.add);
    router.get ('/api/v1/users/:userId',                     token, authorizeUserManager, routes.users.load, routes.users.get); // this is manage scope because it returns non-restricted fields
    router.del ('/api/v1/users/:userId',                     token, authorizeUserManager, routes.users.load, routes.users.del);
    router.put ('/api/v1/users/:userId/role',          json, token, authorizeUserManager, routes.users.load, routes.users.setRole);
    router.put ('/api/v1/users/:userId/active',        json, token, authorizeUserManager, routes.users.load, routes.users.setActive);
    router.post('/api/v1/users/:userId/profile',       json, token, authorizeUserManager, routes.users.load, routes.users.updateProfile);
    router.get ('/api/v1/users/:userId/avatar',              token, authorizeUserManager, routes.users.load, routes.users.getAvatar);
    router.post('/api/v1/users/:userId/avatar',              token, authorizeUserManager, multipart, routes.users.load, routes.users.setAvatar);
    router.del ('/api/v1/users/:userId/avatar',              token, authorizeUserManager, routes.users.load, routes.users.unsetAvatar);
    router.post('/api/v1/users/:userId/password',      json, token, authorizeUserManager, routes.users.load, routes.users.setPassword);
    router.post('/api/v1/users/:userId/ghost',         json, token, authorizeAdmin,       routes.users.load, routes.users.setGhost);
    router.put ('/api/v1/users/:userId/groups',        json, token, authorizeUserManager, routes.users.load, routes.users.setLocalGroups);
    router.get ('/api/v1/users/:userId/password_reset_link',             token, authorizeUserManager, routes.users.load, routes.users.getPasswordResetLink);
    router.post('/api/v1/users/:userId/send_password_reset_email', json, token, authorizeUserManager, routes.users.load, routes.users.sendPasswordResetEmail);
    router.get ('/api/v1/users/:userId/invite_link',                     token, authorizeUserManager, routes.users.load, routes.users.getInviteLink);
    router.post('/api/v1/users/:userId/send_invite_email',         json, token, authorizeUserManager, routes.users.load, routes.users.sendInviteEmail);
    router.post('/api/v1/users/:userId/totp_disable', json, token, authorizeUserManager, routes.users.load, routes.users.disableTotp);

    // Group management
    router.get ('/api/v1/groups',                        token, authorizeUserManager, routes.groups.list);
    router.post('/api/v1/groups',                  json, token, authorizeUserManager, routes.groups.add);
    router.get ('/api/v1/groups/:groupId',               token, authorizeUserManager, routes.groups.load, routes.groups.get);
    router.put ('/api/v1/groups/:groupId/members', json, token, authorizeUserManager, routes.groups.load, routes.groups.setMembers);
    router.put ('/api/v1/groups/:groupId/apps',    json, token, authorizeUserManager, routes.groups.load, routes.groups.setAllowedApps);
    router.put ('/api/v1/groups/:groupId/name',    json, token, authorizeUserManager, routes.groups.load, routes.groups.setName);
    router.del ('/api/v1/groups/:groupId',               token, authorizeUserManager, routes.groups.load, routes.groups.del);

    // User directory
    router.get ('/api/v1/user_directory/profile_config',          token, authorizeAdmin, routes.userDirectory.getProfileConfig);
    router.post('/api/v1/user_directory/profile_config',    json, token, authorizeAdmin, routes.userDirectory.setProfileConfig);

    // External LDAP
    router.get ('/api/v1/external_ldap/config',          token, authorizeAdmin, routes.externalLdap.getConfig);
    router.post('/api/v1/external_ldap/config',    json, token, authorizeAdmin, routes.externalLdap.setConfig);
    router.post('/api/v1/external_ldap/sync',      json, token, authorizeAdmin, routes.externalLdap.sync);

    // Directory Server
    router.get ('/api/v1/directory_server/config',          token, authorizeAdmin, routes.directoryServer.getConfig);
    router.post('/api/v1/directory_server/config',    json, token, authorizeAdmin, routes.directoryServer.setConfig);

    // appstore and subscription routes
    router.post('/api/v1/appstore/unlink_account',                       json, token, authorizeOwner, routes.appstore.unlinkAccount);
    router.get ('/api/v1/appstore/subscription',                               token, authorizeUser,  routes.appstore.getSubscription); // for all users
    router.get ('/api/v1/appstore/apps',                                       token, authorizeAdmin, routes.appstore.getApps);
    router.get ('/api/v1/appstore/apps/:appstoreId',                           token, authorizeAdmin, routes.appstore.getApp);
    router.get ('/api/v1/appstore/apps/:appstoreId/versions/:versionId',       token, authorizeAdmin, routes.appstore.getAppVersion);

    // community app routes
    router.get ('/api/v1/community/app',                                       token, authorizeAdmin, routes.community.getAppVersion);

    // app routes
    router.post('/api/v1/apps',                       jsonOrMultipart, token,                   authorizeAdmin,    routes.apps.install);
    router.get ('/api/v1/apps',                                        token,                   authorizeUser,     routes.apps.listByUser);
    router.get ('/api/v1/apps/:id',                                    token, routes.apps.load, authorizeOperator, routes.apps.getApp);
    router.get ('/api/v1/apps/:id/icon',                                      routes.apps.load,                    routes.apps.getAppIcon);
    router.post('/api/v1/apps/:id/archive',                      json, token, routes.apps.load, authorizeAdmin, routes.apps.archive);
    router.post('/api/v1/apps/:id/uninstall',                    json, token, routes.apps.load, authorizeAdmin, routes.apps.uninstall);
    router.post('/api/v1/apps/:id/configure/access_restriction', json, token, routes.apps.load, authorizeAdmin, routes.apps.setAccessRestriction);
    router.post('/api/v1/apps/:id/configure/operators',          json, token, routes.apps.load, authorizeAdmin, routes.apps.setOperators);
    router.post('/api/v1/apps/:id/configure/label',              json, token, routes.apps.load, authorizeOperator, routes.apps.setLabel);
    router.post('/api/v1/apps/:id/configure/tags',               json, token, routes.apps.load, authorizeOperator, routes.apps.setTags);
    router.post('/api/v1/apps/:id/configure/icon',               json, token, routes.apps.load, authorizeOperator, routes.apps.setIcon);
    router.post('/api/v1/apps/:id/configure/notes',              json, token, routes.apps.load, authorizeOperator, routes.apps.setNotes);
    router.post('/api/v1/apps/:id/configure/memory_limit',       json, token, routes.apps.load, authorizeOperator, routes.apps.setMemoryLimit);
    router.post('/api/v1/apps/:id/configure/cpu_quota',          json, token, routes.apps.load, authorizeOperator, routes.apps.setCpuQuota);
    router.post('/api/v1/apps/:id/configure/automatic_backup',   json, token, routes.apps.load, authorizeOperator, routes.apps.setAutomaticBackup);
    router.post('/api/v1/apps/:id/configure/automatic_update',   json, token, routes.apps.load, authorizeOperator, routes.apps.setAutomaticUpdate);
    router.post('/api/v1/apps/:id/configure/reverse_proxy',      json, token, routes.apps.load, authorizeOperator, routes.apps.setReverseProxyConfig);
    router.post('/api/v1/apps/:id/configure/cert',               json, token, routes.apps.load, authorizeOperator, routes.apps.setCertificate);
    router.post('/api/v1/apps/:id/configure/debug_mode',         json, token, routes.apps.load, authorizeOperator, routes.apps.setDebugMode);
    router.post('/api/v1/apps/:id/configure/mailbox',            json, token, routes.apps.load, authorizeAdmin, routes.apps.setMailbox);
    router.post('/api/v1/apps/:id/configure/inbox',              json, token, routes.apps.load, authorizeAdmin, routes.apps.setInbox);
    router.post('/api/v1/apps/:id/configure/turn',               json, token, routes.apps.load, authorizeAdmin, routes.apps.setTurn);
    router.post('/api/v1/apps/:id/configure/redis',              json, token, routes.apps.load, authorizeAdmin, routes.apps.setRedis);
    router.post('/api/v1/apps/:id/configure/env',                json, token, routes.apps.load, authorizeOperator, routes.apps.setEnvironment);
    router.post('/api/v1/apps/:id/configure/storage',            json, token, routes.apps.load, authorizeAdmin, routes.apps.setStorage);
    router.post('/api/v1/apps/:id/configure/location',           json, token, routes.apps.load, authorizeAdmin, routes.apps.setLocation);
    router.post('/api/v1/apps/:id/configure/mounts',             json, token, routes.apps.load, authorizeAdmin, routes.apps.setMounts);
    router.post('/api/v1/apps/:id/configure/devices',            json, token, routes.apps.load, authorizeAdmin, routes.apps.setDevices);
    router.post('/api/v1/apps/:id/configure/crontab',            json, token, routes.apps.load, authorizeOperator, routes.apps.setCrontab);
    router.post('/api/v1/apps/:id/configure/upstream_uri',       json, token, routes.apps.load, authorizeOperator, routes.apps.setUpstreamUri);
    router.post('/api/v1/apps/:id/repair',                       json, token, routes.apps.load, authorizeOperator, routes.apps.repair);
    router.post('/api/v1/apps/:id/check_update',                 json, token, routes.apps.load, authorizeOperator, routes.apps.checkUpdate);
    router.post('/api/v1/apps/:id/update',            jsonOrMultipart, token, routes.apps.load, authorizeOperator, routes.apps.update);
    router.post('/api/v1/apps/:id/restore',                      json, token, routes.apps.load, authorizeOperator, routes.apps.restore);
    router.post('/api/v1/apps/:id/import',                       json, token, routes.apps.load, authorizeOperator, routes.apps.importApp);
    router.post('/api/v1/apps/:id/export',                       json, token, routes.apps.load, authorizeOperator, routes.apps.exportApp);
    router.post('/api/v1/apps/:id/backup',                       json, token, routes.apps.load, authorizeOperator, routes.apps.backup);
    router.get ('/api/v1/apps/:id/backups',                            token, routes.apps.load, authorizeOperator, routes.apps.listBackups);
    router.get ('/api/v1/apps/:id/backup_sites',                       token, routes.apps.load, authorizeOperator, routes.apps.listBackupSites);
    router.post('/api/v1/apps/:id/backups/:backupId',            json, token, routes.apps.load, authorizeOperator, routes.apps.updateBackup);
    router.get ('/api/v1/apps/:id/backups/:backupId/download',         token, routes.apps.load, authorizeOperator, routes.apps.downloadBackup);
    router.put ('/api/v1/apps/:id/checklist/:key',               json, token, routes.apps.load, authorizeOperator, routes.apps.setChecklistItem);
    router.post('/api/v1/apps/:id/start',                        json, token, routes.apps.load, authorizeOperator, routes.apps.start);
    router.post('/api/v1/apps/:id/stop',                         json, token, routes.apps.load, authorizeOperator, routes.apps.stop);
    router.post('/api/v1/apps/:id/restart',                      json, token, routes.apps.load, authorizeOperator, routes.apps.restart);
    router.get ('/api/v1/apps/:id/logstream',                          token, routes.apps.load, authorizeOperator, routes.apps.getLogStream);
    router.get ('/api/v1/apps/:id/logs',                               token, routes.apps.load, authorizeOperator, routes.apps.getLogs);
    router.get ('/api/v1/apps/:id/eventlog',                           token, routes.apps.load, authorizeOperator, routes.apps.listEventlog);
    router.get ('/api/v1/apps/:id/task',                               token, routes.apps.load, authorizeOperator, routes.apps.getTask);
    router.get ('/api/v1/apps/:id/tasks',                              token, routes.apps.load, authorizeOperator, routes.apps.listAppTasks);
    router.get ('/api/v1/apps/:id/tasks/:taskId',                      token, routes.apps.load, authorizeOperator, routes.apps.loadTask, routes.apps.getAppTask);
    router.get ('/api/v1/apps/:id/tasks/:taskId/logs',                 token, routes.apps.load, authorizeOperator, routes.apps.loadTask, routes.apps.getAppTaskLogs);
    router.get ('/api/v1/apps/:id/tasks/:taskId/logstream',            token, routes.apps.load, authorizeOperator, routes.apps.loadTask, routes.apps.getAppTaskLogStream);
    router.post('/api/v1/apps/:id/tasks/:taskId/stop',           json, token, routes.apps.load, authorizeOperator, routes.apps.loadTask, routes.apps.stopAppTask);
    router.get ('/api/v1/apps/:id/metrics',                            token, routes.apps.load, authorizeOperator, routes.apps.getMetrics);
    router.get ('/api/v1/apps/:id/metricstream',                       token, routes.apps.load, authorizeOperator, routes.apps.getMetricStream);
    router.post('/api/v1/apps/:id/clone',                        json, token, routes.apps.load, authorizeAdmin,    routes.apps.clone);
    router.use ('/api/v1/apps/:id/files/*filepath',                    token, routes.apps.load, authorizeOperator, routes.filemanager.proxy('app'));
    router.post('/api/v1/apps/:id/exec',                         json, token, routes.apps.load, authorizeOperator, routes.apps.createExec);
    router.get ('/api/v1/apps/:id/exec/:execId/start',                 token, routes.apps.load, authorizeOperator, routes.apps.startExec);
    router.get ('/api/v1/apps/:id/exec/:execId',                       token, routes.apps.load, authorizeOperator, routes.apps.getExec);
    // these two routes are wrappers on exec. It allows upload/download to anywhere in filesystem unlike the files route which is only /app/data
    router.get ('/api/v1/apps/:id/download',                           token, routes.apps.load, authorizeOperator, routes.apps.downloadFile);
    router.post('/api/v1/apps/:id/upload',                             token, multipart, routes.apps.load, authorizeOperator, routes.apps.uploadFile);

    // websocket cannot do bearer authentication
    router.get ('/api/v1/apps/:id/exec/:execId/startws',    token, routes.apps.load, authorizeOperator, routes.apps.startExecWebSocket);

    // app links in dashboard
    router.get ('/api/v1/applinks',           token,                       authorizeUser,  routes.applinks.listByUser);
    router.post('/api/v1/applinks',     json, token,                       authorizeAdmin, routes.applinks.add);
    router.get ('/api/v1/applinks/:id',       token, routes.applinks.load, authorizeAdmin, routes.applinks.get);
    router.post('/api/v1/applinks/:id', json, token, routes.applinks.load, authorizeAdmin, routes.applinks.update);
    router.del ('/api/v1/applinks/:id',       token, routes.applinks.load, authorizeAdmin, routes.applinks.del);
    router.get ('/api/v1/applinks/:id/icon',  token, routes.applinks.load, authorizeUser,  routes.applinks.getIcon);

    // branding routes
    router.get ('/api/v1/branding/cloudron_name',         token, authorizeAdmin, routes.branding.getCloudronName);
    router.post('/api/v1/branding/cloudron_name',   json, token, authorizeAdmin, routes.branding.setCloudronName);
    router.get ('/api/v1/branding/cloudron_avatar',       token, authorizeAdmin, routes.branding.getCloudronAvatar);
    router.post('/api/v1/branding/cloudron_avatar',       token, authorizeAdmin, multipart, routes.branding.setCloudronAvatar);
    router.get ('/api/v1/branding/cloudron_background',   token, authorizeAdmin, routes.branding.getCloudronBackground);
    router.post('/api/v1/branding/cloudron_background',   token, authorizeAdmin, multipart, routes.branding.setCloudronBackground);
    router.del ('/api/v1/branding/cloudron_background',   token, authorizeAdmin,            routes.branding.unsetCloudronBackground);
    router.get ('/api/v1/branding/footer',                token, authorizeAdmin, routes.branding.getFooter);
    router.post('/api/v1/branding/footer',          json, token, authorizeAdmin, routes.branding.setFooter);

    // reverseproxy routes
    router.post('/api/v1/reverseproxy/renew_certs', json, token, authorizeAdmin, routes.reverseProxy.renewCerts);
    router.get ('/api/v1/reverseproxy/trusted_ips',       token, authorizeAdmin, routes.reverseProxy.getTrustedIps);
    router.post('/api/v1/reverseproxy/trusted_ips', json, token, authorizeAdmin, routes.reverseProxy.setTrustedIps);

    // network routes
    router.get ('/api/v1/network/blocklist',         token, authorizeAdmin, routes.network.getBlocklist);
    router.post('/api/v1/network/blocklist',   json, token, authorizeAdmin, routes.network.setBlocklist);
    router.get ('/api/v1/network/dynamic_dns',       token, authorizeAdmin, routes.network.getDynamicDns);
    router.post('/api/v1/network/dynamic_dns', json, token, authorizeAdmin, routes.network.setDynamicDns);
    router.get ('/api/v1/network/ipv4_config',       token, authorizeAdmin, routes.network.getIPv4Config);
    router.post('/api/v1/network/ipv4_config', json, token, authorizeAdmin, routes.network.setIPv4Config);
    router.get ('/api/v1/network/ipv6_config',       token, authorizeAdmin, routes.network.getIPv6Config);
    router.post('/api/v1/network/ipv6_config', json, token, authorizeAdmin, routes.network.setIPv6Config);
    router.get ('/api/v1/network/ipv4',              token, authorizeAdmin, routes.network.getIPv4);
    router.get ('/api/v1/network/ipv6',              token, authorizeAdmin, routes.network.getIPv6);

    // private docker registry
    router.get ('/api/v1/docker/registries',            token,                               authorizeAdmin, routes.dockerRegistries.list);
    router.post('/api/v1/docker/registries',      json, token,                               authorizeAdmin, routes.dockerRegistries.add);
    router.get ('/api/v1/docker/registries/:id',        token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.get);
    router.post('/api/v1/docker/registries/:id',  json, token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.update);
    router.del ('/api/v1/docker/registries/:id',        token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.del);

    // email routes
    router.get ('/api/v1/mailserver/eventlog',                token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/clear_eventlog',          token, authorizeAdmin, routes.mailserver.proxy);
    router.use ('/api/v1/mailserver/files/*filepath',         token, authorizeAdmin, routes.filemanager.proxy('mail'));
    router.get ('/api/v1/mailserver/location',                token, authorizeAdmin, routes.mailserver.getLocation);
    router.post('/api/v1/mailserver/location',          json, token, authorizeAdmin, routes.mailserver.setLocation);
    router.get ('/api/v1/mailserver/max_email_size',          token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/max_email_size',          token, authorizeAdmin, routes.mailserver.proxy);
    router.get ('/api/v1/mailserver/spam_acl',                token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/spam_acl',                token, authorizeAdmin, routes.mailserver.proxy);
    router.get ('/api/v1/mailserver/spam_custom_config',      token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/spam_custom_config',      token, authorizeAdmin, routes.mailserver.proxy);
    router.get ('/api/v1/mailserver/dnsbl_config',            token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/dnsbl_config',            token, authorizeAdmin, routes.mailserver.proxy);
    router.get ('/api/v1/mailserver/fts_config',              token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/fts_config',              token, authorizeAdmin, routes.mailserver.proxyAndRestart);
    router.get ('/api/v1/mailserver/mailbox_sharing',         token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/mailbox_sharing',         token, authorizeAdmin, routes.mailserver.proxyAndRestart);
    router.get ('/api/v1/mailserver/virtual_all_mail',        token, authorizeAdmin, routes.mailserver.proxy);
    router.post('/api/v1/mailserver/virtual_all_mail',        token, authorizeAdmin, routes.mailserver.proxyAndRestart);
    router.get ('/api/v1/mailserver/usage',                   token, authorizeMailManager, routes.mailserver.proxy);

    router.use ('/api/v1/mailserver/queue',                   token, authorizeAdmin, routes.mailserver.queueProxy);

    router.get ('/api/v1/mail/:domain',                               token, authorizeMailManager, routes.mail.getDomain);
    router.get ('/api/v1/mail',                                       token, authorizeMailManager, routes.mail.listDomains);
    router.post('/api/v1/mail/:domain/enable',                  json, token, authorizeAdmin, routes.mail.setMailEnabled);
    router.get ('/api/v1/mail/:domain/status',                        token, authorizeMailManager, routes.mail.getStatus);
    router.post('/api/v1/mail/:domain/mail_from_validation',    json, token, authorizeMailManager, routes.mail.setMailFromValidation);
    router.post('/api/v1/mail/:domain/catch_all',               json, token, authorizeMailManager, routes.mail.setCatchAllAddress);
    router.post('/api/v1/mail/:domain/relay',                   json, token, authorizeAdmin, routes.mail.setMailRelay);
    router.post('/api/v1/mail/:domain/banner',                  json, token, authorizeMailManager, routes.mail.setBanner);
    router.post('/api/v1/mail/:domain/send_test_mail',          json, token, authorizeMailManager, routes.mail.sendTestMail);
    router.get ('/api/v1/mail/:domain/stats',                         token, authorizeMailManager, routes.mail.getStats);
    router.get ('/api/v1/mail/:domain/mailboxes',                     token, authorizeMailManager, routes.mail.listMailboxesByDomain);
    router.get ('/api/v1/mail/:domain/mailboxes/:name',               token, authorizeMailManager, routes.mail.getMailbox);
    router.post('/api/v1/mail/:domain/mailboxes',               json, token, authorizeMailManager, routes.mail.addMailbox);
    router.post('/api/v1/mail/:domain/mailboxes/:name',         json, token, authorizeMailManager, routes.mail.updateMailbox);
    router.del ('/api/v1/mail/:domain/mailboxes/:name',         json, token, authorizeMailManager, routes.mail.delMailbox);
    router.get ('/api/v1/mail/:domain/mailboxes/:name/aliases',       token, authorizeMailManager, routes.mail.getAliases);
    router.put ('/api/v1/mail/:domain/mailboxes/:name/aliases', json, token, authorizeMailManager, routes.mail.setAliases);
    router.get ('/api/v1/mail/:domain/lists',                         token, authorizeMailManager, routes.mail.listMailingListsByDomain);
    router.post('/api/v1/mail/:domain/lists',                   json, token, authorizeMailManager, routes.mail.addMailingList);
    router.get ('/api/v1/mail/:domain/lists/:name',                   token, authorizeMailManager, routes.mail.getMailingList);
    router.post('/api/v1/mail/:domain/lists/:name',             json, token, authorizeMailManager, routes.mail.updateMailingList);
    router.del ('/api/v1/mail/:domain/lists/:name',                   token, authorizeMailManager, routes.mail.delMailingList);

    // domain routes
    router.post('/api/v1/domains',                    json, token, authorizeAdmin, routes.domains.add);
    router.post('/api/v1/domains/sync_dns',           json, token, authorizeAdmin, routes.domains.syncDnsRecords);
    router.get ('/api/v1/domains',                          token, authorizeUser,  routes.domains.list);
    router.get ('/api/v1/domains/:domain',                  token, authorizeAdmin, routes.domains.get);  // this is manage scope because it returns non-restricted fields
    router.post('/api/v1/domains/:domain/config',     json, token, authorizeAdmin, routes.domains.setConfig);
    router.post('/api/v1/domains/:domain/wellknown',  json, token, authorizeAdmin, routes.domains.setWellKnown);
    router.del ('/api/v1/domains/:domain',                  token, authorizeAdmin, routes.domains.del);
    router.get ('/api/v1/domains/:domain/dns_check',        token, authorizeAdmin, routes.domains.checkDnsRecords);

    // volume routes
    router.post('/api/v1/volumes',                   json, token, authorizeAdmin, routes.volumes.add);
    router.get ('/api/v1/volumes',                         token, authorizeAdmin, routes.volumes.list);
    router.get ('/api/v1/volumes/:id',                     token, authorizeAdmin, routes.volumes.load, routes.volumes.get);
    router.post('/api/v1/volumes/:id',               json, token, authorizeAdmin, routes.volumes.load, routes.volumes.update);
    router.del ('/api/v1/volumes/:id',                     token, authorizeAdmin, routes.volumes.load, routes.volumes.del);
    router.get ('/api/v1/volumes/:id/status',              token, authorizeAdmin, routes.volumes.load, routes.volumes.getStatus);
    router.post('/api/v1/volumes/:id/remount',             token, authorizeAdmin, routes.volumes.load, routes.volumes.remount);
    router.use ('/api/v1/volumes/:id/files/*filepath',     token, authorizeAdmin, routes.filemanager.proxy('volume'));

    // service routes
    router.get ('/api/v1/services',                          token, authorizeAdmin, routes.services.list);
    router.get ('/api/v1/services/platform_status',          token, authorizeUser,  routes.services.getPlatformStatus);
    router.get ('/api/v1/services/:service',                 token, authorizeAdmin, routes.services.get);
    router.post('/api/v1/services/:service',           json, token, authorizeAdmin, routes.services.configure);
    router.get ('/api/v1/services/:service/metrics',         token, authorizeAdmin, routes.services.getMetrics);
    router.get ('/api/v1/services/:service/logs',            token, authorizeAdmin, routes.services.getLogs);
    router.get ('/api/v1/services/:service/logstream',       token, authorizeAdmin, routes.services.getLogStream);
    router.post('/api/v1/services/:service/restart',   json, token, authorizeAdmin, routes.services.restart);
    router.post('/api/v1/services/:service/rebuild',   json, token, authorizeAdmin, routes.services.rebuild);

    // well known
    router.get ('/well-known-handler/*location', routes.wellknown.get);

    // OpenID connect clients
    router.get ('/api/v1/oidc/clients',                   token, authorizeAdmin, routes.oidcClients.list);
    router.post('/api/v1/oidc/clients',             json, token, authorizeAdmin, routes.oidcClients.add);
    router.get ('/api/v1/oidc/clients/:clientId',         token, authorizeAdmin, routes.oidcClients.load, routes.oidcClients.get);
    router.post('/api/v1/oidc/clients/:clientId',   json, token, authorizeAdmin, routes.oidcClients.load, routes.oidcClients.update);
    router.del ('/api/v1/oidc/clients/:clientId',         token, authorizeAdmin, routes.oidcClients.load, routes.oidcClients.del);

    // dashboard assets. Only entry points are served up here, rest is via static assets from nginx
    router.get ('/', routes.dashboard.renderIndex);
    router.get ('/filemanager.html', routes.dashboard.renderFilemanager);
    router.get ('/logs.html', routes.dashboard.renderLogs);
    router.get ('/terminal.html', routes.dashboard.renderTerminal);
    router.get ('/passwordreset.html', routes.dashboard.renderPasswordreset);
    router.get ('/setupaccount.html', routes.dashboard.renderSetupaccount);
    router.get ('/activation.html', routes.dashboard.renderActivation);
    router.get ('/setup.html', routes.dashboard.renderSetup);
    router.get ('/restore.html', routes.dashboard.renderRestore);

    // upgrade handler
    httpServer.on('upgrade', function (req, socket, head) {
        // create a node response object for express
        const res = new http.ServerResponse({});
        res.assignSocket(socket);

        if (req.headers.upgrade === 'websocket') {
            res.handleUpgrade = function (callback) {
                wsServer.handleUpgrade(req, socket, head, callback);
            };
        } else {
            res.sendUpgradeHandshake = function () { // could extend express.response as well
                socket.write('HTTP/1.1 101 TCP Handshake\r\n' +
                             'Upgrade: tcp\r\n' +
                             'Connection: Upgrade\r\n' +
                             '\r\n');
            };
        }

        // route through express middleware. if we provide no callback, express will provide a 'finalhandler'
        // TODO: it's not clear if socket needs to be destroyed
        app(req, res);
    });

    return httpServer;
}

async function start() {
    assert(gHttpServer === null, 'Server is already up and running.');

    log('==========================================');
    log(`           Cloudron ${constants.VERSION}  `);
    log('==========================================');

    await platform.initialize();

    gHttpServer = await initializeExpressSync();

    await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.PORT, '127.0.0.1');
    await safe(eventlog.add(eventlog.ACTION_START, AuditSource.BOOT, { version: constants.VERSION })); // can fail if db down
}

async function stop() {
    if (!gHttpServer) return;

    await platform.uninitialize();
    gHttpServer.closeAllConnections();
    await util.promisify(gHttpServer.close.bind(gHttpServer))();

    gHttpServer = null;
}

export default {
    start,
    stop
};
