
/**
 * @packageDocumentation
 * @module mqtt5
 */

import * as mqtt from "mqtt";
import * as mqtt_shared from "../common/mqtt_shared";
import * as mqtt5 from "./mqtt5";
import { CrtError } from "./error";

export const MAXIMUM_VARIABLE_LENGTH_INTEGER : number= 268435455;
export const MAXIMUM_PACKET_SIZE : number = 5 + MAXIMUM_VARIABLE_LENGTH_INTEGER;
export const DEFAULT_RECEIVE_MAXIMUM : number = 65535;
export const DEFAULT_CONNECT_TIMEOUT_MS : number = 30000;
export const DEFAULT_MIN_RECONNECT_DELAY_MS : number = 1000;
export const DEFAULT_MAX_RECONNECT_DELAY_MS : number = 120000;
export const DEFAULT_MIN_CONNECTED_TIME_TO_RESET_RECONNECT_DELAY_MS : number = 30000;

/** @internal */
function set_defined_property(object: any, propertyName: string, value: any) : boolean {
    if (value === undefined || value == null) {
        return false;
    }

    object[propertyName] = value;

    return true;
}

/** @internal */
export function transform_mqtt_js_connack_to_crt_connack(mqtt_js_connack: mqtt.IConnackPacket) : mqtt5.ConnackPacket {
    if (mqtt_js_connack == null || mqtt_js_connack == undefined) {
        throw new CrtError("transform_mqtt_js_connack_to_crt_connack: mqtt_js_connack not defined");
    }

    let connack : mqtt5.ConnackPacket =  {
        type: mqtt5.PacketType.Connack,
        sessionPresent: mqtt_js_connack.sessionPresent,
        reasonCode : mqtt_js_connack.reasonCode ?? mqtt5.ConnectReasonCode.Success
    };

    set_defined_property(connack, "sessionExpiryInterval", mqtt_js_connack.properties?.sessionExpiryInterval);
    set_defined_property(connack, "receiveMaximum", mqtt_js_connack.properties?.receiveMaximum);
    set_defined_property(connack, "maximumQos", mqtt_js_connack.properties?.maximumQoS);
    set_defined_property(connack, "retainAvailable", mqtt_js_connack.properties?.retainAvailable);
    set_defined_property(connack, "maximumPacketSize", mqtt_js_connack.properties?.maximumPacketSize);
    set_defined_property(connack, "assignedClientIdentifier", mqtt_js_connack.properties?.assignedClientIdentifier);
    set_defined_property(connack, "topicAliasMaximum", mqtt_js_connack.properties?.topicAliasMaximum);
    set_defined_property(connack, "reasonString", mqtt_js_connack.properties?.reasonString);
    set_defined_property(connack, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(mqtt_js_connack.properties?.userProperties));
    set_defined_property(connack, "wildcardSubscriptionsAvailable", mqtt_js_connack.properties?.wildcardSubscriptionAvailable);
    set_defined_property(connack, "subscriptionIdentifiersAvailable", mqtt_js_connack.properties?.subscriptionIdentifiersAvailable);
    set_defined_property(connack, "sharedSubscriptionsAvailable", mqtt_js_connack.properties?.sharedSubscriptionAvailable);
    set_defined_property(connack, "serverKeepAlive", mqtt_js_connack.properties?.serverKeepAlive);
    set_defined_property(connack, "responseInformation", mqtt_js_connack.properties?.responseInformation);
    set_defined_property(connack, "serverReference", mqtt_js_connack.properties?.serverReference);

    return connack;
}

