import { DoorLockCCValues, DoorLockMode, LockCCValues, NotificationCCValues, UserCodeCC, UserIDStatus, getEffectiveCCVersion, } from "@zwave-js/cc";
import { getNotificationEnumBehavior, getNotificationStateValueWithEnum, getNotificationValueMetadata, } from "@zwave-js/cc/NotificationCC";
import { CommandClasses, UNKNOWN_STATE, getNotification, getNotificationValue, valueIdToString, } from "@zwave-js/core";
import { isUint8Array, setTimer, stringify, } from "@zwave-js/shared";
export function getDefaultNotificationHandlerStore() {
    return {
        idleTimeouts: new Map(),
    };
}
/** Handles the receipt of a Notification Report */
export function handleNotificationReport(ctx, node, command, store) {
    if (command.notificationType == undefined) {
        if (command.alarmType == undefined) {
            ctx.logNode(node.id, {
                message: `received unsupported notification ${stringify(command)}`,
                direction: "inbound",
            });
        }
        return;
    }
    // Apply notification remapping from compat config
    const remappings = node.deviceConfig?.compat?.remapNotifications;
    if (remappings?.length) {
        const match = remappings.find((m) => m.from.notificationType === command.notificationType
            && m.from.notificationEvent === command.notificationEvent);
        if (match) {
            ctx.logNode(node.id, {
                message: `Notification remapped via compat flag`,
                direction: "inbound",
                level: "verbose",
            });
            if (match.action.type === "clear" || match.action.type === "idle") {
                for (const target of match.action.targets) {
                    const targetNotification = getNotification(target.notificationType);
                    if (!targetNotification)
                        continue;
                    const targetValueConfig = getNotificationValue(targetNotification, target.notificationEvent);
                    if (targetValueConfig?.type !== "state")
                        continue;
                    const valueId = NotificationCCValues
                        .notificationVariable(targetNotification.name, targetValueConfig.variableName).endpoint(command.endpointIndex);
                    if (match.action.type === "clear") {
                        // Only clear if the variable currently reflects this specific event,
                        // to avoid spurious updates when multiple targets share the same variable.
                        if (node.valueDB.getValue(valueId)
                            === target.notificationEvent) {
                            node.valueDB.setValue(valueId, UNKNOWN_STATE);
                        }
                        // Special case for doorStateSimple. If the parent value
                        // gets cleared, this also needs to get cleared.
                        if (target.notificationType === 0x06
                            && (target.notificationEvent === 0x16
                                || target.notificationEvent === 0x17)) {
                            const simpleDoorStateId = NotificationCCValues
                                .deprecated_doorStateSimple
                                .endpoint(command.endpointIndex);
                            if (node.valueDB.getValue(simpleDoorStateId)
                                === target.notificationEvent) {
                                node.valueDB.setValue(simpleDoorStateId, UNKNOWN_STATE);
                            }
                        }
                    }
                    else {
                        node.valueDB.setValue(valueId, 0 /* idle */);
                    }
                }
                return;
            }
            else {
                // Normal remap: overwrite notification type and event, then fall through
                // to normal handling.
                const to = match.action.to;
                command.notificationType = to.notificationType;
                command.notificationEvent = to.notificationEvent;
            }
        }
    }
    const ccVersion = getEffectiveCCVersion(ctx, command);
    // Look up the received notification in the config
    const notification = getNotification(command.notificationType);
    if (notification) {
        // This is a notification (status or event) with a known type
        const notificationName = notification.name;
        ctx.logNode(node.id, {
            message: `[handleNotificationReport] notificationName: ${notificationName}`,
            level: "silly",
        });
        /** Returns a single notification state to idle */
        const setStateIdle = (prevValue) => {
            manuallyIdleNotificationValueInternal(ctx, node, store, notification, prevValue, command.endpointIndex);
        };
        const setUnknownStateIdle = (prevValue) => {
            // Find the value for the unknown notification variable bucket
            const unknownNotificationVariableValueId = NotificationCCValues
                .unknownNotificationVariable(command.notificationType, notificationName).endpoint(command.endpointIndex);
            const currentValue = node.valueDB.getValue(unknownNotificationVariableValueId);
            // ... and if it exists
            if (currentValue == undefined)
                return;
            // ... reset it to idle
            if (prevValue == undefined || currentValue === prevValue) {
                node.valueDB.setValue(unknownNotificationVariableValueId, 0);
            }
        };
        const value = command.notificationEvent;
        if (value === 0) {
            // Generic idle notification, this contains a value to be reset
            if (isUint8Array(command.eventParameters)
                && command.eventParameters.length) {
                // The target value is the first byte of the event parameters
                setStateIdle(command.eventParameters[0]);
                setUnknownStateIdle(command.eventParameters[0]);
            }
            else {
                // Reset all values to idle
                const nonIdleValues = node.valueDB
                    .getValues(CommandClasses.Notification)
                    .filter((v) => (v.endpoint || 0) === command.endpointIndex
                    && v.property === notificationName
                    && typeof v.value === "number"
                    && v.value !== 0);
                for (const v of nonIdleValues) {
                    setStateIdle(v.value);
                }
                setUnknownStateIdle();
            }
            return;
        }
        // Find out which property we need to update
        const valueConfig = getNotificationValue(notification, value);
        if (valueConfig) {
            ctx.logNode(node.id, {
                message: `[handleNotificationReport] valueConfig:
  label: ${valueConfig.label}
  ${valueConfig.type === "event"
                    ? "type: event"
                    : `type: state
  variableName: ${valueConfig.variableName}`}`,
                level: "silly",
            });
        }
        else {
            ctx.logNode(node.id, {
                message: `[handleNotificationReport] valueConfig: undefined`,
                level: "silly",
            });
        }
        // Perform some heuristics on the known notification
        handleKnownNotification(ctx, node, command);
        let allowIdleReset;
        if (!valueConfig) {
            // We don't know what this notification refers to, so we don't force a reset
            allowIdleReset = false;
        }
        else if (valueConfig.type === "state") {
            allowIdleReset = valueConfig.idle;
        }
        else {
            // This is an event
            const endpoint = node.getEndpoint(command.endpointIndex)
                ?? node;
            // Build the notification event args
            const eventArgs = {
                type: command.notificationType,
                event: value,
                label: notification.name,
                eventLabel: valueConfig.label,
                parameters: command.eventParameters,
            };
            // If the lookupUserIdInNotificationEvents preference is enabled and the event contains a userId,
            // look up the user code and status and add them to the parameters
            const prefs = ctx.getUserPreferences();
            if (prefs.lookupUserIdInNotificationEvents
                && command.eventParameters != null
                && typeof command.eventParameters === "object"
                && !isUint8Array(command.eventParameters)
                && "userId" in command.eventParameters
                && typeof command.eventParameters.userId === "number") {
                const userId = command.eventParameters.userId;
                const nodeEndpoint = {
                    nodeId: node.id,
                    index: command.endpointIndex,
                    virtual: false,
                };
                // Create a new parameters object with the additional fields
                const enhancedParameters = {
                    ...command.eventParameters,
                };
                const userIdStatus = UserCodeCC.getUserIdStatusCached(ctx, nodeEndpoint, userId);
                if (userIdStatus != null) {
                    enhancedParameters.userIdStatus = userIdStatus;
                }
                const userCode = UserCodeCC.getUserCodeCached(ctx, nodeEndpoint, userId);
                if (userCode != null) {
                    enhancedParameters.userCode = userCode;
                }
                eventArgs.parameters = enhancedParameters;
            }
            node.emit("notification", endpoint, CommandClasses.Notification, eventArgs);
            // We may need to reset some linked states to idle
            if (valueConfig.idleVariables?.length) {
                for (const variable of valueConfig.idleVariables) {
                    setStateIdle(variable);
                }
            }
            return;
        }
        // Now that we've gathered all we need to know, update the value in our DB
        let valueId;
        if (valueConfig) {
            valueId = NotificationCCValues.notificationVariable(notificationName, valueConfig.variableName).endpoint(command.endpointIndex);
            extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig);
        }
        else {
            // Collect unknown values in an "unknown" bucket
            const unknownValue = NotificationCCValues
                .unknownNotificationVariable(command.notificationType, notificationName);
            valueId = unknownValue.endpoint(command.endpointIndex);
            if (ccVersion >= 2) {
                if (!node.valueDB.hasMetadata(valueId)) {
                    node.valueDB.setMetadata(valueId, unknownValue.meta);
                }
            }
        }
        if (typeof command.eventParameters === "number") {
            // This notification contains an enum value. Depending on how the enum behaves,
            // we may need to set "fake" values for these to distinguish them
            // from states without enum values
            const enumBehavior = valueConfig
                ? getNotificationEnumBehavior(notification, valueConfig)
                : "extend";
            const valueWithEnum = enumBehavior === "replace"
                ? command.eventParameters
                : getNotificationStateValueWithEnum(value, command.eventParameters);
            node.valueDB.setValue(valueId, valueWithEnum);
        }
        else {
            node.valueDB.setValue(valueId, value);
        }
        // Nodes before V8 (and some misbehaving V8 ones) don't necessarily reset the notification to idle.
        // The specifications advise to auto-reset the variables, but it has been found that this interferes
        // with some motion sensors that don't refresh their active notification. Therefore, we set a fallback
        // timer if the `forceNotificationIdleReset` compat flag is set.
        if (allowIdleReset
            && !!node.deviceConfig?.compat?.forceNotificationIdleReset) {
            ctx.logNode(node.id, {
                message: `[handleNotificationReport] scheduling idle reset`,
                level: "silly",
            });
            scheduleNotificationIdleReset(store, valueId, () => setStateIdle(value));
        }
    }
    else {
        // This is an unknown notification
        const unknownValue = NotificationCCValues.unknownNotificationType(command.notificationType);
        const valueId = unknownValue.endpoint(command.endpointIndex);
        // Make sure the metdata exists
        if (ccVersion >= 2) {
            if (!node.valueDB.hasMetadata(valueId)) {
                node.valueDB.setMetadata(valueId, unknownValue.meta);
            }
        }
        // And set its value
        node.valueDB.setValue(valueId, command.notificationEvent);
        // We don't know what this notification refers to, so we don't force a reset
    }
}
function handleKnownNotification(ctx, node, command) {
    const lockEvents = new Set([0x01, 0x03, 0x05, 0x09]);
    const unlockEvents = new Set([0x02, 0x04, 0x06]);
    const doorStatusEvents = [
        // Actual status
        0x16,
        0x17,
        // Synthetic status with enum (deprecated)
        0x1600,
        0x1601,
    ];
    if (
    // Access Control, manual/keypad/rf/auto (un)lock operation
    command.notificationType === 0x06
        && (lockEvents.has(command.notificationEvent)
            || unlockEvents.has(command.notificationEvent))
        && (node.supportsCC(CommandClasses["Door Lock"])
            || node.supportsCC(CommandClasses.Lock))) {
        // The Door Lock Command Class is constrained to the S2 Access Control key,
        // while the Notification Command Class, in the same device, could use a
        // different key. This way the device can notify devices which don't belong
        // to the S2 Access Control key group of changes in its state.
        const isLocked = lockEvents.has(command.notificationEvent);
        // Update the current lock status
        if (node.supportsCC(CommandClasses["Door Lock"])) {
            node.valueDB.setValue(DoorLockCCValues.currentMode.endpoint(command.endpointIndex), isLocked ? DoorLockMode.Secured : DoorLockMode.Unsecured);
        }
        if (node.supportsCC(CommandClasses.Lock)) {
            node.valueDB.setValue(LockCCValues.locked.endpoint(command.endpointIndex), isLocked);
        }
    }
    else if (command.notificationType === 0x06
        && doorStatusEvents.includes(command.notificationEvent)) {
        // https://github.com/zwave-js/zwave-js/pull/5394 added support for
        // notification enums. Unfortunately, there's no way to discover which nodes
        // actually support them, which makes working with the Door state variable
        // very cumbersome. Also, this is currently the only notification where the enum values
        // extend the state value.
        // To work around this, we synthesize a stable door/window state that only
        // exposes tilt after we've actually seen a tilted notification.
        const isTilted = command.notificationEvent === 0x16
            && command.eventParameters === 0x01;
        const openingStateValue = NotificationCCValues.openingState;
        const openingStateValueId = openingStateValue.endpoint(command.endpointIndex);
        const openingStateTiltMetadata = {
            ...openingStateValue.meta,
            states: {
                ...openingStateValue.meta.states,
                [0x02]: "Tilted",
            },
        };
        const openingStateMetadata = node.valueDB.getMetadata(openingStateValueId);
        if (isTilted && !openingStateMetadata?.states?.[2]) {
            node.valueDB.setMetadata(openingStateValueId, openingStateTiltMetadata);
        }
        node.valueDB.setValue(openingStateValueId, command.notificationEvent === 0x17 ? 0x00 : isTilted ? 0x02 : 0x01);
        // Keep the legacy compatibility value limited to the historic open/closed states.
        node.valueDB.setValue(NotificationCCValues.deprecated_doorStateSimple.endpoint(command.endpointIndex), command.notificationEvent === 0x17 ? 0x17 : 0x16);
        // In addition to that, we also hard-code a notification value for only the tilt status.
        // This will only be created after receiving a notification for the tilted state.
        // Only after it exists, it will be updated. Otherwise, we'd get phantom
        // values, since some devices send the enum value, even when they don't support tilt.
        const tiltValue = NotificationCCValues.deprecated_doorTiltState;
        const tiltValueId = tiltValue.endpoint(command.endpointIndex);
        let tiltValueWasCreated = node.valueDB.hasMetadata(tiltValueId);
        if (isTilted && !tiltValueWasCreated) {
            node.valueDB.setMetadata(tiltValueId, tiltValue.meta);
            tiltValueWasCreated = true;
        }
        if (tiltValueWasCreated) {
            node.valueDB.setValue(tiltValueId, isTilted ? 0x01 : 0x00);
        }
    }
    else if (
    // Access Control, all user codes deleted
    command.notificationType === 0x06
        && command.notificationEvent === 0x0c
        && node.supportsCC(CommandClasses["User Code"])) {
        // Clear all user codes from the cache
        const endpoint = {
            nodeId: node.id,
            index: command.endpointIndex,
            virtual: false,
        };
        const numUsers = UserCodeCC.getSupportedUsersCached(ctx, endpoint) ?? 0;
        // Clear each user code by setting status to Available and code to empty
        for (let userId = 1; userId <= numUsers; userId++) {
            UserCodeCC.setUserIdStatusCached(ctx, endpoint, userId, UserIDStatus.Available);
            UserCodeCC.setUserCodeCached(ctx, endpoint, userId, "");
        }
    }
}
/** Manually resets a single notification value to idle */
export function manuallyIdleNotificationValueInternal(ctx, node, store, notification, prevValue, endpointIndex) {
    const valueConfig = getNotificationValue(notification, prevValue);
    // Only known variables may be reset to idle
    if (!valueConfig || valueConfig.type !== "state")
        return;
    // Some properties may not be reset to idle
    if (!valueConfig.idle)
        return;
    const notificationName = notification.name;
    const variableName = valueConfig.variableName;
    const valueId = NotificationCCValues.notificationVariable(notificationName, variableName).endpoint(endpointIndex);
    // Make sure the value is actually set to the previous value
    if (node.valueDB.getValue(valueId) !== prevValue)
        return;
    // Since the node has reset the notification itself, we don't need the idle reset
    clearNotificationIdleReset(store, valueId);
    extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig);
    node.valueDB.setValue(valueId, 0 /* idle */);
}
/** Schedules a notification value to be reset */
function scheduleNotificationIdleReset(store, valueId, handler) {
    clearNotificationIdleReset(store, valueId);
    const key = valueIdToString(valueId);
    store.idleTimeouts.set(key, 
    // Unref'ing long running timeouts allows to quit the application before the timeout elapses
    setTimer(handler, 5 * 60 * 1000 /* 5 minutes */).unref());
}
/** Removes a scheduled notification reset */
function clearNotificationIdleReset(store, valueId) {
    const key = valueIdToString(valueId);
    if (store.idleTimeouts.has(key)) {
        store.idleTimeouts.get(key)?.clear();
        store.idleTimeouts.delete(key);
    }
}
// Fallback for V2 notifications that don't allow us to predefine the metadata during the interview.
// Instead of defining useless values for each possible notification event, we build the metadata on demand
function extendNotificationValueMetadata(ctx, node, valueId, notification, valueConfig) {
    const ccVersion = ctx.getSupportedCCVersion(CommandClasses.Notification, node.id, node.index);
    if (ccVersion === 2 || !node.valueDB.hasMetadata(valueId)) {
        const metadata = getNotificationValueMetadata(node.valueDB.getMetadata(valueId), notification, valueConfig);
        node.valueDB.setMetadata(valueId, metadata);
    }
}
//# sourceMappingURL=NotificationCC.js.map