"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigRunnerBuilder = void 0;
const base_1 = require("@darlean/base");
const utils_1 = require("@darlean/utils");
const infra_1 = require("./infra");
const bsondeser_1 = require("./infra/bsondeser");
const natsserver_1 = require("./infra/natsserver");
const running_1 = require("./running");
const json5 = __importStar(require("json5"));
const fs = __importStar(require("fs"));
const path_1 = __importDefault(require("path"));
const DMB_BASE_PORT = 4500;
const DMB_CLUSTER_PORT_BASE = 5500;
const DEFAULT_SHARD_COUNT = 8;
/**
 * Builds an {@link ActorRunner} instance based on configuration settings, command-line
 * arguments and environment variables.
 *
 * Command-line arguments have priority, followed by environment variables, with the provided
 * configuration data as fall-back.
 *
 * **Command-line arguments** must by default be prefixed with `--darlean-`. So, when the documentation
 * mentions a command-line argument `app-id`, it must by default be provided as `--darlean-app-id`.
 * The value must be provided as after the `=` sign: `--darlean-app-id=client03`. When
 * multiple vales are expected, they must be comma-separated.
 *
 * **Environment variables** must by default be prefixed with `DARLEAN_`. So, when the documentation
 * mentions an environment variable `APP_ID`, it must by default be provided as `DARLEAN_APP_ID`.
 * When multiple values are expected, they must be comma-separated.
 *
 * The argument and environment variable prefix can be configured in the constructor.
 *
 * @see {@link IApplicationCfg} for an overview of all the available configuration items.
 */