/** @internal */
export function create_negotiated_settings(config : mqtt5.Mqtt5ClientConfig, connack: mqtt5.ConnackPacket) : mqtt5.NegotiatedSettings {
    if (config == null || config == undefined) {
        throw new CrtError("create_negotiated_settings: config not defined");
    }
    if (connack == null || connack == undefined) {
        throw new CrtError("create_negotiated_settings: connack not defined");
    }

    return {
        maximumQos: Math.min(connack.maximumQos ?? mqtt5.QoS.ExactlyOnce, mqtt5.QoS.AtLeastOnce),
        sessionExpiryInterval: connack.sessionExpiryInterval ?? config.connectProperties?.sessionExpiryIntervalSeconds ?? 0,
        receiveMaximumFromServer: connack.receiveMaximum ?? DEFAULT_RECEIVE_MAXIMUM,
        maximumPacketSizeToServer: connack.maximumPacketSize ?? MAXIMUM_PACKET_SIZE,
        serverKeepAlive: connack.serverKeepAlive ?? config.connectProperties?.keepAliveIntervalSeconds ?? mqtt_shared.DEFAULT_KEEP_ALIVE,
        retainAvailable: connack.retainAvailable ?? true,
        wildcardSubscriptionsAvailable: connack.wildcardSubscriptionsAvailable ?? true,
        subscriptionIdentifiersAvailable: connack.subscriptionIdentifiersAvailable ?? true,
        sharedSubscriptionsAvailable: connack.sharedSubscriptionsAvailable ?? true,
        rejoinedSession: connack.sessionPresent,
        clientId: connack.assignedClientIdentifier ?? config.connectProperties?.clientId ?? ""
    };
}

/** @internal */
function create_mqtt_js_will_from_crt_config(connectProperties? : mqtt5.ConnectPacket) : any {
    if (!connectProperties || !connectProperties.will) {
        return undefined;
    }

    let crtWill : mqtt5.PublishPacket = connectProperties.will;

    let hasWillProperties : boolean = false;
    let willProperties : any = {};
    hasWillProperties = set_defined_property(willProperties, "willDelayInterval", connectProperties.willDelayIntervalSeconds) || hasWillProperties;
    if (crtWill.payloadFormat !== undefined) {
        hasWillProperties = set_defined_property(willProperties, "payloadFormatIndicator", crtWill.payloadFormat == mqtt5.PayloadFormatIndicator.Utf8) || hasWillProperties;
    }
    hasWillProperties = set_defined_property(willProperties, "messageExpiryInterval", crtWill.messageExpiryIntervalSeconds) || hasWillProperties;
    hasWillProperties = set_defined_property(willProperties, "contentType", crtWill.contentType) || hasWillProperties;
    hasWillProperties = set_defined_property(willProperties, "responseTopic", crtWill.responseTopic) || hasWillProperties;
    hasWillProperties = set_defined_property(willProperties, "correlationData", crtWill.correlationData) || hasWillProperties;
    hasWillProperties = set_defined_property(willProperties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(crtWill.userProperties)) || hasWillProperties;

    let will : any = {
        topic: crtWill.topicName,
        payload: crtWill.payload ?? "",
        qos: crtWill.qos,
        retain: crtWill.retain ?? false
    };

    if (hasWillProperties) {
        will["properties"] = willProperties;
    }

    return will;
}

/** @internal */
export function getOrderedReconnectDelayBounds(configMin?: number, configMax?: number) : [number, number] {
    const minDelay : number = Math.max(1, configMin ?? DEFAULT_MIN_RECONNECT_DELAY_MS);
    const maxDelay : number = Math.max(1, configMax ?? DEFAULT_MAX_RECONNECT_DELAY_MS);
    if (minDelay > maxDelay) {
        return [maxDelay, minDelay];
    } else {
        return [minDelay, maxDelay];
    }
}

/** @internal */
function should_mqtt_js_use_clean_start(session_behavior? : mqtt5.ClientSessionBehavior) : boolean {
    return session_behavior !== mqtt5.ClientSessionBehavior.RejoinPostSuccess && session_behavior !== mqtt5.ClientSessionBehavior.RejoinAlways;
}

/** @internal */
export function compute_mqtt_js_reconnect_delay_from_crt_max_delay(maxReconnectDelayMs : number) : number {
    /*
     * This is an attempt to guarantee that the mqtt-js will never try to reconnect on its own and instead always
     * be controlled by our reconnection scheduler logic.
     */
    return maxReconnectDelayMs * 2 + 60000;
}

function validate_required_uint16(propertyName : string, value: number) {
    if (value < 0 || value > 65535) {
        throw new CrtError(`Invalid value for property ${propertyName}: ` + value);
    }
}

function validate_optional_uint16(propertyName : string, value?: number) {
    if (value !== undefined) {
        validate_required_uint16(propertyName, value);
    }
}

