"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessManager = void 0;
const crypto_1 = require("crypto");
const BackendManager_1 = require("./BackendManager");
const ViewManager_1 = require("../view-manager/ViewManager");
const ipc_1 = require("../ipc");
const PackageManager_1 = require("../package-manager/PackageManager");
const AppChannelManager_1 = require("../appbus/AppChannelManager");
const tsyringe_1 = require("tsyringe");
const ipc_2 = require("../ipc");
const ProcessHandler_1 = require("./ProcessHandler");
/**
 * ProcessManager
 *
 * Handles app lifecycle (launch, stop) and coordination between workers and views.
 */
let ProcessManager = class ProcessManager extends ipc_2.EdenEmitter {
    backendManager;
    viewManager;
    packageManager;
    appChannelManager;
    runningApps = new Map();
    appsDirectory;
    processHandler;
    constructor(backendManager, viewManager, ipcBridge, packageManager, appChannelManager, appsDirectory, commandRegistry) {
        super(ipcBridge);
        this.backendManager = backendManager;
        this.viewManager = viewManager;
        this.packageManager = packageManager;
        this.appChannelManager = appChannelManager;
        this.appsDirectory = appsDirectory;
        this.setupEventHandlers();
        // Create and register handler
        this.processHandler = new ProcessHandler_1.ProcessHandler(this);
        commandRegistry.registerManager(this.processHandler);
    }
    /**
     * Setup event handlers
     */
    setupEventHandlers() {
        // Handle backend errors
        this.backendManager.on("backend-error", ({ appId, error }) => {
            console.error(`Backend error for app ${appId}:`, error);
            this.handleAppError(appId, error);
        });
        // Handle backend exits
        this.backendManager.on("backend-exit", ({ appId, code }) => {
            console.log(`Backend exited for app ${appId} with code ${code}`);
            this.handleAppExit(appId, code);
        });
        // Transfer backend port to frontend when view loads
        // Subscribe via ipcBridge since ViewManager emits via EdenEmitter
        this.ipcBridge.eventSubscribers.subscribeInternal("view/view-loaded", ({ viewId, appId }) => {
            console.log(`View loaded for app ${appId}`);
            // If app has a backend, transfer the port to the frontend
            const backendPort = this.backendManager.getFrontendPort(appId);
            if (backendPort) {
                const viewInfo = this.viewManager.getViewInfo(viewId);
                if (viewInfo) {
                    console.log(`Transferring backend port to view ${viewId} for app ${appId}`);
                    viewInfo.view.webContents.postMessage("backend-port", {}, [
                        backendPort,
                    ]);
                    // Port has been transferred
                }
            }
            else {
                console.log(`No backend port for app ${appId} (may be frontend-only)`);
            }
        });
    }
    /**
     * Launch an app
     */
    async launchApp(appId, bounds, launchArgs) {
        const manifest = this.packageManager.getAppManifest(appId);
        if (!manifest) {
            throw new Error(`App ${appId} is not installed`);
        }
        // Validate manifest has at least frontend or backend
        if (!manifest.frontend?.entry && !manifest.backend?.entry) {
            throw new Error(`App ${appId} must have at least a frontend or backend entry`);
        }
        // Check if already running
        if (this.runningApps.has(appId)) {
            throw new Error(`App ${appId} is already running`);
        }
        // Get the correct install path
        const installPath = this.packageManager.getAppPath(appId);
        if (!installPath) {
            throw new Error(`App path not found for ${appId}`);
        }
        const instanceId = (0, crypto_1.randomUUID)();
        try {
            // Create backend utility process if one is defined
            if (manifest.backend?.entry) {
                await this.backendManager.createBackend(appId, manifest, installPath, launchArgs);
            }
            // Create view for frontend only if frontend is defined
            let viewId;
            if (manifest.frontend?.entry) {
                viewId = this.viewManager.createView(appId, manifest, installPath, bounds, launchArgs);
            }
            // Create app instance
            const instance = {
                manifest,
                instanceId,
                installPath,
                viewId: viewId ?? -1, // -1 indicates no view (backend-only)
                state: "running",
                installedAt: new Date(),
                lastLaunched: new Date(),
            };
            this.runningApps.set(appId, instance);
            this.syncRunningAppsState();
            this.notify("launched", { instance });
            // Return serializable data only
            return {
                success: true,
                instanceId,
                appId,
            };
        }
        catch (error) {
            console.error(`Failed to launch app ${appId}:`, error);
            throw error;
        }
    }
    /**
     * Stop a running app
     */
    async stopApp(appId) {
        const instance = this.runningApps.get(appId);
        if (!instance) {
            throw new Error(`App ${appId} is not running`);
        }
        try {
            // Unregister all services exposed by this app
            this.appChannelManager.unregisterAllServices(appId);
            // Remove view first (before backend termination to avoid race)
            if (instance.viewId !== -1) {
                this.viewManager.removeView(instance.viewId);
            }
            // Terminate backend after view is removed
            if (this.backendManager.hasBackend(appId)) {
                await this.backendManager.terminateBackend(appId);
            }
            // Remove from running apps
            this.runningApps.delete(appId);
            this.syncRunningAppsState();
            this.notify("stopped", { appId });
        }
        catch (error) {
            console.error(`Failed to stop app ${appId}:`, error);
            throw error;
        }
    }
    /**
     * Get list of running apps
     * @param showHidden - If true, includes overlay apps and daemons (hidden by default)
     */
    getRunningApps(showHidden = false) {
        const apps = Array.from(this.runningApps.values());
        return showHidden
            ? apps
            : apps.filter((app) => !app.manifest.overlay && !!app.manifest.frontend?.entry);
    }
    /**
     * Get app instance
     */
    getAppInstance(appId) {
        return this.runningApps.get(appId);
    }
    /**
     * Sync running app IDs with the IPC bridge
     */
    syncRunningAppsState() {
        this.ipcBridge.updateRunningApps(this.runningApps.keys());
    }
    /**
     * Handle app error
     */
    handleAppError(appId, error) {
        const instance = this.runningApps.get(appId);
        if (instance) {
            instance.state = "error";
            this.notify("error", { appId, error });
        }
    }
    /**
     * Handle app exit (called when backend crashes or exits unexpectedly)
     * Note: If stopApp() was called, the instance is already removed so this is a no-op
     */
    handleAppExit(appId, code) {
        const instance = this.runningApps.get(appId);
        if (!instance) {
            // App was already cleaned up by stopApp(), nothing to do
            return;
        }
        // Unregister all services exposed by this app
        this.appChannelManager.unregisterAllServices(appId);
        // Clean up view (only if frontend exists, viewId !== -1)
        if (instance.viewId !== -1) {
            try {
                this.viewManager.removeView(instance.viewId);
            }
            catch (e) {
                // View may already be removed
            }
        }
        this.runningApps.delete(appId);
        this.syncRunningAppsState();
        this.notify("exited", { appId, code });
    }
    /**
     * Shutdown all apps
     */
    async shutdown() {
        const runningAppIds = Array.from(this.runningApps.keys());
        console.log(`Stopping ${runningAppIds.length} running app(s)...`);
        // Stop all apps sequentially and wait for each to complete
        for (const appId of runningAppIds) {
            try {
                console.log(`Stopping app: ${appId}`);
                await this.stopApp(appId);
                console.log(`✓ Stopped app: ${appId}`);
            }
            catch (error) {
                console.error(`Failed to stop app ${appId}:`, error);
            }
        }
    }
    /**
     * Reload a running app
     */
    async reloadApp(appId) {
        const instance = this.runningApps.get(appId);
        if (!instance) {
            console.log(`App ${appId} is not running, skipping reload`);
            return;
        }
        // Save the current view bounds
        const viewInfo = this.viewManager.getViewInfo(instance.viewId);
        const bounds = viewInfo ? viewInfo.view.getBounds() : undefined;
        console.log(`Reloading app ${appId}...`);
        // Stop the app
        await this.stopApp(appId);
        // Small delay to ensure cleanup
        await new Promise((resolve) => setTimeout(resolve, 100));
        // Relaunch with same bounds
        await this.launchApp(appId, bounds);
        console.log(`App ${appId} reloaded successfully`);
    }
};
exports.ProcessManager = ProcessManager;
exports.ProcessManager = ProcessManager = __decorate([
    (0, tsyringe_1.singleton)(),
    (0, tsyringe_1.injectable)(),
    (0, ipc_2.EdenNamespace)("process"),
    __param(0, (0, tsyringe_1.inject)(BackendManager_1.BackendManager)),
    __param(1, (0, tsyringe_1.inject)(ViewManager_1.ViewManager)),
    __param(2, (0, tsyringe_1.inject)(ipc_1.IPCBridge)),
    __param(3, (0, tsyringe_1.inject)(PackageManager_1.PackageManager)),
    __param(4, (0, tsyringe_1.inject)(AppChannelManager_1.AppChannelManager)),
    __param(5, (0, tsyringe_1.inject)("appsDirectory")),
    __param(6, (0, tsyringe_1.inject)(ipc_2.CommandRegistry)),
    __metadata("design:paramtypes", [BackendManager_1.BackendManager,
        ViewManager_1.ViewManager,
        ipc_1.IPCBridge,
        PackageManager_1.PackageManager,
        AppChannelManager_1.AppChannelManager, String, ipc_2.CommandRegistry])
], ProcessManager);
//# sourceMappingURL=ProcessManager.js.map