"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDevRuntimeOptions = exports.getProdRuntimeOptions = exports.RuntimeContext = exports.getRuntime = void 0;
const Cache_1 = __importDefault(require("../runtime/Cache"));
const watch_1 = __importDefault(require("../utils/watch"));
const edge_functions_1 = require("../deploy/edge-functions");
const global_helpers_1 = require("./global.helpers");
const path_1 = require("path");
const edgeFunctionUtils_1 = require("../utils/edgeFunctionUtils");
const EdgeFunctionsNodeBuilder_1 = __importDefault(require("../deploy/edge-functions/EdgeFunctionsNodeBuilder"));
const ports_1 = require("../utils/ports");
const edgio_fs_1 = require("../edgio.fs");
const origins_1 = require("../origins");
const paths_1 = require("../deploy/paths");
const ts_fs_utils_1 = require("ts-fs-utils");
const serverMetrics_1 = require("./serverMetrics");
/**
 * RuntimeContext is initialized only once, as we cannot change the
 * runtime context once the app is started (for instance, we cannot
 * execute in the same process a dev server and a production server, or aws)
 */
let runtime = undefined;
const getRuntime = () => {
    const isProductionMode = process.env.NODE_ENV === 'production';
    // If the runtime is already initialized, we return it.
    // This is important as we cannot change the runtime context once the app is started.
    runtime =
        runtime !== null && runtime !== void 0 ? runtime : new RuntimeContext(isProductionMode ? (0, exports.getProdRuntimeOptions)() : (0, exports.getDevRuntimeOptions)());
    return runtime;
};
exports.getRuntime = getRuntime;
/**
 * A runtime class that is responsible for loading and reloading all the external resources
 * and creating runtime objects (config, router, propertyContext, cache) from them. It also ensures
 * that all the external resources are available only after the app is started and all the resources
 * are loaded.
 */
