"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.migrateIfNecessary = migrateIfNecessary;
const node_fs_1 = require("node:fs");
const data_1 = __importDefault(require("./data"));
const settings = __importStar(require("./settings"));
const utils_1 = __importDefault(require("./utils"));
const SUPPORTED_VERSIONS = [undefined, 2, 3, 4, settings.CURRENT_VERSION];
function backupSettings(version) {
    const filePath = data_1.default.joinPath("configuration.yaml");
    (0, node_fs_1.copyFileSync)(filePath, filePath.replace(".yaml", `_backup_v${version}.yaml`));
}
/**
 * Set the given path in given settings to given value. If requested, create path.
 *
 * @param currentSettings
 * @param path
 * @param value
 * @param createPathIfNotExist
 * @returns Returns true if value was set, false if not.
 */
// biome-ignore lint/suspicious/noExplicitAny: auto-parsing
function setValue(currentSettings, path, value, createPathIfNotExist = false) {
    for (let i = 0; i < path.length; i++) {
        const key = path[i];
        if (i === path.length - 1) {
            currentSettings[key] = value;
        }
        else {
            if (!currentSettings[key]) {
                if (createPathIfNotExist) {
                    currentSettings[key] = {};
                    /* v8 ignore start */
                }
                else {
                    // invalid path
                    // ignored in test since currently call is always guarded by get-validated path, so this is never reached
                    return false;
                }
                /* v8 ignore stop */
            }
            currentSettings = currentSettings[key];
        }
    }
    return true;
}
/**
 * Get the value at the given path in given settings.
 *
 * @param currentSettings
 * @param path
 * @returns
 *   - true if path was valid
 *   - the value at path
 */
// biome-ignore lint/suspicious/noExplicitAny: auto-parsing
function getValue(currentSettings, path) {
    for (let i = 0; i < path.length; i++) {
        const key = path[i];
        const value = currentSettings[key];
        if (i === path.length - 1) {
            return [value !== undefined, value];
        }
        if (!value) {
            // invalid path
            break;
        }
        currentSettings = value;
    }
    return [false, undefined];
}
/**
 * Add a value at given path, path is created as needed.
 * @param currentSettings
 * @param addition
 */
function addValue(currentSettings, addition) {
    setValue(currentSettings, addition.path, addition.value, true);
}
/**
 * Remove value at given path, if path is valid.
 * Value is actually set to undefined, which triggers removal when `settings.apply` is called.
 * @param currentSettings
 * @param removal
 * @returns
 */
function removeValue(currentSettings, removal) {
    const [validPath, previousValue] = getValue(currentSettings, removal.path);
    if (validPath && previousValue != null) {
        setValue(currentSettings, removal.path, undefined);
    }
    return [validPath, previousValue];
}
/**
 * Change value at given path, if path is valid, and value matched one of the defined values (if any).
 * @param currentSettings
 * @param change
 * @returns
 */
function changeValue(currentSettings, change) {
    const [validPath, previousValue] = getValue(currentSettings, change.path);
    let changed = false;
    if (validPath && previousValue !== change.newValue) {
        if (!change.previousValueAnyOf || change.previousValueAnyOf.includes(previousValue)) {
            setValue(currentSettings, change.path, change.newValue);
            changed = true;
        }
    }
    return [validPath, previousValue, changed];
}
/**
 * Transfer value at given path, to new path.
 * Given path must be valid.
 * New path must not be valid or new path value must be nullish, otherwise given path is removed only.
 * Value at given path is actually set to undefined, which triggers removal when `settings.apply` is called.
 * New path is created as needed.
 * @param currentSettings
 * @param transfer
 * @returns
 */
