"use strict";
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
    if (typeof path === "string" && /^\.\.?\//.test(path)) {
        return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
            return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
        });
    }
    return path;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scope = exports.MissingDefaultFilesOptionError = void 0;
const harper_logger_js_1 = require("../utility/logging/harper_logger.js");
const node_events_1 = require("node:events");
const databases_ts_1 = require("../resources/databases.js");
const Server_ts_1 = require("../server/Server.js");
const EntryHandler_ts_1 = require("./EntryHandler.js");
const OptionsWatcher_ts_1 = require("./OptionsWatcher.js");
const Resources_ts_1 = require("../resources/Resources.js");
const requestRestart_ts_1 = require("./requestRestart.js");
class MissingDefaultFilesOptionError extends Error {
    constructor() {
        super('No default files option exists. Ensure `files` is specified in config.yaml');
        this.name = 'MissingDefaultFilesOptionError';
    }
}
exports.MissingDefaultFilesOptionError = MissingDefaultFilesOptionError;
/**
 * This class is what is passed to the `handleApplication` function of an extension.
 *
 * It is imperative that the instance is "ready" before it's passed to the `handleApplication` function
 * so that the developer can immediately start using `scope.options`, etc.
 *
 */
class Scope extends node_events_1.EventEmitter {
    #configFilePath;
    #directory;
    #appName;
    #pluginName;
    #entryHandler;
    #entryHandlers;
    #logger;
    #pendingInitialLoads;
    applicationScope;
    options;
    resources;
    server;
    ready;
    databaseEvents;
    constructor(appName, pluginName, directory, configFilePath, applicationScope) {
        super();
        this.#appName = appName;
        this.#pluginName = pluginName;
        this.#directory = directory;
        this.#configFilePath = configFilePath;
        this.#logger = logger || (0, harper_logger_js_1.loggerWithTag)(this.#appName);
        this.databaseEvents = databases_ts_1.databaseEventsEmitter;
        this.applicationScope = applicationScope;
        this.resources = applicationScope?.resources ?? Resources_ts_1.resources;
        this.server = applicationScope?.server ?? Server_ts_1.server;
        this.#entryHandlers = [];
        this.#pendingInitialLoads = new Set();
        this.ready = (0, node_events_1.once)(this, 'ready');
        // Create the options instance for the scope immediately
        this.options = new OptionsWatcher_ts_1.OptionsWatcher(pluginName, configFilePath, this.#logger)
            .on('error', this.#handleError.bind(this))
            .on('change', this.#optionsWatcherChangeListener.bind(this)())
            .on('ready', this.#handleOptionsWatcherReady.bind(this));
    }
    get logger() {
        return this.#logger;
    }
    get appName() {
        return this.#appName;
    }
    get pluginName() {
        return this.#pluginName;
    }
    get directory() {
        return this.#directory;
    }
    get configFilePath() {
        return this.#configFilePath;
    }
    #handleOptionsWatcherReady() {
        // This previously created the default entry handler immediately, but now we wait for the user to call `handleEntry`
        // The issue was that since the component loader was awaiting `scope.ready()` and then calling `pluginModule.handleApplication(scope)`,
        // the default entry handler could start receiving events before the plugin provided its own handler.
        // We could make the user call `await scope.ready()` in their `handleApplication` function, but that could lead to the same issue and it'd
        // be harder for the user to understand why.
        this.emit('ready');
    }
    #handleError(error) {
        this.emit('error', error);
    }
    close() {
        for (const entryHandler of this.#entryHandlers) {
            entryHandler.close();
        }
        this.options.close();
        this.emit('close');
        this.removeAllListeners();
        return this;
    }
    #createEntryHandler(config) {
        const entryHandler = new EntryHandler_ts_1.EntryHandler(this.#pluginName, this.#directory, config, this.#logger)
            .on('error', this.#handleError.bind(this))
            .on('add', this.#defaultEntryHandlerListener('add'))
            .on('change', this.#defaultEntryHandlerListener('change'))
            .on('unlink', this.#defaultEntryHandlerListener('unlink'))
            .on('addDir', this.#defaultEntryHandlerListener('addDir'))
            .on('unlinkDir', this.#defaultEntryHandlerListener('unlinkDir'));
        this.#entryHandlers.push(entryHandler);
        return entryHandler;
    }
    #defaultEntryHandlerListener(event) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const scope = this;
        return function () {
            if (this.listenerCount('all') > 0 || this.listenerCount(event) > 1) {
                return;
            }
            scope.requestRestart();
        };
    }
    #optionsWatcherChangeListener() {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const scope = this;
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        return function handleOptionsWatcherChange(
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        ...[key, _, config]) {
            if (key[0] === 'files' || key[0] === 'urlPath') {
                // TODO: validate options
                // If not entry handler exists then likely the config did not have `files` initially
                // Now, it does, so create a default entry handler.
                if (!scope.#entryHandler) {
                    scope.#entryHandler = scope.#createEntryHandler(config);
                    return;
                }
                // Otherwise, if an entry handler exists, update it with the new config
                scope.#entryHandler.update(config);
                return;
            }
            // If the user isn't handling option changes, request a restart
            if (this.listenerCount('change') > 1) {
                return;
            }
            scope.#logger.debug?.(`Options changed: ${key.join('.')}, requesting restart`);
            scope.requestRestart();
        };
    }
    #getFilesOption() {
        const config = this.options.getAll();
        if (config &&
            typeof config === 'object' &&
            config !== null &&
            !Array.isArray(config) &&
            'files' in config /*&& validate config.files*/) {
            return {
                files: config.files,
                urlPath: config.urlPath,
            };
        }
        return undefined;
    }
    handleEntry(filesOrHandler, handler) {
        let entryHandler;
        // Helper to wrap async handlers for tracking
        const wrapHandler = (targetEntryHandler, entryEventHandler) => {
            const pendingOperations = new Set();
            const wrapped = (entry) => {
                const result = entryEventHandler(entry);
                if (result instanceof Promise) {
                    const tracked = result
                        .catch((error) => {
                        this.#logger.error?.('Error in async entry handler:', error);
                        this.#handleError(error);
                        throw error;
                    })
                        .finally(() => pendingOperations.delete(tracked));
                    pendingOperations.add(tracked);
                }
            };
            // When the entry handler's initial scan completes, wait for all pending async operations
            const initialLoadPromise = (0, node_events_1.once)(targetEntryHandler, 'ready').then(async () => {
                if (pendingOperations.size > 0) {
                    await Promise.all(pendingOperations);
                }
                targetEntryHandler.emit('initialLoadComplete');
            });
            // Track this promise so the component loader can await it
            this.#pendingInitialLoads.add(initialLoadPromise);
            initialLoadPromise.finally(() => this.#pendingInitialLoads.delete(initialLoadPromise));
            return wrapped;
        };
        // No arguments
        if (filesOrHandler === undefined) {
            // If entry handler already exists, return it
            if (this.#entryHandler) {
                entryHandler = this.#entryHandler;
            }
            else {
                // Otherwise, try to create a default entry handler using the files option
                const filesOption = this.#getFilesOption();
                if (filesOption) {
                    this.#entryHandler = this.#createEntryHandler(filesOption);
                    entryHandler = this.#entryHandler;
                }
                else {
                    this.emit('error', new MissingDefaultFilesOptionError());
                    return;
                }
            }
        }
        // Provided a handler function
        else if (typeof filesOrHandler === 'function') {
            // If an entry handler already exists, return it with the handler attached
            if (this.#entryHandler) {
                entryHandler = this.#entryHandler;
            }
            else {
                // Otherwise, try to create a default entry handler using the files option
                const filesOption = this.#getFilesOption();
                if (filesOption) {
                    this.#entryHandler = this.#createEntryHandler(filesOption);
                    entryHandler = this.#entryHandler;
                }
                else {
                    this.emit('error', new MissingDefaultFilesOptionError());
                    return;
                }
            }
            const wrappedHandler = wrapHandler(entryHandler, filesOrHandler);
            entryHandler.on('all', wrappedHandler);
        }
        // otherwise this is a custom config entry handler
        else {
            entryHandler = this.#createEntryHandler(filesOrHandler);
            if (handler) {
                const wrappedHandler = wrapHandler(entryHandler, handler);
                entryHandler.on('all', wrappedHandler);
            }
        }
        return entryHandler;
    }
    requestRestart() {
        this.#logger.debug?.(`Restart requested from ${this.#pluginName} scope for ${this.#appName}`);
        (0, requestRestart_ts_1.requestRestart)();
    }
    /**
     * Wait for all entry handlers' initial loads to complete.
     * This includes waiting for any async operations in entry handler callbacks.
     * Called by the component loader after handleApplication completes.
     */
    async waitForInitialLoads() {
        if (this.#pendingInitialLoads.size > 0) {
            await Promise.all(this.#pendingInitialLoads);
        }
    }
    /**
     * Import a file into the scope's sandbox.
     * @param filePath - The path of the file to import.
     * @returns A promise that resolves with the imported module or value.
     */
    async import(filePath) {
        return this.applicationScope ? this.applicationScope.import(filePath) : import(__rewriteRelativeImportExtension(filePath));
    }
}
exports.Scope = Scope;
//# sourceMappingURL=Scope.js.map