"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.ConfigEditorService = void 0;
const node_path_1 = require("node:path");
const common_1 = require("@nestjs/common");
const dayjs_1 = __importDefault(require("dayjs"));
const fs_extra_1 = require("fs-extra");
const semver_1 = require("semver");
const config_service_1 = require("../../core/config/config.service");
const logger_service_1 = require("../../core/logger/logger.service");
const scheduler_service_1 = require("../../core/scheduler/scheduler.service");
const plugins_service_1 = require("../plugins/plugins.service");
let ConfigEditorService = class ConfigEditorService {
    constructor(logger, configService, schedulerService, pluginsService) {
        this.logger = logger;
        this.configService = configService;
        this.schedulerService = schedulerService;
        this.pluginsService = pluginsService;
        this.start();
        this.scheduleConfigBackupCleanup();
    }
    async start() {
        await this.ensureBackupPathExists();
        await this.migrateConfigBackups();
    }
    scheduleConfigBackupCleanup() {
        const scheduleRule = new this.schedulerService.RecurrenceRule();
        scheduleRule.hour = 1;
        scheduleRule.minute = 10;
        scheduleRule.second = Math.floor(Math.random() * 59) + 1;
        this.logger.debug(`Next config.json backup cleanup scheduled for ${scheduleRule.nextInvocationDate(new Date()).toString()}.`);
        this.schedulerService.scheduleJob('cleanup-config-backups', scheduleRule, () => {
            this.logger.log('Running job to cleanup config.json backup files older than 60 days...');
            this.cleanupConfigBackups();
        });
    }
    async getConfigFile() {
        const config = await (0, fs_extra_1.readJson)(this.configService.configPath);
        if (!config.bridge || typeof config.bridge !== 'object') {
            config.bridge = {};
        }
        if (!config.accessories || !Array.isArray(config.accessories)) {
            config.accessories = [];
        }
        if (!config.platforms || !Array.isArray(config.platforms)) {
            config.platforms = [];
        }
        return config;
    }
    async updateConfigFile(config) {
        const now = new Date();
        if (!config) {
            config = {};
        }
        if (!config.bridge) {
            config.bridge = {};
        }
        if (typeof config.bridge.port === 'string') {
            config.bridge.port = Number.parseInt(config.bridge.port, 10);
        }
        if (!config.bridge.port || typeof config.bridge.port !== 'number' || config.bridge.port > 65533 || config.bridge.port < 1025) {
            config.bridge.port = Math.floor(Math.random() * (52000 - 51000 + 1) + 51000);
        }
        if (!config.bridge.username) {
            config.bridge.username = this.generateUsername();
        }
        const usernamePattern = /^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$/i;
        if (!usernamePattern.test(config.bridge.username)) {
            if (usernamePattern.test(this.configService.homebridgeConfig.bridge.username)) {
                config.bridge.username = this.configService.homebridgeConfig.bridge.username;
            }
            else {
                config.bridge.username = this.generateUsername();
            }
        }
        if (!config.bridge.pin) {
            config.bridge.pin = this.generatePin();
        }
        const pinPattern = /^\d{3}-\d{2}-\d{3}$/;
        if (!pinPattern.test(config.bridge.pin)) {
            if (pinPattern.test(this.configService.homebridgeConfig.bridge.pin)) {
                config.bridge.pin = this.configService.homebridgeConfig.bridge.pin;
            }
            else {
                config.bridge.pin = this.generatePin();
            }
        }
        if (!config.bridge.name || typeof config.bridge.name !== 'string') {
            config.bridge.name = `Homebridge ${config.bridge.username.substring(config.bridge.username.length - 5).replace(/:/g, '')}`;
        }
        if (!config.accessories || !Array.isArray(config.accessories)) {
            config.accessories = [];
        }
        if (!config.platforms || !Array.isArray(config.platforms)) {
            config.platforms = [];
        }
        if (config.plugins && Array.isArray(config.plugins)) {
            if (!config.plugins.length) {
                delete config.plugins;
            }
        }
        else if (config.plugins) {
            delete config.plugins;
        }
        if (config.mdns && typeof config.mdns !== 'object') {
            delete config.mdns;
        }
        if (config.disabledPlugins && !Array.isArray(config.disabledPlugins)) {
            delete config.disabledPlugins;
        }
        try {
            await (0, fs_extra_1.rename)(this.configService.configPath, (0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${now.getTime().toString()}`));
        }
        catch (e) {
            if (e.code === 'ENOENT') {
                await this.ensureBackupPathExists();
            }
            else {
                this.logger.warn(`Could not create a backup of the config.json file to ${this.configService.configBackupPath} as ${e.message}.`);
            }
        }
        (0, fs_extra_1.writeJsonSync)(this.configService.configPath, config, { spaces: 4 });
        this.logger.log('Changes to config.json saved.');
        const configCopy = JSON.parse(JSON.stringify(config));
        this.configService.parseConfig(configCopy);
        return config;
    }
    async getConfigForPlugin(pluginName) {
        const [plugin, config] = await Promise.all([
            await this.pluginsService.getPluginAlias(pluginName),
            await this.getConfigFile(),
        ]);
        if (!plugin.pluginAlias) {
            return new common_1.BadRequestException('Plugin alias could not be determined.');
        }
        const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms';
        return config[arrayKey].filter((block) => {
            return block[plugin.pluginType] === plugin.pluginAlias
                || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`;
        });
    }
    async updateConfigForPlugin(pluginName, pluginConfig) {
        const [plugin, config] = await Promise.all([
            await this.pluginsService.getPluginAlias(pluginName),
            await this.getConfigFile(),
        ]);
        if (!plugin.pluginAlias) {
            return new common_1.BadRequestException('Plugin alias could not be determined.');
        }
        const arrayKey = plugin.pluginType === 'accessory' ? 'accessories' : 'platforms';
        if (!Array.isArray(pluginConfig)) {
            throw new common_1.BadRequestException('Plugin Config must be an array.');
        }
        for (const block of pluginConfig) {
            if (typeof block !== 'object' || Array.isArray(block)) {
                throw new common_1.BadRequestException('Plugin config must be an array of objects.');
            }
            block[plugin.pluginType] = plugin.pluginAlias;
        }
        let positionIndices;
        if (arrayKey === 'accessories') {
            config.accessories = config.accessories?.filter((block, index) => {
                if (block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`) {
                    positionIndices = index;
                    return false;
                }
                else {
                    return true;
                }
            }) || [];
        }
        else {
            config.platforms = config.platforms?.filter((block, index) => {
                if (block[plugin.pluginType] === plugin.pluginAlias || block[plugin.pluginType] === `${pluginName}.${plugin.pluginAlias}`) {
                    positionIndices = index;
                    return false;
                }
                else {
                    return true;
                }
            }) || [];
        }
        pluginConfig.forEach((block) => {
            if (block._bridge) {
                const isEnvObjAllowed = (0, semver_1.gte)(this.configService.homebridgeVersion, '1.8.0');
                Object.keys(block._bridge).forEach((key) => {
                    if (key === 'env' && isEnvObjAllowed) {
                        Object.keys(block._bridge.env).forEach((envKey) => {
                            if (block._bridge.env[envKey] === undefined || typeof block._bridge.env[envKey] !== 'string' || block._bridge.env[envKey].trim() === '') {
                                delete block._bridge.env[envKey];
                            }
                        });
                        if (Object.keys(block._bridge.env).length === 0) {
                            delete block._bridge.env;
                        }
                    }
                    else {
                        if (block._bridge[key] === undefined || (typeof block._bridge[key] === 'string' && block._bridge[key].trim() === '')) {
                            delete block._bridge[key];
                        }
                    }
                });
            }
        });
        if (arrayKey === 'accessories') {
            if (positionIndices !== undefined) {
                config.accessories?.splice(positionIndices, 0, ...pluginConfig);
            }
            else {
                config.accessories?.push(...pluginConfig);
            }
        }
        else {
            if (positionIndices !== undefined) {
                config.platforms?.splice(positionIndices, 0, ...pluginConfig);
            }
            else {
                config.platforms?.push(...pluginConfig);
            }
        }
        await this.updateConfigFile(config);
        return pluginConfig;
    }
    async setPropertyForUi(property, value) {
        if (property === 'platform') {
            throw new common_1.BadRequestException('Cannot update the platform property.');
        }
        const config = await this.getConfigFile();
        const pluginConfig = config.platforms.find(x => x.platform === 'config');
        const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];
        if (property.includes('.')) {
            const properties = property.split('.');
            let current = pluginConfig;
            for (let i = 0; i < properties.length - 1; i += 1) {
                if (!forbiddenKeys.includes(properties[i])) {
                    if (!current[properties[i]]) {
                        current[properties[i]] = {};
                    }
                    current = current[properties[i]];
                }
            }
            const finalProperty = properties[properties.length - 1];
            if (!forbiddenKeys.includes(finalProperty)) {
                current[finalProperty] = value;
            }
        }
        else {
            if (!forbiddenKeys.includes(property)) {
                pluginConfig[property] = value;
            }
        }
        config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig);
        await this.updateConfigFile(config);
    }
    async setAccessoryControlInstanceBlacklist(value) {
        const config = await this.getConfigFile();
        const pluginConfig = config.platforms.find(x => x.platform === 'config');
        if (!pluginConfig.accessoryControl) {
            pluginConfig.accessoryControl = {};
        }
        pluginConfig.accessoryControl.instanceBlacklist = (value || [])
            .filter(x => typeof x === 'string' && x.trim() !== '' && /^(?:[A-F0-9]{2}:){5}[A-F0-9]{2}$/i.test(x.trim()))
            .map(x => x.trim().toUpperCase())
            .sort((a, b) => a.localeCompare(b));
        config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig);
        await this.updateConfigFile(config);
    }
    async setPluginsHideUpdatesFor(value) {
        const config = await this.getConfigFile();
        const pluginConfig = config.platforms.find(x => x.platform === 'config');
        if (!pluginConfig.plugins) {
            pluginConfig.plugins = {};
        }
        pluginConfig.plugins.hideUpdatesFor = (value || [])
            .filter(x => typeof x === 'string' && x.trim() !== '' && /^(?:@[\w-]+(?:\.[\w-]+)*\/)?homebridge-[\w-]+$/i.test(x.trim()))
            .map(x => x.trim().toLowerCase());
        config.platforms[config.platforms.findIndex(x => x.platform === 'config')] = this.cleanUpUiConfig(pluginConfig);
        await this.updateConfigFile(config);
    }
    async disablePlugin(pluginName) {
        if (pluginName === this.configService.name) {
            throw new common_1.BadRequestException('Disabling this plugin is now allowed.');
        }
        const config = await this.getConfigFile();
        if (!Array.isArray(config.disabledPlugins)) {
            config.disabledPlugins = [];
        }
        config.disabledPlugins.push(pluginName);
        await this.updateConfigFile(config);
        return config.disabledPlugins;
    }
    async enablePlugin(pluginName) {
        const config = await this.getConfigFile();
        if (!Array.isArray(config.disabledPlugins)) {
            config.disabledPlugins = [];
        }
        const idx = config.disabledPlugins.findIndex(x => x === pluginName);
        if (idx > -1) {
            config.disabledPlugins.splice(idx, 1);
            await this.updateConfigFile(config);
        }
        return config.disabledPlugins;
    }
    async listConfigBackups() {
        const dirContents = await (0, fs_extra_1.readdir)(this.configService.configBackupPath);
        return dirContents
            .filter(x => x.match(/^config.json.\d{09,15}/))
            .sort()
            .reverse()
            .map((x) => {
            const ext = x.split('.');
            if (ext.length === 3 && !Number.isNaN(ext[2])) {
                return {
                    id: ext[2],
                    timestamp: new Date(Number.parseInt(ext[2], 10)),
                    file: x,
                };
            }
            else {
                return null;
            }
        })
            .filter(x => x && !Number.isNaN(x.timestamp.getTime()));
    }
    async getConfigBackup(backupId) {
        const requestedBackupPath = (0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${backupId}`);
        if (!await (0, fs_extra_1.pathExists)(requestedBackupPath)) {
            throw new common_1.NotFoundException(`Backup ${backupId} Not Found`);
        }
        return await (0, fs_extra_1.readFile)(requestedBackupPath);
    }
    async deleteConfigBackup(backupId) {
        const requestedBackupPath = (0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${backupId}`);
        if (!await (0, fs_extra_1.pathExists)(requestedBackupPath)) {
            throw new common_1.NotFoundException(`Backup ${backupId} Not Found`);
        }
        await (0, fs_extra_1.unlink)((0, node_path_1.resolve)(this.configService.configBackupPath, `config.json.${backupId}`));
    }
    async deleteAllConfigBackups() {
        const backups = await this.listConfigBackups();
        for (const backupFile of backups) {
            await (0, fs_extra_1.unlink)((0, node_path_1.resolve)(this.configService.configBackupPath, backupFile.file));
        }
    }
    async ensureBackupPathExists() {
        try {
            await (0, fs_extra_1.ensureDir)(this.configService.configBackupPath);
        }
        catch (e) {
            this.logger.error(`Could not create directory for config backups ${this.configService.configBackupPath} as ${e.message}.`);
            this.logger.error(`Config backups will continue to use ${this.configService.storagePath}.`);
            this.configService.configBackupPath = this.configService.storagePath;
        }
    }
    async cleanupConfigBackups() {
        try {
            const backups = await this.listConfigBackups();
            for (const backup of backups) {
                if ((0, dayjs_1.default)().diff((0, dayjs_1.default)(backup.timestamp), 'day') >= 60) {
                    await (0, fs_extra_1.remove)((0, node_path_1.resolve)(this.configService.configBackupPath, backup.file));
                }
            }
        }
        catch (e) {
            this.logger.warn(`Failed to cleanup old config.json backup files as ${e.message}`);
        }
    }
    async migrateConfigBackups() {
        try {
            if (this.configService.configBackupPath === this.configService.storagePath) {
                this.logger.error('Skipping migration of existing config.json backups...');
                return;
            }
            const dirContents = await (0, fs_extra_1.readdir)(this.configService.storagePath);
            const backups = dirContents
                .filter(x => x.match(/^config.json.\d{09,15}/))
                .sort()
                .reverse();
            for (const backupFileName of backups.splice(0, 100)) {
                const sourcePath = (0, node_path_1.resolve)(this.configService.storagePath, backupFileName);
                const targetPath = (0, node_path_1.resolve)(this.configService.configBackupPath, backupFileName);
                await (0, fs_extra_1.move)(sourcePath, targetPath, { overwrite: true });
            }
            for (const backupFileName of backups) {
                const sourcePath = (0, node_path_1.resolve)(this.configService.storagePath, backupFileName);
                await (0, fs_extra_1.remove)(sourcePath);
            }
        }
        catch (e) {
            this.logger.warn(`Migrating config.json backups to new location failed as ${e.message}.`);
        }
    }
    generatePin() {
        let code = `${Math.floor(10000000 + Math.random() * 90000000)}`;
        code = code.split('');
        code.splice(3, 0, '-');
        code.splice(6, 0, '-');
        code = code.join('');
        return code;
    }
    generateUsername() {
        const hexDigits = '0123456789ABCDEF';
        let username = '0E:';
        for (let i = 0; i < 5; i += 1) {
            username += hexDigits.charAt(Math.round(Math.random() * 15));
            username += hexDigits.charAt(Math.round(Math.random() * 15));
            if (i !== 4) {
                username += ':';
            }
        }
        return username;
    }
    removeEmpty(obj) {
        Object.keys(obj).forEach((key) => {
            const value = obj[key];
            if (value === '' || value === null || value === undefined || value === false || (Array.isArray(value) && value.length === 0)) {
                delete obj[key];
            }
            else if (typeof value === 'object') {
                this.removeEmpty(value);
                if (Object.keys(value).length === 0) {
                    delete obj[key];
                }
            }
        });
    }
    cleanUpUiConfig(uiConfig) {
        const { name, platform, ...rest } = uiConfig;
        const cleanedUiConfig = {
            name,
            platform: platform || 'config',
            ...rest,
        };
        this.removeEmpty(cleanedUiConfig);
        return cleanedUiConfig;
    }
};
exports.ConfigEditorService = ConfigEditorService;
exports.ConfigEditorService = ConfigEditorService = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [logger_service_1.Logger,
        config_service_1.ConfigService,
        scheduler_service_1.SchedulerService,
        plugins_service_1.PluginsService])
], ConfigEditorService);
//# sourceMappingURL=config-editor.service.js.map