function transferValue(currentSettings, transfer) {
    const [validPath, previousValue] = getValue(currentSettings, transfer.path);
    const [destValidPath, destValue] = getValue(currentSettings, transfer.newPath);
    const transfered = validPath && previousValue != null && (!destValidPath || destValue == null || Array.isArray(destValue));
    // no point in set if already undefined
    if (validPath && previousValue != null) {
        setValue(currentSettings, transfer.path, undefined);
    }
    if (transfered) {
        if (Array.isArray(previousValue) && Array.isArray(destValue)) {
            setValue(currentSettings, transfer.newPath, [...previousValue, ...destValue], true);
        }
        else {
            setValue(currentSettings, transfer.newPath, previousValue, true);
        }
    }
    return [validPath, previousValue, transfered];
}
const noteIfWasTrue = (previousValue) => previousValue === true;
const noteIfWasDefined = (previousValue) => previousValue != null;
const noteIfWasNonEmptyArray = (previousValue) => Array.isArray(previousValue) && previousValue.length > 0;
function migrateToTwo(currentSettings, transfers, changes, additions, removals, customHandlers) {
    transfers.push({
        path: ["advanced", "homeassistant_discovery_topic"],
        note: "HA discovery_topic was moved from advanced.homeassistant_discovery_topic to homeassistant.discovery_topic.",
        noteIf: noteIfWasDefined,
        newPath: ["homeassistant", "discovery_topic"],
    }, {
        path: ["advanced", "homeassistant_status_topic"],
        note: "HA status_topic was moved from advanced.homeassistant_status_topic to homeassistant.status_topic.",
        noteIf: noteIfWasDefined,
        newPath: ["homeassistant", "status_topic"],
    }, {
        path: ["advanced", "baudrate"],
        note: "Baudrate was moved from advanced.baudrate to serial.baudrate.",
        noteIf: noteIfWasDefined,
        newPath: ["serial", "baudrate"],
    }, {
        path: ["advanced", "rtscts"],
        note: "RTSCTS was moved from advanced.rtscts to serial.rtscts.",
        noteIf: noteIfWasDefined,
        newPath: ["serial", "rtscts"],
    }, {
        path: ["experimental", "transmit_power"],
        note: "Transmit power was moved from experimental.transmit_power to advanced.transmit_power.",
        noteIf: noteIfWasDefined,
        newPath: ["advanced", "transmit_power"],
    }, {
        path: ["experimental", "output"],
        note: "Output was moved from experimental.output to advanced.output.",
        noteIf: noteIfWasDefined,
        newPath: ["advanced", "output"],
    }, {
        path: ["ban"],
        note: "ban was renamed to passlist.",
        noteIf: noteIfWasDefined,
        newPath: ["blocklist"],
    }, {
        path: ["whitelist"],
        note: "whitelist was renamed to passlist.",
        noteIf: noteIfWasDefined,
        newPath: ["passlist"],
    });
    changes.push({
        path: ["advanced", "log_level"],
        note: `Log level 'warn' has been renamed to 'warning'.`,
        noteIf: (previousValue) => previousValue === "warn",
        previousValueAnyOf: ["warn"],
        newValue: "warning",
    });
    additions.push({
        path: ["version"],
        note: "Migrated settings to version 2",
        value: 2,
    });
    const haLegacyTriggers = {
        path: ["homeassistant", "legacy_triggers"],
        note: "Action and click sensors have been removed (homeassistant.legacy_triggers setting). This means all sensor.*_action and sensor.*_click entities are removed. Use the MQTT device trigger instead.",
        noteIf: noteIfWasTrue,
    };
    const haLegacyEntityAttrs = {
        path: ["homeassistant", "legacy_entity_attributes"],
        note: "Entity attributes (homeassistant.legacy_entity_attributes setting) has been removed. This means that entities discovered by Zigbee2MQTT will no longer have entity attributes (Home Assistant entity attributes are accessed via e.g. states.binary_sensor.my_sensor.attributes).",
        noteIf: noteIfWasTrue,
    };
    const otaIkeaUseTestUrl = {
        path: ["ota", "ikea_ota_use_test_url"],
        note: "Due to the OTA rework, the ota.ikea_ota_use_test_url option has been removed.",
        noteIf: noteIfWasTrue,
    };
    removals.push(haLegacyTriggers, haLegacyEntityAttrs, {
        path: ["advanced", "homeassistant_legacy_triggers"],
        note: haLegacyTriggers.note,
        noteIf: haLegacyTriggers.noteIf,
    }, {
        path: ["advanced", "homeassistant_legacy_entity_attributes"],
        note: haLegacyEntityAttrs.note,
        noteIf: haLegacyEntityAttrs.noteIf,
    }, {
        path: ["permit_join"],
        note: "The permit_join setting has been removed, use the frontend or MQTT to permit joining.",
        noteIf: noteIfWasTrue,
    }, otaIkeaUseTestUrl, {
        path: ["advanced", "ikea_ota_use_test_url"],
        note: otaIkeaUseTestUrl.note,
        noteIf: otaIkeaUseTestUrl.noteIf,
    }, {
        path: ["advanced", "legacy_api"],
        note: "The MQTT legacy API has been removed (advanced.legacy_api setting). See link below for affected topics.",
        noteIf: noteIfWasTrue,
    }, {
        path: ["advanced", "legacy_availability_payload"],
        note: 'Due to the removal of advanced.legacy_availability_payload, zigbee2mqtt/bridge/state will now always be a JSON object ({"state":"online"} or {"state":"offline"})',
        noteIf: noteIfWasTrue,
    }, {
        path: ["advanced", "soft_reset_timeout"],
        note: "Removed deprecated: Soft reset feature (advanced.soft_reset_timeout setting)",
        noteIf: noteIfWasDefined,
    }, {
        path: ["advanced", "report"],
        note: "Removed deprecated: Report feature (advanced.report setting)",
        noteIf: noteIfWasTrue,
    }, {
        path: ["advanced", "availability_timeout"],
        note: "Removed deprecated: advanced.availability_timeout availability settings",
        noteIf: noteIfWasDefined,
    }, {
        path: ["advanced", "availability_blocklist"],
        note: "Removed deprecated: advanced.availability_blocklist availability settings",
        noteIf: noteIfWasNonEmptyArray,
    }, {
        path: ["advanced", "availability_passlist"],
        note: "Removed deprecated: advanced.availability_passlist availability settings",
        noteIf: noteIfWasNonEmptyArray,
    }, {
        path: ["advanced", "availability_blacklist"],
        note: "Removed deprecated: advanced.availability_blacklist availability settings",
        noteIf: noteIfWasNonEmptyArray,
    }, {
        path: ["advanced", "availability_whitelist"],
        note: "Removed deprecated: advanced.availability_whitelist availability settings",
        noteIf: noteIfWasNonEmptyArray,
    }, {
        path: ["device_options", "legacy"],
        note: "Removed everything that was enabled through device_options.legacy. See link below for affected devices.",
        noteIf: noteIfWasTrue,
    }, {
        path: ["experimental"],
        note: "The entire experimental section was removed.",
        noteIf: noteIfWasDefined,
    }, {
        path: ["external_converters"],
        note: "External converters are now automatically loaded from the 'data/external_converters' directory without requiring settings to be set. Make sure your external converters are still needed (might be supported out-of-the-box now), and if so, move them to that directory.",
        noteIf: noteIfWasNonEmptyArray,
    });
    // note only once
    const noteEntityOptionsRetrieveState = "Retrieve state option ((devices|groups).xyz.retrieve_state setting)";
    for (const deviceKey in currentSettings.devices) {
        removals.push({
            path: ["devices", deviceKey, "retrieve_state"],
            note: noteEntityOptionsRetrieveState,
            noteIf: noteIfWasTrue,
        });
    }
    for (const groupKey in currentSettings.groups) {
        removals.push({
            path: ["groups", groupKey, "retrieve_state"],
            note: noteEntityOptionsRetrieveState,
            noteIf: noteIfWasTrue,
        });
        removals.push({
            path: ["groups", groupKey, "devices"],
            note: "Removed configuring group members through configuration.yaml (groups.xyz.devices setting). This will not impact current group members; however, you will no longer be able to add or remove devices from a group through the configuration.yaml.",
            noteIf: noteIfWasDefined,
        });
    }
    customHandlers.push();
}
function migrateToThree(_currentSettings, transfers, changes, additions, removals, customHandlers) {
    transfers.push();
    changes.push({
        path: ["version"],
        note: "Migrated settings to version 3",
        newValue: 3,
    });
    additions.push();
    removals.push();
    const changeToObject = (currentSettings, path) => {
        const [validPath, previousValue] = getValue(currentSettings, path);
        if (validPath) {
            if (typeof previousValue === "boolean") {
                setValue(currentSettings, path, { enabled: previousValue });
            }
            else {
                setValue(currentSettings, path, { enabled: true, ...previousValue });
            }
        }
        return [validPath, previousValue, validPath];
    };
    customHandlers.push({
        note: `Property 'homeassistant' is now always an object.`,
        noteIf: () => true,
        execute: (currentSettings) => changeToObject(currentSettings, ["homeassistant"]),
    }, {
        note: `Property 'frontend' is now always an object.`,
        noteIf: () => true,
        execute: (currentSettings) => changeToObject(currentSettings, ["frontend"]),
    }, {
        note: `Property 'availability' is now always an object.`,
        noteIf: () => true,
        execute: (currentSettings) => changeToObject(currentSettings, ["availability"]),
    });
}
function migrateToFour(_currentSettings, transfers, changes, additions, removals, customHandlers) {
    transfers.push();
    changes.push({
        path: ["version"],
        note: "Migrated settings to version 4",
        newValue: 4,
    });
    additions.push();
    removals.push();
    const saveBase64DeviceIconsAsImage = (currentSettings) => {
        const [validPath, previousValue] = getValue(currentSettings, ["devices"]);
        let changed = false;
        if (validPath) {
            for (const deviceKey in currentSettings.devices) {
                const base64Match = utils_1.default.matchBase64File(currentSettings.devices[deviceKey].icon);
                if (base64Match) {
                    changed = true;
                    currentSettings.devices[deviceKey].icon = utils_1.default.saveBase64DeviceIcon(base64Match);
                }
            }
        }
        return [validPath, previousValue, changed];
    };
    customHandlers.push({
        note: "Device icons are now saved as images.",
        noteIf: () => true,
        execute: (currentSettings) => saveBase64DeviceIconsAsImage(currentSettings),
    });
}
function migrateToFive(_currentSettings, transfers, changes, additions, removals, customHandlers) {
    transfers.push();
    changes.push({
        path: ["version"],
        note: "Migrated settings to version 5",
        newValue: 5,
    });
    additions.push();
    removals.push();
    const resetOtaStates = (_currentSettings) => {
        const filePath = data_1.default.joinPath("state.json");
        if ((0, node_fs_1.existsSync)(filePath)) {
            try {
                const stateFile = (0, node_fs_1.readFileSync)(filePath, "utf8");
                const stateObj = JSON.parse(stateFile);
                for (const key in stateObj) {
                    const entry = stateObj[key];
                    if (entry.update) {
                        delete entry.update;
                    }
                }
                (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(stateObj), "utf8");
                // hack, this does not actually affect settings, we just want to ensure the migration note triggers
                return [true, undefined, true];
            }
            catch (error) {
                console.error(`Failed to write state to '${filePath}' (${error})`);
            }
        }
        return [false, undefined, false];
    };
    customHandlers.push({
        note: "Reset all cached OTA states due to internal changes.",
        noteIf: () => true,
        execute: (currentSettings) => resetOtaStates(currentSettings),
    });
}
/**
 * Order of execution:
 * - Transfer
 * - Change
 * - Add
 * - Remove
 *
 * Should allow the most flexibility whenever combination of migrations is necessary (e.g. Transfer + Change)
 */