class RuntimeContext {
    constructor(runtimeOptions) {
        this.runtimeOptions = runtimeOptions;
        this.watchers = [];
    }
    get isDevMode() {
        return this.runtimeOptions.devMode;
    }
    /**
     * Waits for the runtime to be ready and returns RuntimeObjects. The runtime is collection of external resources,
     * like file system (routes.js, edgio.config.js) and node server (dev.js, prod.js).
     * This method waits for all these resources to be ready, and then returns them.
     *
     * @returns runtime objects (config, router, propertyContext, cache),
     * that are constructed from files in the file system
     */
    async waitForRuntime() {
        // must be in this order, as appStartedPromise must be awaited first,
        // so it will execute all needed preparation before the reloadPromise
        // (for instance, in dev mode we execute dev.js, which has dev server
        // that will build and bundle edgio.config.js and routes.js)
        if (this.appStartedPromise === undefined)
            this.appStartedPromise = this.startApp(ports_1.jsPort);
        await this.appStartedPromise;
        if (this.reloadPromise == undefined)
            this.reloadPromise = this.reload();
        return this.reloadPromise;
    }
    async reload() {
        const internal = this.runtimeOptions.fs.edgio.lambda.internal;
        // Force reload on files that we are watching
        // as we are using utility provided by ts-fs-utils,
        // we need to explicitly call reload on the files
        // as they are cached
        internal.routes.contentReload();
        internal.config.contentReload();
        const router = internal.routes.content;
        const cache = this.runtimeOptions.isCacheEnabled ? new Cache_1.default() : undefined;
        // Rebuild edge functions only in dev mode.
        // In prod mode, the edge functions are already built,
        // and we'll use the base64 encoded bytecode from the edge_control config.
        if (this.runtimeOptions.devMode) {
            await new EdgeFunctionsNodeBuilder_1.default(router, (0, path_1.dirname)(this.runtimeOptions.fs.srcRoutesTs.value)).build();
        }
        const config = RuntimeContext.withOrigins(internal.config.content, this.runtimeOptions.origins);
        const configForEdge = RuntimeContext.withOrigins(internal.config.content, this.runtimeOptions.originsForEdgeControl);
        return {
            config: RuntimeContext.withEdgeFunctions(config, router.rules),
            configForEdge: RuntimeContext.withEdgeFunctions(configForEdge, router.rules),
            router,
            cache,
        };
    }
    // Helper function to inject the origins, as they are not part of the config file
    // they are either loaded from env variables or from hardcoded values in the core
    static withOrigins(config, origins) {
        var _a;
        return {
            ...config,
            origins: [...((_a = config.origins) !== null && _a !== void 0 ? _a : []), ...origins],
        };
    }
    // Loads Edge Functions bytecode as a base64 string
    // and injects it into the config
    static withEdgeFunctions(config, rules) {
        var _a;
        return {
            ...config,
            edge_functions: {
                quickjs_bytecode_base64: (_a = (0, edge_functions_1.getEdgeFunctionBytecode)(rules)) === null || _a === void 0 ? void 0 : _a.toString('base64'),
            },
        };
    }
    setWatchers() {
        var _a, _b;
        const router = this.runtimeOptions.fs.edgio.lambda.internal.routes.content;
        const edgeFunctionFiles = (0, edgeFunctionUtils_1.getEdgeFunctionFiles)(router.rules);
        const appFiles = (_b = (_a = global_helpers_1.EdgioRuntimeGlobal.devOptions) === null || _a === void 0 ? void 0 : _a.reloadOnChangeOf) !== null && _b !== void 0 ? _b : [];
        const internalFiles = [
            this.runtimeOptions.fs.edgio.lambda.internal.routes.value,
            this.runtimeOptions.fs.edgio.lambda.internal.config.value,
        ];
        const setWatcher = (fileOrFolder, resetOnChange = false) => {
            const watcher = (0, watch_1.default)(fileOrFolder, { ignoreInitial: true });
            // we don't call directly reload, as it would execute async code in sync context,
            // and we want to avoid that, so we just set the promise and await it in waitForRuntime
            // so caller can await waitForRuntime and be sure that all the resources are loaded
            watcher.on('all', async () => {
                // When routes or config change,
                // we need to reset the watchers as well
                if (resetOnChange)
                    this.resetWatchers();
                return (this.reloadPromise = this.reload());
            });
            this.watchers.push(watcher);
        };
        internalFiles.forEach(file => setWatcher(file, true));
        appFiles.forEach(file => setWatcher(file));
        edgeFunctionFiles.forEach(file => setWatcher(file));
    }
    clearWatchers() {
        this.watchers.forEach(watcher => watcher.close());
        this.watchers = [];
    }
    resetWatchers() {
        this.clearWatchers();
        this.setWatchers();
    }
    async startApp(jsPort) {
        var _a;
        global_helpers_1.EdgioRuntimeGlobal.runtimeOptions = this.runtimeOptions;
        const { isProductionBuild } = this.runtimeOptions;
        const workingPath = isProductionBuild
            ? this.runtimeOptions.fs.edgio.lambda.app.value
            : process.cwd();
        // When we're in dev mode, the working path is the root of the project.
        // When we're in prod mode, we need to change the working path to the .edgio/lambda/app folder
        // from initial .edgio/lambda because user's apps assume
        // that the working path is where the project's files are located,
        // and they use process.cwd() to resolve paths instead of __dirname.
        // For many frameworks such as Angular, Astro or apps using nodejs-connector
        // is no way for us to specify the working path for the app's server
        // like we can do for Next.js server.
        process.chdir(workingPath);
        serverMetrics_1.metrics.appStartedAt = Date.now();
        let entry = await import(`file://${this.runtimeOptions.entryFile.value}`);
        // find the default export
        entry = ((_a = entry === null || entry === void 0 ? void 0 : entry.default) === null || _a === void 0 ? void 0 : _a.default) || (entry === null || entry === void 0 ? void 0 : entry.default) || entry;
        // load the entry point from the framework integration package
        await entry(jsPort);
        serverMetrics_1.metrics.appReadyAt = Date.now();
        // set again the cwd as customers can change working path in their custom entry file
        process.chdir(workingPath);
        if (this.runtimeOptions.devMode) {
            // TODO: consider moving this logic to the entry file itself, and read the result of the call
            // as it is more obvious what is happening, than implicitly reading the result from global envirovment
            this.setWatchers();
        }
    }
}
exports.RuntimeContext = RuntimeContext;
/**
 * Returns the runtime options
 * for the production mode
 * @returns
 */
