import { refreshConfigParamMetadataFromConfigFile } from "@zwave-js/cc/ConfigurationCC";
import { DeviceConfig, parseDeviceConfigHash } from "@zwave-js/config";
import { InterviewStage, NOT_KNOWN } from "@zwave-js/core";
import { Bytes, formatId } from "@zwave-js/shared";
import { cacheKeys } from "../../driver/NetworkCache.js";
import { FirmwareUpdateMixin } from "./70_FirmwareUpdate.js";
export class DeviceConfigMixin extends FirmwareUpdateMixin {
    _deviceConfig;
    /**
     * Contains additional information about this node, loaded from a config file
     */
    get deviceConfig() {
        return this._deviceConfig;
    }
    set deviceConfig(value) {
        this._deviceConfig = value;
    }
    /**
     * Returns the manufacturer/brand name defined in the device configuration,
     * or looks it up from the manufacturer database if no config is available
     */
    get manufacturer() {
        if (this._deviceConfig)
            return this._deviceConfig.manufacturer;
        if (this.manufacturerId != undefined) {
            return this.driver.lookupManufacturer(this.manufacturerId);
        }
    }
    /**
     * Returns the device label defined in the device configuration.
     */
    get label() {
        return this._deviceConfig?.label;
    }
    get deviceDatabaseUrl() {
        if (this.manufacturerId != undefined
            && this.productType != undefined
            && this.productId != undefined) {
            const manufacturerId = formatId(this.manufacturerId);
            const productType = formatId(this.productType);
            const productId = formatId(this.productId);
            const firmwareVersion = this.firmwareVersion || "0.0";
            return `https://devices.zwave-js.io/?jumpTo=${manufacturerId}:${productType}:${productId}:${firmwareVersion}`;
        }
    }
    /**
     * Returns whether the device config for this node has changed since the last interview
     * in a way that affects interview behavior (compat flags, association settings, proprietary config).
     * Changes to configuration parameter metadata are applied dynamically and do not require re-interview.
     */
    hasDeviceConfigChanged() {
        // We can't know if the node is not fully interviewed
        if (this.interviewStage !== InterviewStage.Complete)
            return NOT_KNOWN;
        // The controller cannot be re-interviewed
        if (this.isControllerNode)
            return false;
        // If the hash was never stored, we can only (very likely) know if the config has not changed
        if (this.cachedDeviceConfigHash == undefined) {
            return this.deviceConfig == undefined ? false : NOT_KNOWN;
        }
        // If it was, a change in hash means the config has changed.
        // We handle the different hash versions when loading the config already.
        if (this._currentDeviceConfigHash) {
            return !DeviceConfig.areHashesEqual(this._currentDeviceConfigHash, this.cachedDeviceConfigHash);
        }
        return true;
    }
    /**
     * @internal
     * The hash of the device config that was applied during the last interview.
     */
    get cachedDeviceConfigHash() {
        return this.driver.cacheGet(cacheKeys.node(this.id).deviceConfigHash);
    }
    set cachedDeviceConfigHash(value) {
        this.driver.cacheSet(cacheKeys.node(this.id).deviceConfigHash, value);
    }
    _currentDeviceConfigHash;
    /**
     * @internal
     * The hash of the currently used device config
     */
    get currentDeviceConfigHash() {
        return this._currentDeviceConfigHash;
    }
    set currentDeviceConfigHash(value) {
        this._currentDeviceConfigHash = value;
    }
    /**
     * Loads the device configuration for this node from a config file
     */
    async loadDeviceConfig() {
        // But the configuration definitions might change
        if (this.manufacturerId == undefined
            || this.productType == undefined
            || this.productId == undefined) {
            return;
        }
        // Try to load the config file
        this.deviceConfig = await this.driver.configManager.lookupDevice(this.manufacturerId, this.productType, this.productId, this.firmwareVersion, this.sdkVersion);
        if (!this.deviceConfig) {
            this.driver.controllerLog.logNode(this.id, "No device config found", "warn");
            return;
        }
        // We need to remember the hash of the device config here, because we're in an async context
        // and later comparisons are sync.
        // There are two legacy versions of the device config hash:
        // - 16 byte MD5 hash
        // - 32 byte SHA-256 hash
        // Both only support checking for exact equality of the config hashable.
        // To be able to compare those stored hashes with the current config,
        // we need to figure out the stored version now and hash the config using
        // the same version.
        // New "hashes" are variable length, contain a condensed version of the device config and are prefixed with a version number.
        const versionPrefix = Bytes.from("$v", "utf8");
        const hasVersionPrefix = !!this.cachedDeviceConfigHash
            && Bytes
                .view(this.cachedDeviceConfigHash.subarray(0, 2))
                .equals(versionPrefix);
        let cachedHashVersion;
        if (this.cachedDeviceConfigHash?.length === 16
            && !hasVersionPrefix) {
            // MD5 = version 0
            cachedHashVersion = 0;
            this._currentDeviceConfigHash = await this.deviceConfig
                .getHash(0);
        }
        else if (this.cachedDeviceConfigHash?.length === 32
            && !hasVersionPrefix) {
            // SHA-256 = version 1
            cachedHashVersion = 1;
            this._currentDeviceConfigHash = await this.deviceConfig
                .getHash(1);
        }
        else {
            // Variable length prefixed hash - determine the hash version from the cache
            if (this.cachedDeviceConfigHash) {
                const parsed = parseDeviceConfigHash(this.cachedDeviceConfigHash);
                if (parsed) {
                    cachedHashVersion = parsed.version;
                }
            }
            // Use that version for comparison purposes if possible
            this._currentDeviceConfigHash = await this.deviceConfig.getHash();
            // and default to requiring an upgrade if the version could not be parsed
            cachedHashVersion ??= 0;
        }
        // Update the cached device config hash to the most recent version upon restoring,
        // if the node was previously interviewed and the device config has not changed
        // since then.
        if (this.interviewStage === InterviewStage.Complete) {
            if (cachedHashVersion < DeviceConfig.maxHashVersion
                && this.hasDeviceConfigChanged() === false) {
                this.cachedDeviceConfigHash = await this.deviceConfig.getHash();
                // Also update the current hash to the new version, in case
                // it was previously generated with an older version
                this._currentDeviceConfigHash = this.cachedDeviceConfigHash;
            }
            // Apply all param metadata from the device config dynamically.
            // This handles updates to existing params, addition of new params,
            // and removal/hiding of old params.
            for (const ep of this.getAllEndpoints()) {
                refreshConfigParamMetadataFromConfigFile(this.driver, this.id, ep.index);
            }
        }
        this.driver.controllerLog.logNode(this.id, `${this.deviceConfig.isEmbedded
            ? "Embedded"
            : "User-provided"} device config loaded`);
    }
}
//# sourceMappingURL=80_DeviceConfig.js.map