function validate_required_uint32(propertyName : string, value: number) {
    if (value < 0 || value >= 4294967296) {
        throw new CrtError(`Invalid value for property ${propertyName}: ` + value);
    }
}

function validate_optional_uint32(propertyName : string, value?: number) {
    if (value !== undefined) {
        validate_required_uint32(propertyName, value);
    }
}

function validate_required_nonnegative_uint32(propertyName : string, value: number) {
    if (value <= 0 || value >= 4294967296) {
        throw new CrtError(`Invalid value for property ${propertyName}: ` + value);
    }
}

function validate_optional_nonnegative_uint32(propertyName : string, value?: number) {
    if (value !== undefined) {
        validate_required_nonnegative_uint32(propertyName, value);
    }
}

function validate_mqtt5_client_config(crtConfig : mqtt5.Mqtt5ClientConfig) {
    if (crtConfig == null || crtConfig == undefined) {
        throw new CrtError("validate_mqtt5_client_config: crtConfig not defined");
    }
    validate_required_uint16("keepAliveIntervalSeconds", crtConfig.connectProperties?.keepAliveIntervalSeconds ?? 0);
    validate_optional_uint32("sessionExpiryIntervalSeconds", crtConfig.connectProperties?.sessionExpiryIntervalSeconds);
    validate_optional_uint16("receiveMaximum", crtConfig.connectProperties?.receiveMaximum);
    validate_optional_nonnegative_uint32("maximumPacketSizeBytes", crtConfig.connectProperties?.maximumPacketSizeBytes);
    validate_optional_uint32("willDelayIntervalSeconds", crtConfig.connectProperties?.willDelayIntervalSeconds);
}

/** @internal */
export function create_mqtt_js_client_config_from_crt_client_config(crtConfig : mqtt5.Mqtt5ClientConfig) : mqtt.IClientOptions {

    validate_mqtt5_client_config(crtConfig);

    let [_, maxDelay] = getOrderedReconnectDelayBounds(crtConfig.minReconnectDelayMs, crtConfig.maxReconnectDelayMs);

    maxDelay = compute_mqtt_js_reconnect_delay_from_crt_max_delay(maxDelay);

    let mqttJsClientConfig : mqtt.IClientOptions = {
        protocolVersion: 5,
        keepalive: crtConfig.connectProperties?.keepAliveIntervalSeconds ?? mqtt_shared.DEFAULT_KEEP_ALIVE,
        connectTimeout: crtConfig.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
        clean: should_mqtt_js_use_clean_start(crtConfig.sessionBehavior),
        reconnectPeriod: maxDelay,
        queueQoSZero : false,
        // @ts-ignore
        autoUseTopicAlias : false,
        // @ts-ignore
        autoAssignTopicAlias : false,
        transformWsUrl: undefined, /* TODO */
        resubscribe : false
    };

    /*
     * If you leave clientId undefined, mqtt-js will make up some weird thing for you, but the intent is that it
     * should pass the empty client id so that the server assigns you one.
     */
    set_defined_property(mqttJsClientConfig, "clientId", crtConfig.connectProperties?.clientId ?? "");
    set_defined_property(mqttJsClientConfig, "username", crtConfig.connectProperties?.username);
    set_defined_property(mqttJsClientConfig, "password", crtConfig.connectProperties?.password);
    set_defined_property(mqttJsClientConfig, "will", create_mqtt_js_will_from_crt_config(crtConfig.connectProperties));

    let hasProperties : boolean = false;
    let properties: any = {};
    hasProperties = set_defined_property(properties, "sessionExpiryInterval", crtConfig.connectProperties?.sessionExpiryIntervalSeconds) || hasProperties;
    hasProperties = set_defined_property(properties, "receiveMaximum", crtConfig.connectProperties?.receiveMaximum) || hasProperties;
    hasProperties = set_defined_property(properties, "maximumPacketSize", crtConfig.connectProperties?.maximumPacketSizeBytes) || hasProperties;
    hasProperties = set_defined_property(properties, "requestResponseInformation", crtConfig.connectProperties?.requestResponseInformation) || hasProperties;
    hasProperties = set_defined_property(properties, "requestProblemInformation", crtConfig.connectProperties?.requestProblemInformation) || hasProperties;
    hasProperties = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(crtConfig.connectProperties?.userProperties)) || hasProperties;

    if (hasProperties) {
        mqttJsClientConfig["properties"] = properties;
    }

    return mqttJsClientConfig;
}

