"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 __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 __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __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.Frontend = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_fs_1 = require("node:fs");
const node_http_1 = require("node:http");
const node_https_1 = require("node:https");
const node_path_1 = require("node:path");
const bind_decorator_1 = __importDefault(require("bind-decorator"));
const ws_1 = __importDefault(require("ws"));
const data_1 = __importDefault(require("../util/data"));
const logger_1 = __importDefault(require("../util/logger"));
const settings = __importStar(require("../util/settings"));
const staticFileServer_1 = require("../util/staticFileServer");
const stringify_1 = require("../util/stringify");
const utils_1 = __importDefault(require("../util/utils"));
const extension_1 = __importDefault(require("./extension"));
/**
 * This extension servers the frontend
 */
class Frontend extends extension_1.default {
    mqttBaseTopic;
    server;
    wss;
    baseUrl;
    constructor(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension) {
        super(zigbee, mqtt, state, publishEntityState, eventBus, enableDisableExtension, restartCallback, addExtension);
        const frontendSettings = settings.get().frontend;
        (0, node_assert_1.default)(frontendSettings.enabled, `Frontend extension created with setting 'enabled: false'`);
        this.baseUrl = frontendSettings.base_url;
        this.mqttBaseTopic = settings.get().mqtt.base_topic;
    }
    async start() {
        if (settings.get().frontend.disable_ui_serving) {
            const { host, port } = settings.get().frontend;
            this.wss = new ws_1.default.Server({ port, host, path: node_path_1.posix.join(this.baseUrl, "api") });
            logger_1.default.info(
            /* v8 ignore next */
            `Frontend UI serving is disabled. WebSocket at: ${this.wss.options.host ?? "0.0.0.0"}:${this.wss.options.port}${this.wss.options.path}`);
        }
        else {
            const { host, port, ssl_key: sslKey, ssl_cert: sslCert } = settings.get().frontend;
            const hasSSL = (val, key) => {
                if (val) {
                    if ((0, node_fs_1.existsSync)(val)) {
                        return true;
                    }
                    logger_1.default.error(`Defined ${key} '${val}' file path does not exists, server won't be secured.`);
                }
                return false;
            };
            const frontend = (await import(settings.get().frontend.package));
            const logError = logger_1.default.error.bind(logger_1.default);
            const fileServer = (0, staticFileServer_1.createStaticFileServer)(frontend.default.getPath(), logError);
            const deviceIconsFileServer = (0, staticFileServer_1.createStaticFileServer)(data_1.default.joinPath("device_icons"), logError);
            const onRequest = (request, response) => {
                // biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
                const url = request.url;
                const newUrl = node_path_1.posix.relative(this.baseUrl, url);
                // The request url is not within the frontend base url, so the relative path starts with '..'
                if (newUrl.startsWith(".")) {
                    (0, staticFileServer_1.sendNotFound)(request, response);
                    return;
                }
                // The base url itself is a directory, redirect to its trailing slash form so the browser resolves the
                // relative asset paths in `index.html` against the frontend root instead of against its parent.
                if (newUrl === "" && !url.endsWith("/")) {
                    response.writeHead(301, { Location: `${url}/` });
                    response.end();
                    return;
                }
                request.url = `/${newUrl}`;
                if (newUrl.startsWith("device_icons/")) {
                    request.url = request.url.replace("/device_icons", "");
                    deviceIconsFileServer(request, response);
                }
                else {
                    fileServer(request, response);
                }
            };
            if (hasSSL(sslKey, "ssl_key") && hasSSL(sslCert, "ssl_cert")) {
                const serverOptions = { key: (0, node_fs_1.readFileSync)(sslKey), cert: (0, node_fs_1.readFileSync)(sslCert) };
                this.server = (0, node_https_1.createServer)(serverOptions, onRequest);
            }
            else {
                this.server = (0, node_http_1.createServer)(onRequest);
            }
            this.server.on("upgrade", this.onUpgrade);
            if (!host) {
                this.server.listen(port);
                logger_1.default.info(`Started frontend on port ${port}`);
            }
            else if (host.startsWith("/")) {
                this.server.listen(host);
                logger_1.default.info(`Started frontend on socket ${host}`);
            }
            else {
                this.server.listen(port, host);
                logger_1.default.info(`Started frontend on port ${host}:${port}`);
            }
            this.wss = new ws_1.default.Server({ noServer: true, path: node_path_1.posix.join(this.baseUrl, "api") });
        }
        this.wss.on("connection", this.onWebSocketConnection);
        this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessageOrEntityState);
        this.eventBus.onPublishEntityState(this, this.onMQTTPublishMessageOrEntityState);
    }
    async stop() {
        await super.stop();
        if (this.wss) {
            for (const client of this.wss.clients) {
                client.send((0, stringify_1.stringify)({ topic: "bridge/state", payload: { state: "offline" } }));
                client.terminate();
            }
            this.wss.close();
        }
        await new Promise((resolve) => (this.server ? this.server.close(resolve) : resolve(undefined)));
    }
    onUpgrade(request, socket, head) {
        this.wss.handleUpgrade(request, socket, head, (ws) => {
            // biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
            const { searchParams } = new URL(request.url, "http://localhost"); // dummy base, may not be absolute
            const authToken = settings.get().frontend.auth_token;
            if (!authToken || authToken === searchParams.get("token")) {
                this.wss.emit("connection", ws, request);
            }
            else {
                ws.close(4401, "Unauthorized");
            }
        });
    }
    onWebSocketConnection(ws) {
        ws.on("error", (msg) => logger_1.default.error(`WebSocket error: ${msg.message}`));
        ws.on("message", (data, isBinary) => {
            if (!isBinary && data) {
                const message = data.toString();
                const { topic, payload } = JSON.parse(message);
                this.mqtt.onMessage(`${this.mqttBaseTopic}/${topic}`, Buffer.from((0, stringify_1.stringify)(payload)));
            }
        });
        for (const [topic, payload] of Object.entries(this.mqtt.retainedMessages)) {
            if (topic.startsWith(`${this.mqttBaseTopic}/`)) {
                ws.send((0, stringify_1.stringify)({
                    // Send topic without base_topic
                    topic: topic.substring(this.mqttBaseTopic.length + 1),
                    payload: utils_1.default.parseJSON(payload.payload, payload.payload),
                }));
            }
        }
        for (const device of this.zigbee.devicesIterator(utils_1.default.deviceNotCoordinator)) {
            const payload = this.state.get(device);
            const lastSeen = settings.get().advanced.last_seen;
            if (lastSeen !== "disable") {
                payload.last_seen = utils_1.default.formatDate(device.zh.lastSeen ?? /* v8 ignore next */ 0, lastSeen);
            }
            if (device.zh.linkquality !== undefined) {
                payload.linkquality = device.zh.linkquality;
            }
            ws.send((0, stringify_1.stringify)({ topic: device.name, payload }));
        }
    }
    onMQTTPublishMessageOrEntityState(data) {
        let topic;
        let payload;
        if ("topic" in data) {
            // MQTTMessagePublished
            if (data.options.meta.isEntityState || !data.topic.startsWith(`${this.mqttBaseTopic}/`)) {
                // Don't send entity state to frontend on `MQTTMessagePublished` event, this is handled by
                // `PublishEntityState` instead. Reason for this is to skip attribute messages when `output` is
                // set to `attribute` or `attribute_and_json`, we only want to send JSON entity states to the
                // frontend.
                return;
            }
            // Send topic without base_topic
            topic = data.topic.substring(this.mqttBaseTopic.length + 1);
            payload = utils_1.default.parseJSON(data.payload, data.payload);
        }
        else {
            // PublishEntityState
            topic = data.entity.name;
            payload = data.message;
        }
        for (const client of this.wss.clients) {
            if (client.readyState === ws_1.default.OPEN) {
                client.send((0, stringify_1.stringify)({ topic, payload }));
            }
        }
    }
}
exports.Frontend = Frontend;
__decorate([
    bind_decorator_1.default
], Frontend.prototype, "onUpgrade", null);
__decorate([
    bind_decorator_1.default
], Frontend.prototype, "onWebSocketConnection", null);
__decorate([
    bind_decorator_1.default
], Frontend.prototype, "onMQTTPublishMessageOrEntityState", null);
exports.default = Frontend;
//# sourceMappingURL=data:application/json;base64, 