"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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PluginsSettingsUiService = void 0;
const node_child_process_1 = require("node:child_process");
const node_path_1 = require("node:path");
const node_process_1 = __importDefault(require("node:process"));
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const fs_extra_1 = require("fs-extra");
const node_cache_1 = __importDefault(require("node-cache"));
const rxjs_1 = require("rxjs");
const config_service_1 = require("../../../core/config/config.service");
const logger_service_1 = require("../../../core/logger/logger.service");
const plugins_service_1 = require("../../plugins/plugins.service");
let PluginsSettingsUiService = class PluginsSettingsUiService {
    constructor(loggerService, pluginsService, configService, httpService) {
        this.loggerService = loggerService;
        this.pluginsService = pluginsService;
        this.configService = configService;
        this.httpService = httpService;
        this.pluginUiMetadataCache = new node_cache_1.default({ stdTTL: 86400 });
        this.pluginUiLastVersionCache = new node_cache_1.default({ stdTTL: 86400 });
    }
    async serveCustomUiAsset(reply, pluginName, assetPath, origin, version) {
        try {
            if (!assetPath) {
                assetPath = 'index.html';
            }
            if (assetPath === 'index.html' && version) {
                if (version !== this.pluginUiLastVersionCache.get(pluginName)) {
                    this.pluginUiMetadataCache.del(pluginName);
                }
            }
            const pluginUi = this.pluginUiMetadataCache.get(pluginName)
                || (await this.getPluginUiMetadata(pluginName));
            const safeSuffix = (0, node_path_1.normalize)(assetPath).replace(/^(\.\.(\/|\\|$))+/, '');
            const filePath = (0, node_path_1.join)(pluginUi.publicPath, safeSuffix);
            if (!filePath.startsWith((0, node_path_1.resolve)(pluginUi.publicPath))) {
                return reply.code(404).send('Not Found');
            }
            reply.header('Content-Security-Policy', '');
            if (assetPath === 'index.html') {
                return reply
                    .type('text/html')
                    .header('Origin-Agent-Cluster', '?1')
                    .send(await this.buildIndexHtml(pluginUi, origin));
            }
            if (pluginUi.devServer) {
                return await this.serveAssetsFromDevServer(reply, pluginUi, assetPath);
            }
            const fallbackPath = (0, node_path_1.resolve)(node_process_1.default.env.UIX_BASE_PATH, 'public', (0, node_path_1.basename)(filePath));
            if (await (0, fs_extra_1.pathExists)(filePath)) {
                return reply.sendFile((0, node_path_1.basename)(filePath), (0, node_path_1.dirname)(filePath));
            }
            else if (fallbackPath.match(/^.*\.(jpe?g|gif|png|svg|ttf|woff2|css)$/i) && await (0, fs_extra_1.pathExists)(fallbackPath)) {
                return reply.sendFile((0, node_path_1.basename)(fallbackPath), (0, node_path_1.dirname)(fallbackPath));
            }
            else {
                this.loggerService.warn(`[${pluginName}] asset not found: ${assetPath}.`);
                return reply.code(404).send('Not Found');
            }
        }
        catch (e) {
            this.loggerService.error(`[${pluginName}] UI threw an error - ${e.message}.`);
            return e.message === 'Not Found' ? reply.code(404).send(e.message) : reply.code(500).send(e.message);
        }
    }
    async getPluginUiMetadata(pluginName) {
        try {
            const pluginUi = await this.pluginsService.getPluginUiMetadata(pluginName);
            this.pluginUiMetadataCache.set(pluginName, pluginUi);
            this.pluginUiLastVersionCache.set(pluginName, pluginUi.plugin.installedVersion);
            return pluginUi;
        }
        catch (e) {
            this.loggerService.warn(`[${pluginName}] custom UI threw an error - ${e.message}.`);
            throw new common_1.NotFoundException();
        }
    }
    async serveAssetsFromDevServer(reply, pluginUi, assetPath) {
        try {
            const response = await (0, rxjs_1.firstValueFrom)(this.httpService.get(`${pluginUi.devServer}/${assetPath}`, { responseType: 'text' }));
            for (const [key, value] of Object.entries(response.headers)) {
                reply.header(key, value);
            }
            reply.send(response.data);
        }
        catch {
            reply.code(404).send('Not Found');
        }
    }
    async getIndexHtmlBody(pluginUi) {
        if (pluginUi.devServer) {
            return (await (0, rxjs_1.firstValueFrom)(this.httpService.get(pluginUi.devServer, { responseType: 'text' }))).data;
        }
        else {
            return await (0, fs_extra_1.readFile)((0, node_path_1.join)(pluginUi.publicPath, 'index.html'), 'utf8');
        }
    }
    async buildIndexHtml(pluginUi, origin) {
        const body = await this.getIndexHtmlBody(pluginUi);
        return `
      <!doctype html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>${pluginUi.plugin.name}</title>
          <meta name="viewport" content="width=device-width, initial-scale=1">
          <script>
          window._homebridge = {
            plugin: ${JSON.stringify(pluginUi.plugin)},
            serverEnv: ${JSON.stringify(this.configService.uiSettings(true))},
          };
          </script>
          <script src="${origin || 'http://localhost:4200'}${origin ? (this.configService.ui.webroot || '') : ''}/assets/plugin-ui-utils/ui.js?v=${this.configService.package.version}"></script>
          <script>
            window.addEventListener('load', () => {
              window.parent.postMessage({action: 'loaded'}, '*');
            }, false)
          </script>
        </head>
        <body style="display:none;">
          ${body}
        </body>
      </html>
    `;
    }
    async startCustomUiHandler(pluginName, client) {
        const pluginUi = this.pluginUiMetadataCache.get(pluginName)
            || (await this.getPluginUiMetadata(pluginName));
        if (!await (0, fs_extra_1.pathExists)((0, node_path_1.resolve)(pluginUi.serverPath))) {
            client.emit('ready', { server: false });
            return;
        }
        const childEnv = Object.assign({}, node_process_1.default.env);
        childEnv.HOMEBRIDGE_STORAGE_PATH = this.configService.storagePath;
        childEnv.HOMEBRIDGE_CONFIG_PATH = this.configService.configPath;
        childEnv.HOMEBRIDGE_UI_VERSION = this.configService.package.version;
        const child = (0, node_child_process_1.fork)(pluginUi.serverPath, [], {
            silent: true,
            env: childEnv,
        });
        child.stdout.on('data', (data) => {
            this.loggerService.log(`[${pluginName}] ${data.toString().trim()}`);
        });
        child.stderr.on('data', (data) => {
            this.loggerService.error(`[${pluginName}] ${data.toString().trim()}`);
        });
        child.on('exit', () => {
            this.loggerService.debug(`[${pluginName}] custom UI closed (child process ended).`);
        });
        child.addListener('message', (response) => {
            if (typeof response === 'object' && response.action) {
                response.action = response.action === 'error' ? 'server_error' : response.action;
                client.emit(response.action, response.payload);
            }
        });
        const cleanup = () => {
            this.loggerService.debug(`[${pluginName}] custom UI closing (terminating child process)...`);
            const childPid = child.pid;
            if (child.connected) {
                child.disconnect();
            }
            setTimeout(() => {
                try {
                    node_process_1.default.kill(childPid, 'SIGTERM');
                }
                catch (e) { }
            }, 5000);
            client.removeAllListeners('end');
            client.removeAllListeners('disconnect');
            client.removeAllListeners('request');
        };
        client.on('disconnect', () => {
            cleanup();
        });
        client.on('end', () => {
            cleanup();
        });
        client.on('request', (request) => {
            if (child.connected) {
                child.send(request);
            }
        });
    }
};
exports.PluginsSettingsUiService = PluginsSettingsUiService;
exports.PluginsSettingsUiService = PluginsSettingsUiService = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [logger_service_1.Logger,
        plugins_service_1.PluginsService,
        config_service_1.ConfigService,
        axios_1.HttpService])
], PluginsSettingsUiService);
//# sourceMappingURL=plugins-settings-ui.service.js.map