/** @internal */
export function transform_crt_user_properties_to_mqtt_js_user_properties(userProperties?: mqtt5.UserProperty[]) : mqtt.UserProperties | undefined {
    if (!userProperties) {
        return undefined;
    }

    /*
     * More restricted version of mqtt.UserProperties so that we can have type-checking but don't need to handle
     * the non-array case.
     */
    let mqttJsProperties : {[key : string] : string[] } = {};

    for (const property of userProperties) {
        const key : string = property.name;
        if (!(key in mqttJsProperties)) {
            mqttJsProperties[key] = [];
        }
        mqttJsProperties[key].push(property.value);
    }

    return mqttJsProperties;
}

/** @internal */
export function transform_mqtt_js_user_properties_to_crt_user_properties(userProperties?: mqtt.UserProperties) : mqtt5.UserProperty[] | undefined {
    if (!userProperties) {
        return undefined;
    }

    let crtProperties : mqtt5.UserProperty[] | undefined = undefined;

    for (const [propName, propValue] of Object.entries(userProperties)) {

        let values : string[] = (typeof propValue === 'string') ? [propValue] : propValue;
        for (const valueIter of values) {
            let propertyEntry = {name : propName, value : valueIter};
            if (!crtProperties) {
                crtProperties = [propertyEntry];
            } else {
                crtProperties.push(propertyEntry);
            }
        }
    }

    return crtProperties;
}

function validate_crt_disconnect(disconnect: mqtt5.DisconnectPacket) {
    if (disconnect == null || disconnect == undefined) {
        throw new CrtError("validate_crt_disconnect: disconnect not defined");
    }
    validate_optional_uint32("sessionExpiryIntervalSeconds", disconnect.sessionExpiryIntervalSeconds);
}

/** @internal */
export function transform_crt_disconnect_to_mqtt_js_disconnect(disconnect: mqtt5.DisconnectPacket) : mqtt.IDisconnectPacket {

    validate_crt_disconnect(disconnect);

    let properties = {};
    let propertiesValid : boolean = false;

    propertiesValid = set_defined_property(properties, "sessionExpiryInterval", disconnect.sessionExpiryIntervalSeconds) || propertiesValid;
    propertiesValid = set_defined_property(properties, "reasonString", disconnect.reasonString) || propertiesValid;
    propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(disconnect.userProperties)) || propertiesValid;
    propertiesValid = set_defined_property(properties, "serverReference", disconnect.serverReference) || propertiesValid;

    let mqttJsDisconnect : mqtt.IDisconnectPacket = {
        cmd: 'disconnect',
        reasonCode : disconnect.reasonCode
    };

    if (propertiesValid) {
        mqttJsDisconnect["properties"] = properties;
    }

    return mqttJsDisconnect;
}

/** @internal **/
export function transform_mqtt_js_disconnect_to_crt_disconnect(disconnect: mqtt.IDisconnectPacket) : mqtt5.DisconnectPacket {

    if (disconnect == null || disconnect == undefined) {
        throw new CrtError("transform_mqtt_js_disconnect_to_crt_disconnect: disconnect not defined");
    }

    let crtDisconnect : mqtt5.DisconnectPacket = {
        type: mqtt5.PacketType.Disconnect,
        reasonCode : disconnect.reasonCode ?? mqtt5.DisconnectReasonCode.NormalDisconnection
    };

    set_defined_property(crtDisconnect, "sessionExpiryIntervalSeconds", disconnect.properties?.sessionExpiryInterval);
    set_defined_property(crtDisconnect, "reasonString", disconnect.properties?.reasonString);
    set_defined_property(crtDisconnect, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(disconnect.properties?.userProperties));
    set_defined_property(crtDisconnect, "serverReference", disconnect.properties?.serverReference);

    return crtDisconnect;
}

function validate_crt_subscribe(subscribe: mqtt5.SubscribePacket) {
    if (subscribe == null || subscribe == undefined) {
        throw new CrtError("validate_crt_subscribe: subscribe not defined");
    }
    validate_optional_uint32("subscriptionIdentifier", subscribe.subscriptionIdentifier);
}