class ConfigRunnerBuilder {
    constructor(config, envPrefix, argPrefix) {
        this.actors = [];
        this.envPrefix = envPrefix ?? 'DARLEAN_';
        this.argPrefix = argPrefix ?? '--darlean-';
        this.overrides = new Map();
        const cfg = this.deriveConfig();
        this.config = cfg;
        this.loadOverrides();
        this.appId = this.fetchString('APP_ID', 'app-id') ?? cfg.appId ?? 'app';
    }
    /**
     * Registers an individual actor that must be hosted by the actor runner.
     * @param options The options for the actor.
     * @returns The builder
     */
    registerActor(options) {
        this.actors.push(options);
        return this;
    }
    /**
     * Registers a suite of actors that must be hosted by the actor runner.
     * @param suite The suite to be registered
     * @returns The builder
     */
    registerSuite(suite) {
        for (const options of suite.getRegistrationOptions()) {
            this.registerActor(options);
        }
        return this;
    }
    getAppId() {
        return this.appId ?? '';
    }
    build() {
        const config = this.config;
        const appId = this.appId;
        const builder = new running_1.ActorRunnerBuilder();
        this.appId = appId;
        const runtimeAppsExplicit = this.fetchArray('RUNTIME_APPS', 'runtime-apps') ?? config.runtimeApps;
        const runtimeApps = runtimeAppsExplicit ?? [appId];
        const allInOne = runtimeAppsExplicit === undefined;
        const implicitRuntime = runtimeAppsExplicit?.includes(appId);
        const runtimeEnabledString = this.fetchString('RUNTIME_ENABLED', 'runtime-enabled');
        const runtimeEnabled = runtimeEnabledString === undefined
            ? config.runtime?.enabled ?? (allInOne || implicitRuntime)
            : runtimeEnabledString.toLowerCase() === 'true';
        builder.setRuntimeApps(runtimeApps);
        if (runtimeEnabled) {
            const runtime = config.runtime;
            this.configureHostActorLock(builder, runtime, runtimeApps);
            this.configureHostActorRegistry(builder, runtime, runtimeApps);
            this.configurePersistence(builder, runtime);
            this.configureFsPersistence(builder, runtime);
        }
        this.configureActors(builder);
        this.configureTransports(builder, config, allInOne, runtimeApps, appId);
        const runner = builder.build();
        if (runtimeEnabled && !allInOne) {
            this.configureDmbServer(runner, config, runtimeApps, appId);
        }
        this.configurePidAndRun(appId, runner, config);
        return runner;
    }
    configureHostActorLock(builder, runtime, runtimeApps) {
        if (!(runtime?.actorLock?.enabled === false)) {
            builder.hostActorLock(runtime?.actorLock?.apps ?? runtimeApps, runtime?.actorLock?.redundancy ?? 3);
        }
    }
    configureHostActorRegistry(builder, runtime, runtimeApps) {
        if (!(runtime?.actorRegistry?.enabled === false)) {
            builder.hostActorRegistry(runtime?.actorRegistry?.apps ?? runtimeApps);
        }
    }
    configurePersistence(builder, runtime) {
        if (!(runtime?.persistence?.enabled === false)) {
            const options = {
                compartments: [],
                handlers: []
            };
            const DEFAULT_SPECIFIER = {
                specifier: '*',
                compartment: 'fs.default'
            };
            for (const spec of [...(runtime?.persistence?.specifiers ?? []), DEFAULT_SPECIFIER]) {
                options.compartments.push({
                    compartment: spec.compartment,
                    specifier: spec.specifier
                });
            }
            const DEFAULT_HANDLER = {
                compartment: 'fs.*',
                actorType: base_1.FS_PERSISTENCE_SERVICE
            };
            for (const handler of [...(runtime?.persistence?.handlers ?? []), DEFAULT_HANDLER]) {
                options.handlers.push({
                    compartment: handler.compartment,
                    actorType: handler.actorType
                });
            }
            builder.hostPersistence(options);
        }
    }
    configureFsPersistence(builder, runtime) {
        if (!(runtime?.persistence?.fs?.enabled === false)) {
            const options = {
                compartments: []
            };
            const maxShardCount = this.fetchNumber('FS_PERSISTENCE_MAX_SHARD_COUNT', 'fs-persistence-max-shard-count') ??
                runtime?.persistence?.fs.maxShardCount;
            const DEFAULT_COMPARTMENT = {
                compartment: 'fs.*',
                basePath: this.fetchString('FS_PERSISTENCE_BASE_PATH', 'fs-persistence-base-path') ?? './persistence/',
                shardCount: limit(this.fetchNumber('FS_PERSISTENCE_SHARD_COUNT', 'fs-persistence-shard-count') ?? DEFAULT_SHARD_COUNT, maxShardCount)
            };
            for (const comp of [DEFAULT_COMPARTMENT, ...(runtime?.persistence?.fs?.compartments ?? [])]) {
                options.compartments.push({
                    compartment: comp.compartment,
                    basePath: comp.basePath,
                    subPath: comp.subPath,
                    nodes: comp.nodes,
                    partitionKeyLen: comp.partitionKeyLen,
                    sortKeyLen: comp.sortKeyLen,
                    shardCount: limit(comp.shardCount ?? DEFAULT_SHARD_COUNT, maxShardCount)
                });
            }
            builder.hostFsPersistence(options);
        }
    }
    configureActors(builder) {
        for (const actor of this.actors) {
            builder.registerActor(actor);
        }
    }
    configureTransports(builder, config, allInOne, runtimeApps, appId) {
        const messagingTransportsExplicit = this.fetchArray('MESSAGING_TRANSPORTS', 'messaging-transports') ?? config.messaging?.transports;
        const messagingTransports = messagingTransportsExplicit ?? allInOne ? [] : ['dmb'];
        let transportSet = false;
        if (!allInOne || (messagingTransportsExplicit?.length ?? 0 > 0)) {
            for (const transport of messagingTransports) {
                if (transport === 'dmb') {
                    const dmb = config.messaging?.dmb;
                    const seedUrls = [];
                    const hosts = this.deriveHosts(dmb, runtimeApps);
                    const offsets = this.derivePortOffsets(hosts);
                    const basePort = this.fetchNumber('DMB_BASE_PORT', 'dmb-base-port') ?? dmb?.basePort ?? DMB_BASE_PORT;
                    for (let idx = 0; idx < hosts.length; idx++) {
                        seedUrls.push(hosts[idx] + ':' + (basePort + offsets[idx]).toString());
                    }
                    builder.setRemoteAccess(appId, new infra_1.NatsTransport(new bsondeser_1.BsonDeSer(), seedUrls));
                    transportSet = true;
                }
            }
        }
        if (!transportSet) {
            builder.setRemoteAccess(appId, new infra_1.InProcessTransport(new bsondeser_1.BsonDeSer()));
        }
    }
    configureDmbServer(runner, config, runtimeApps, appId) {
        const dmb = config.messaging?.dmb;
        const runtimedmb = config.runtime?.dmb;
        const enabled = truefalse(this.fetchString('DMB_SERVER_ENABLED', 'dmb-server-enabled')) ?? runtimedmb?.enabled;
        if (enabled === undefined || enabled === true) {
            const appidx = runtimeApps.indexOf(appId);
            const hosts = this.deriveHosts(dmb, runtimeApps);
            if (appidx >= 0 && appidx < hosts.length) {
                const offsets = this.derivePortOffsets(hosts);
                const basePort = this.fetchNumber('DMB_BASE_PORT', 'dmb-base-port') ?? dmb?.basePort ?? DMB_BASE_PORT;
                const clusterPortBase = this.fetchNumber('DMB_CLUSTER_BASE_PORT', 'dmb-cluster-base-port') ??
                    runtimedmb?.clusterPortBase ??
                    DMB_CLUSTER_PORT_BASE;
                const clusterSeeds = [];
                for (let idx = 0; idx < hosts.length; idx++) {
                    clusterSeeds.push('nats://' + hosts[idx] + ':' + (clusterPortBase + offsets[idx]).toString());
                }
                const clusterListenUrl = 'nats://' + hosts[appidx] + ':' + (clusterPortBase + offsets[appidx]).toString();
                const serverListenPort = basePort + offsets[appidx];
                const server = new natsserver_1.NatsServer((stderr) => {
                    (0, utils_1.notifier)().error('io.darlean.dmbserver.Stopped', 'DMB NATS server[AppId] suddenly stopped: [Error]', () => ({ AppId: appId, Error: stderr }));
                }, serverListenPort, clusterSeeds, clusterListenUrl, appId);
                runner.addStarter(async () => {
                    (0, utils_1.notifier)().info('io.darlean.dmbserver.Starting', 'Starting DMB NATS server [AppId]...', () => ({
                        AppId: appId
                    }));
                    server.start();
                    await (0, utils_1.sleep)(2000);
                    (0, utils_1.notifier)().info('io.darlean.dmbserver.Running', 'DMB NATS server [AppId] is running.', () => ({
                        AppId: appId
                    }));
                }, 20, 'DMB NATS server');
                runner.addStopper(async () => {
                    server.stop();
                    await (0, utils_1.sleep)(2000);
                }, 20, 'DMB NATS server');
            }
            else {
                if (enabled === true) {
                    throw new Error('DMB-server is set to enabled, but current app-id is not in the list of runtime-apps.');
                }
            }
        }
    }
    configurePidAndRun(appId, runner, config) {
        let pidFilePrefix = this.fetchString('PID_FILE_PREFIX', 'pid-file-prefix') ?? config.pidFilePrefix ?? './pid/';
        if (pidFilePrefix === 'none') {
            pidFilePrefix = '';
        }
        let runFilePrefix = this.fetchString('RUN_FILE_PREFIX', 'run-file-prefix') ?? config.runFilePrefix ?? pidFilePrefix;
        if (runFilePrefix === 'none') {
            runFilePrefix = '';
        }
        const pid = process.pid.toString();
        if (pidFilePrefix) {
            const pidFile = pidFilePrefix + makeNice(appId) + '.pid';
            const pidFolder = path_1.default.dirname(pidFile);
            runner.addStarter(async () => {
                if (fs.existsSync(pidFile)) {
                    throw new Error(`Not allowed to start: another instance of [${appId}] may already be running. Ensure it is stopped and that [${pidFile}] is deleted.`);
                }
                fs.mkdirSync(pidFolder, { recursive: true });
                fs.writeFileSync(pidFile, pid, {});
            }, 10, 'PID File Check');
            // Do the cleanup in onApplicationStop instead of runner.addStopper, because we also want this cleanup
            // to be performed when the application crashes so heavily that the asynchronous runner stop code is not
            // properly executed.
            (0, utils_1.onApplicationStop)(() => {
                fs.rmSync(pidFile, { force: true });
            });
            runner.addStopper(async () => {
                fs.rmSync(pidFile, { force: true });
            }, 10, 'PID File Remove');
        }
        if (runFilePrefix) {
            const abort = new AbortController();
            const runFile = runFilePrefix + makeNice(appId) + '.run';
            const runFolder = path_1.default.dirname(runFile);
            runner.addStarter(async () => {
                fs.mkdirSync(runFolder, { recursive: true });
                fs.writeFileSync(runFile, pid);
                fs.watch(runFile, { signal: abort.signal }, (eventType) => {
                    if (eventType === 'rename') {
                        console.log('Removal of run-file detected, gracefully stopping the application...');
                        runner.stop();
                    }
                });
            }, 11, 'Run File Watcher');
            // Do the cleanup in onApplicationStop instead of runner.addStopper, because we also want this cleanup
            // to be performed when the application crashes so heavily that the asynchronous runner stop code is not
            // properly executed.
            // The watching however must be stopped when the runner stops, otherwise the pending eventloop task prevent
            // the onApplicationStop from ever being triggered.
            runner.addStopper(async () => {
                abort.abort();
            }, 11, 'Run File Stop Watching');
            (0, utils_1.onApplicationStop)(() => {
                fs.rmSync(runFile, { force: true });
            });
        }
    }
    fetchString(envName, argName) {
        return (0, utils_1.fetchConfigString)(this.envPrefix + envName, this.argPrefix + argName);
    }
    fetchNumber(envName, argName) {
        return (0, utils_1.fetchConfigNumber)(this.envPrefix + envName, this.argPrefix + argName);
    }
    fetchArray(envName, argName) {
        return (0, utils_1.fetchConfigArray)(this.envPrefix + envName, this.argPrefix + argName);
    }
    derivePortOffsets(hosts) {
        const result = [];
        const ports = new Map();
        for (const host of hosts) {
            const lastPort = (ports.get(host) ?? -1) + 1;
            ports.set(host, lastPort);
            result.push(lastPort);
        }
        return result;
    }
    deriveHosts(dmb, appIds) {
        const hostsExplicit = this.fetchArray('DMB_HOSTS', 'dmb-hosts') ?? dmb?.hosts;
        const hosts = hostsExplicit ?? appIds.map(() => '127.0.0.1');
        return hosts.slice(0, appIds.length);
    }
    deriveConfig() {
        const path = this.fetchString('CONFIG', 'config');
        if (path) {
            const contents = fs.readFileSync(path, { encoding: 'utf-8' });
            return json5.parse(contents);
        }
        if (this.config) {
            return this.config;
        }
        return {};
    }
    loadOverrides() {
        const overrides = this.fetchString('OVERRIDES', 'overrides');
        if (overrides) {
            for (const override of overrides.split(',').map((x) => x.trim())) {
                const contents = fs.readFileSync(override, { encoding: 'utf-8' });
                const lines = contents.split('\n').map((x) => x.trim());
                for (const line of lines) {
                    if (line.startsWith('#') || line.startsWith('//')) {
                        continue;
                    }
                    const p = line.indexOf('=');
                    if (p >= 0) {
                        const key = line.substring(0, p).trim();
                        const value = line.substring(p + 1).trim();
                        this.overrides.set(key, value);
                    }
                }
            }
        }
    }
}
exports.ConfigRunnerBuilder = ConfigRunnerBuilder;
function truefalse(value) {
    if (value === undefined) {
        return undefined;
    }
    return value.toLowerCase() === 'true';
}
function makeNice(value) {
    // Replaces all characters that are not a-z, A-Z or _ with a -.
    return value.replace(/(\W+)/gi, '-');
}
function limit(n, max) {
    if (n === undefined) {
        return undefined;
    }
    return max === undefined || n <= max ? n : max;
}