const getProdRuntimeOptions = () => {
    const baseFolder = new ts_fs_utils_1.AbsoluteFolderName(process.cwd());
    const fs = (0, edgio_fs_1.createEdgioFS)(process.cwd());
    // As AWS doesn't have the same folder structure as CLI, we need to
    // create the lambda folder manually by faking root folder and only
    // set lambda folder as that's what only we care, but we can see that
    // this is dangerous as we are faking the folder structure and if someone
    // will use other locations (other than lambda folder), it will fail
    fs.edgio.lambda = (0, edgio_fs_1.createLambdaFs)(baseFolder.parent.value, baseFolder.name.value);
    return {
        devMode: false,
        isProductionBuild: true,
        isCacheEnabled: global_helpers_1.EdgioRuntimeGlobal.isCacheEnabled,
        originsForEdgeControl: (0, origins_1.getEdgioOrigins)(true),
        origins: (0, origins_1.getEdgioOrigins)(),
        entryFile: getEntryFile(fs),
        // The DRI logging is enabled only in production mode and
        // when EDGIO_HTTP_REQUEST_LOGGING env variable is set to 1
        // by the Console API
        driLogging: process.env.EDGIO_HTTP_REQUEST_LOGGING === '1',
        fs,
    };
};
exports.getProdRuntimeOptions = getProdRuntimeOptions;
/**
 * Returns the runtime options
 * for the development mode
 * @returns
 */
const getDevRuntimeOptions = () => {
    var _a;
    // Dev mode is executed by CLI, so we are in the root folder of the project
    // which should be the current working directory, from there we resolve all
    // other paths
    const baseDir = process.cwd();
    const fs = (0, edgio_fs_1.createEdgioFS)(baseDir);
    const targetConfig = fs.srcEdgioConfig.exists() ? fs.srcEdgioConfig : fs.srcEdgioConfigCjs;
    const entryFile = (_a = (0, paths_1.getEntryPoint)('dev', fs.value, targetConfig.content.connector)) !== null && _a !== void 0 ? _a : (0, paths_1.getEntryPoint)('dev', fs.value);
    return {
        devMode: true,
        isProductionBuild: false,
        isCacheEnabled: global_helpers_1.EdgioRuntimeGlobal.isCacheEnabled,
        originsForEdgeControl: (0, origins_1.getEdgioOrigins)(true),
        origins: (0, origins_1.getEdgioOrigins)(),
        entryFile: new ts_fs_utils_1.AbsoluteFileName(entryFile),
        driLogging: false,
        fs,
    };
};
exports.getDevRuntimeOptions = getDevRuntimeOptions;
/**
 * Returns the entry file.
 * This can be either prod or dev entry file
 * from the connector or the default one from @edgio/core
 * @param fs The file system instance
 */
const getEntryFile = (fs) => {
    // Mostly, we should have backend handler, but in some cases
    // when only edgio/core is used (no other connectors), we don't
    // have backend handler, so we need to use default one, from
    // edgio/core itself, which is just a dummy handler that does
    // nothing, but it allows us to start the server
    if (fs.edgio.lambda.internal.backend.handler.exists()) {
        return fs.edgio.lambda.internal.backend.handler;
    }
    throw new Error('No backend handler found. Build is corrupted, entry file is required.');
};