/** @internal **/
export function transform_crt_subscribe_to_mqtt_js_subscription_map(subscribe: mqtt5.SubscribePacket) : mqtt.ISubscriptionMap {

    validate_crt_subscribe(subscribe);

    let subscriptionMap : mqtt.ISubscriptionMap = {};

    for (const subscription of subscribe.subscriptions) {
        let mqttJsSub = {
            qos: subscription.qos,
            nl : subscription.noLocal ?? false,
            rap: subscription.retainAsPublished ?? false,
            rh: subscription.retainHandlingType ?? mqtt5.RetainHandlingType.SendOnSubscribe
        };

        subscriptionMap[subscription.topicFilter] = mqttJsSub;
    }

    return subscriptionMap;
}

/** @internal **/
export function transform_crt_subscribe_to_mqtt_js_subscribe_options(subscribe: mqtt5.SubscribePacket) : mqtt.IClientSubscribeOptions {

    let properties = {};
    let propertiesValid : boolean = false;

    if (subscribe == null || subscribe == undefined) {
        throw new CrtError("transform_crt_subscribe_to_mqtt_js_subscribe_options: subscribe not defined");
    }

    propertiesValid = set_defined_property(properties, "subscriptionIdentifier", subscribe.subscriptionIdentifier) || propertiesValid;
    propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(subscribe.userProperties)) || propertiesValid;

    let options : mqtt.IClientSubscribeOptions = {
        qos: 0
    }

    if (propertiesValid) {
        options["properties"] = properties;
    }

    return options;
}

/** @internal **/
export function transform_mqtt_js_subscription_grants_to_crt_suback(subscriptionsGranted: mqtt.ISubscriptionGrant[]) : mqtt5.SubackPacket {

    if (subscriptionsGranted == null || subscriptionsGranted == undefined) {
        throw new CrtError("transform_mqtt_js_subscription_grants_to_crt_suback: subscriptionsGranted not defined");
    }

    let crtSuback : mqtt5.SubackPacket = {
        type: mqtt5.PacketType.Suback,
        reasonCodes : subscriptionsGranted.map((subscription: mqtt.ISubscriptionGrant, index: number, array : mqtt.ISubscriptionGrant[]) : mqtt5.SubackReasonCode => { return subscription.qos; })
    }

    /*
     * TODO: mqtt-js does not expose the suback packet to subscribe's completion callback, so we cannot extract
     * reasonString and userProperties atm.
     *
     * Revisit if this changes.
     */


    return crtSuback;
}

function validate_crt_publish(publish: mqtt5.PublishPacket) {
    if (publish == null || publish == undefined) {
        throw new CrtError("validate_crt_publish: publish not defined");
    }
    validate_optional_uint32("messageExpiryIntervalSeconds", publish.messageExpiryIntervalSeconds);
}

/** @internal */
export function transform_crt_publish_to_mqtt_js_publish_options(publish: mqtt5.PublishPacket) : mqtt.IClientPublishOptions {

    validate_crt_publish(publish);

    let properties = {};
    let propertiesValid : boolean = false;

    if (publish.payloadFormat !== undefined) {
        propertiesValid = set_defined_property(properties, "payloadFormatIndicator", publish.payloadFormat == mqtt5.PayloadFormatIndicator.Utf8) || propertiesValid;
    }
    propertiesValid = set_defined_property(properties, "messageExpiryInterval", publish.messageExpiryIntervalSeconds) || propertiesValid;
    propertiesValid = set_defined_property(properties, "responseTopic", publish.responseTopic) || propertiesValid;
    propertiesValid = set_defined_property(properties, "correlationData", publish.correlationData) || propertiesValid;
    propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(publish.userProperties)) || propertiesValid;
    propertiesValid = set_defined_property(properties, "contentType", publish.contentType) || propertiesValid;

    let mqttJsPublish : mqtt.IClientPublishOptions = {
        qos: publish.qos,
        retain: publish.retain ?? false,
    };

    if (propertiesValid) {
        mqttJsPublish["properties"] = properties;
    }

    return mqttJsPublish;
}

