"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 () {
    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.onboard = onboard;
const node_fs_1 = require("node:fs");
const node_http_1 = require("node:http");
const node_path_1 = __importDefault(require("node:path"));
const express_static_gzip_1 = __importDefault(require("express-static-gzip"));
const finalhandler_1 = __importDefault(require("finalhandler"));
const json_stable_stringify_without_jsonify_1 = __importDefault(require("json-stable-stringify-without-jsonify"));
const jszip_1 = __importDefault(require("jszip"));
const adapterDiscovery_1 = require("zigbee-herdsman/dist/adapter/adapterDiscovery");
const data_1 = __importDefault(require("./data"));
const settings = __importStar(require("./settings"));
const yaml_1 = require("./yaml");
/** same as extension/frontend */
const FILE_SERVER_OPTIONS = {
    enableBrotli: true,
    serveStatic: {
        /* v8 ignore start */
        setHeaders: (res, path) => {
            if (path.endsWith("index.html")) {
                res.setHeader("Cache-Control", "no-store");
            }
        },
        /* v8 ignore stop */
    },
};
function getServerUrl() {
    return new URL(process.env.Z2M_ONBOARD_URL ?? "http://0.0.0.0:8080");
}
function getZipEntryTargetPath(entryName) {
    const normalizedEntry = entryName.replace(/\\/g, "/");
    if (!normalizedEntry || normalizedEntry.startsWith("/") || normalizedEntry.includes("\0")) {
        throw new Error(`Invalid ZIP entry path '${entryName}'`);
    }
    const basePath = node_path_1.default.resolve(data_1.default.getPath());
    const targetPath = node_path_1.default.resolve(basePath, normalizedEntry);
    const relativePath = node_path_1.default.relative(basePath, targetPath);
    if (relativePath.startsWith("..") || node_path_1.default.isAbsolute(relativePath)) {
        throw new Error(`Unsafe ZIP entry path '${entryName}'`);
    }
    return targetPath;
}
async function extractZipDataToDataPath(zipContent) {
    const zip = await jszip_1.default.loadAsync(zipContent);
    for (const key in zip.files) {
        const entry = zip.files[key];
        const targetPath = getZipEntryTargetPath(entry.name);
        if (entry.dir) {
            (0, node_fs_1.mkdirSync)(targetPath, { recursive: true });
            continue;
        }
        (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(targetPath), { recursive: true });
        (0, node_fs_1.writeFileSync)(targetPath, await entry.async("nodebuffer"));
    }
}
async function startOnboardingServer() {
    const currentSettings = settings.get();
    const serverUrl = getServerUrl();
    let server;
    const fileServer = (0, express_static_gzip_1.default)((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
    const success = await new Promise((resolve) => {
        server = (0, node_http_1.createServer)(async (req, res) => {
            const pathname = new URL(req.url /* v8 ignore next */ ?? "/", serverUrl).pathname;
            if (req.method === "GET" && pathname === "/data") {
                const payload = {
                    page: "form",
                    settings: currentSettings,
                    settingsSchema: settings.schemaJson,
                    devices: await (0, adapterDiscovery_1.findAllDevices)(),
                };
                res.setHeader("Content-Type", "application/json");
                res.writeHead(200);
                res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                return;
            }
            if (req.method === "POST") {
                if (pathname === "/submit") {
                    let body = "";
                    req.on("data", (chunk) => {
                        body += chunk;
                    });
                    req.on("end", () => {
                        try {
                            const result = (body ? JSON.parse(body) : {});
                            settings.apply(result);
                            const appliedSettings = settings.get();
                            const redirect = !process.env.Z2M_ONBOARD_NO_REDIRECT &&
                                appliedSettings.frontend.enabled &&
                                !appliedSettings.frontend.host?.startsWith("/");
                            const protocol = appliedSettings.frontend.ssl_cert && appliedSettings.frontend.ssl_key ? "https" : "http";
                            const frontendUrl = redirect
                                ? `${protocol}://${appliedSettings.frontend.host ?? "localhost"}:${appliedSettings.frontend.port}${appliedSettings.frontend.base_url}`
                                : null;
                            const payload = { success: true, frontendUrl };
                            res.setHeader("Content-Type", "application/json");
                            res.writeHead(200);
                            res.end((0, json_stable_stringify_without_jsonify_1.default)(payload), () => {
                                resolve(true);
                            });
                        }
                        catch (error) {
                            console.error(`Failed to apply configuration: ${error.message}`);
                            const payload = { success: false, error: error.message };
                            res.setHeader("Content-Type", "application/json");
                            res.writeHead(406);
                            res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                        }
                    });
                    req.on("error", (error) => {
                        console.error(`Failed to parse request body: ${error.message}`);
                        const payload = { success: false, error: error.message };
                        res.setHeader("Content-Type", "application/json");
                        res.writeHead(406);
                        res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                    });
                    return;
                }
                if (pathname === "/submit-zip") {
                    let body = "";
                    req.on("data", (chunk) => {
                        body += chunk;
                    });
                    req.on("end", async () => {
                        try {
                            if (!body) {
                                throw new Error("Invalid ZIP payload: missing content");
                            }
                            const zipContent = Buffer.from(body, "base64");
                            await extractZipDataToDataPath(zipContent);
                            const payload = { success: true, frontendUrl: null };
                            res.setHeader("Content-Type", "application/json");
                            res.writeHead(200);
                            res.end((0, json_stable_stringify_without_jsonify_1.default)(payload), () => {
                                resolve(true);
                            });
                        }
                        catch (error) {
                            console.error(`Failed to apply ZIP data: ${error.message}`);
                            const payload = { success: false, error: error.message };
                            res.setHeader("Content-Type", "application/json");
                            res.writeHead(406);
                            res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                        }
                    });
                    req.on("error", (error) => {
                        console.error(`Failed to parse ZIP request body: ${error.message}`);
                        const payload = { success: false, error: error.message };
                        res.setHeader("Content-Type", "application/json");
                        res.writeHead(406);
                        res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                    });
                    return;
                }
            }
            const next = (0, finalhandler_1.default)(req, res);
            fileServer(req, res, next);
        });
        server.on("error", (error) => {
            console.error("Failed to start onboarding server", error);
            resolve(false);
        });
        server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
            console.log(`Onboarding page is available at ${serverUrl.href}`);
        });
    });
    await new Promise((resolve) => server?.close(resolve));
    return success;
}
async function startFailureServer(errors) {
    const serverUrl = getServerUrl();
    let server;
    const fileServer = (0, express_static_gzip_1.default)((await import("zigbee2mqtt-windfront")).default.getOnboardingPath(), FILE_SERVER_OPTIONS);
    await new Promise((resolve) => {
        server = (0, node_http_1.createServer)((req, res) => {
            const pathname = new URL(req.url /* v8 ignore next */ ?? "/", serverUrl).pathname;
            if (req.method === "GET" && pathname === "/data") {
                const payload = { page: "failure", errors };
                res.setHeader("Content-Type", "application/json");
                res.writeHead(200);
                res.end((0, json_stable_stringify_without_jsonify_1.default)(payload));
                return;
            }
            if (req.method === "POST" && pathname === "/submit") {
                res.writeHead(200);
                res.end(() => {
                    resolve();
                });
                return;
            }
            const next = (0, finalhandler_1.default)(req, res);
            fileServer(req, res, next);
        });
        server.listen(Number.parseInt(serverUrl.port, 10), serverUrl.hostname, () => {
            console.error(`Failure page is available at ${serverUrl.href}`);
        });
    });
    await new Promise((resolve) => server?.close(resolve));
}
async function onSettingsErrors(errors) {
    console.error("\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    console.error("            READ THIS CAREFULLY\n");
    console.error("Refusing to start because configuration is not valid, found the following errors:");
    for (const error of errors) {
        console.error(`- ${error}`);
    }
    console.error("\nIf you don't know how to solve this, read https://www.zigbee2mqtt.io/guide/configuration");
    console.error("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n");
    if (!process.env.Z2M_ONBOARD_NO_SERVER && !process.env.Z2M_ONBOARD_NO_FAILURE_PAGE) {
        await startFailureServer(errors);
    }
}
async function onboard() {
    if (!(0, node_fs_1.existsSync)(data_1.default.getPath())) {
        (0, node_fs_1.mkdirSync)(data_1.default.getPath(), { recursive: true });
    }
    const confExists = (0, node_fs_1.existsSync)(data_1.default.joinPath("configuration.yaml"));
    if (confExists) {
        // initial caching, ensure file is valid yaml first
        try {
            settings.getPersistedSettings();
        }
        catch (error) {
            await onSettingsErrors(error instanceof yaml_1.YAMLFileException
                ? [`Your configuration file: '${error.file}' is invalid (use https://jsonformatter.org/yaml-validator to find and fix the issue)`]
                : [`${error}`]);
            return false;
        }
        // migrate first
        const { migrateIfNecessary } = await import("./settingsMigration.js");
        migrateIfNecessary();
        // make sure existing settings are valid before applying envs
        const errors = settings.validateNonRequired();
        if (errors.length > 0) {
            await onSettingsErrors(errors);
            return false;
        }
        // trigger initial writing of `ZIGBEE2MQTT_CONFIG_*` ENVs
        settings.write();
    }
    else {
        settings.writeMinimalDefaults();
    }
    // use `configuration.yaml` file to detect "brand new install"
    // env allows to re-run onboard even with existing install
    if (!process.env.Z2M_ONBOARD_NO_SERVER && (process.env.Z2M_ONBOARD_FORCE_RUN || !confExists || settings.get().onboarding)) {
        settings.setOnboarding(true);
        const success = await startOnboardingServer();
        if (!success) {
            return false;
        }
    }
    settings.reRead();
    const errors = settings.validate();
    if (errors.length > 0) {
        await onSettingsErrors(errors);
        return false;
    }
    return true;
}
//# sourceMappingURL=data:application/json;base64, 