function migrateIfNecessary() {
    const currentSettings = settings.getPersistedSettings();
    if (!SUPPORTED_VERSIONS.includes(currentSettings.version)) {
        throw new Error(`Your configuration.yaml has an unsupported version ${currentSettings.version}, expected one of ${SUPPORTED_VERSIONS.map((v) => String(v)).join(",")}.`);
    }
    /* v8 ignore next */
    const finalVersion = process.env.VITEST_WORKER_ID ? settings.testing.CURRENT_VERSION : settings.CURRENT_VERSION;
    if (currentSettings.version === finalVersion) {
        // when same version as current, nothing to do
        return;
    }
    while (currentSettings.version !== finalVersion) {
        let migrationNotesFileName;
        // don't duplicate outputs
        const migrationNotes = new Set();
        const transfers = [];
        const changes = [];
        const additions = [];
        const removals = [];
        const customHandlers = [];
        backupSettings(currentSettings.version || 1);
        // each version should only bump to the next version so as to gradually migrate if necessary
        if (currentSettings.version == null) {
            // migrating from 1 (`version` did not exist) to 2
            migrationNotesFileName = "migration-1-to-2.log";
            migrateToTwo(currentSettings, transfers, changes, additions, removals, customHandlers);
        }
        else if (currentSettings.version === 2) {
            migrationNotesFileName = "migration-2-to-3.log";
            migrateToThree(currentSettings, transfers, changes, additions, removals, customHandlers);
        }
        else if (currentSettings.version === 3) {
            migrationNotesFileName = "migration-3-to-4.log";
            migrateToFour(currentSettings, transfers, changes, additions, removals, customHandlers);
        }
        else if (currentSettings.version === 4) {
            migrationNotesFileName = "migration-4-to-5.log";
            migrateToFive(currentSettings, transfers, changes, additions, removals, customHandlers);
        }
        for (const transfer of transfers) {
            const [validPath, previousValue, transfered] = transferValue(currentSettings, transfer);
            if (validPath && (!transfer.noteIf || transfer.noteIf(previousValue))) {
                migrationNotes.add(`[${transfered ? "TRANSFER" : "REMOVAL"}] ${transfer.note}`);
            }
        }
        for (const change of changes) {
            const [validPath, previousValue, changed] = changeValue(currentSettings, change);
            if (validPath && changed && (!change.noteIf || change.noteIf(previousValue))) {
                migrationNotes.add(`[CHANGE] ${change.note}`);
            }
        }
        for (const addition of additions) {
            addValue(currentSettings, addition);
            migrationNotes.add(`[ADDITION] ${addition.note}`);
        }
        for (const removal of removals) {
            const [validPath, previousValue] = removeValue(currentSettings, removal);
            if (validPath && (!removal.noteIf || removal.noteIf(previousValue))) {
                migrationNotes.add(`[REMOVAL] ${removal.note}`);
            }
        }
        for (const customHandler of customHandlers) {
            const [validPath, previousValue, changed] = customHandler.execute(currentSettings);
            if (validPath && changed && (!customHandler.noteIf || customHandler.noteIf(previousValue))) {
                migrationNotes.add(`[SPECIAL] ${customHandler.note}`);
            }
        }
        if (migrationNotesFileName && migrationNotes.size > 0) {
            migrationNotes.add("For more details, see https://github.com/Koenkk/zigbee2mqtt/discussions/24198");
            const migrationNotesFilePath = data_1.default.joinPath(migrationNotesFileName);
            (0, node_fs_1.writeFileSync)(migrationNotesFilePath, Array.from(migrationNotes).join("\r\n\r\n"), "utf8");
            console.log(`Migration notes written in ${migrationNotesFilePath}`);
        }
    }
    // don't throw, onboarding will validate at end of process
    settings.apply(currentSettings, false);
    settings.reRead();
}
//# sourceMappingURL=data:application/json;base64, 