/** @internal **/
export function transform_mqtt_js_publish_to_crt_publish(publish: mqtt.IPublishPacket) : mqtt5.PublishPacket {

    if (publish == null || publish == undefined) {
        throw new CrtError("transform_mqtt_js_publish_to_crt_publish: publish not defined");
    }

    let crtPublish : mqtt5.PublishPacket = {
        type: mqtt5.PacketType.Publish,
        qos: publish.qos,
        retain: publish.retain,
        topicName: publish.topic,
        payload: publish.payload
    };

    if (publish.properties) {
        if (publish.properties.payloadFormatIndicator !== undefined) {
            set_defined_property(crtPublish, "payloadFormat", publish.properties.payloadFormatIndicator ? mqtt5.PayloadFormatIndicator.Utf8 : mqtt5.PayloadFormatIndicator.Bytes);
        }
        set_defined_property(crtPublish, "messageExpiryIntervalSeconds", publish.properties?.messageExpiryInterval);
        set_defined_property(crtPublish, "responseTopic", publish.properties?.responseTopic);
        set_defined_property(crtPublish, "correlationData", publish.properties?.correlationData);
        set_defined_property(crtPublish, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(publish.properties?.userProperties));
        set_defined_property(crtPublish, "contentType", publish.properties?.contentType);

        let subIds : number | number[] | undefined = publish.properties?.subscriptionIdentifier;
        let subIdsType : string = typeof subIds;
        if (subIds) {
            if (subIdsType == 'number') {
                crtPublish["subscriptionIdentifiers"] = [subIds];
            } else if (Array.isArray(subIds)) {
                crtPublish["subscriptionIdentifiers"] = subIds;
            }
        }
    }

    return crtPublish;
}

/** @internal **/
export function transform_mqtt_js_puback_to_crt_puback(puback: mqtt.IPubackPacket) : mqtt5.PubackPacket {

    if (puback == null || puback == undefined) {
        throw new CrtError("transform_mqtt_js_puback_to_crt_puback: puback not defined");
    }

    let crtPuback : mqtt5.PubackPacket = {
        type: mqtt5.PacketType.Puback,
        reasonCode: puback.reasonCode ?? mqtt5.PubackReasonCode.Success,
    };

    if (puback.properties) {
        set_defined_property(crtPuback, "reasonString", puback.properties?.reasonString);
        set_defined_property(crtPuback, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(puback.properties?.userProperties));
    }

    return crtPuback;
}

/** @internal **/
export function transform_crt_unsubscribe_to_mqtt_js_unsubscribe_options(unsubscribe: mqtt5.UnsubscribePacket) : Object {

    if (unsubscribe == null || unsubscribe == undefined) {
        throw new CrtError("transform_crt_unsubscribe_to_mqtt_js_unsubscribe_options: unsubscribe not defined");
    }

    let properties = {};
    let propertiesValid : boolean = false;

    propertiesValid = set_defined_property(properties, "userProperties", transform_crt_user_properties_to_mqtt_js_user_properties(unsubscribe.userProperties));

    let options : any = {};

    if (propertiesValid) {
        options["properties"] = properties;
    }

    return options;
}

/** @internal **/
export function transform_mqtt_js_unsuback_to_crt_unsuback(packet: mqtt.IUnsubackPacket) : mqtt5.UnsubackPacket {

    if (packet == null || packet == undefined) {
        throw new CrtError("transform_mqtt_js_unsuback_to_crt_unsuback: packet not defined");
    }

    let reasonCodes : number | number[] | undefined = packet.reasonCode;

    let codes : number[];
    if (Array.isArray(reasonCodes)) {
        codes = reasonCodes;
    } else if (typeof reasonCodes == 'number') {
        codes = [reasonCodes];
    } else {
        codes = [];
    }

    let crtUnsuback : mqtt5.UnsubackPacket = {
        type: mqtt5.PacketType.Unsuback,
        reasonCodes : codes
    }

    if (packet.properties) {
        set_defined_property(crtUnsuback, "reasonString", packet.properties?.reasonString);
        set_defined_property(crtUnsuback, "userProperties", transform_mqtt_js_user_properties_to_crt_user_properties(packet.properties?.userProperties));
    }

    return crtUnsuback;
}
