//! Client module for {{stinger.name}} IPC
//!
//! This module is only available when the "client" feature is enabled.

/*
DO NOT MODIFY THIS FILE{# <-- Ignore this because you are editing the template file. #}.  It is automatically generated and changes will be over-written
on the next generation.

This is the Client for the {{stinger.name}} interface.

LICENSE: This generated code is not subject to any license restrictions from the generator itself.
TODO: Get license text from stinger file
*/
#[cfg(feature = "client")]
use stinger_mqtt_trait::{Mqtt5PubSub};
use stinger_mqtt_trait::message::{MqttMessage, QoS};
use crate::message;
{%if stinger.methods|length > 0 -%}
use std::collections::HashMap;
use uuid::Uuid;
{%endif-%}
use serde_json;
use crate::discovery::DiscoveredService;
#[allow(unused_imports)]
use crate::payloads::{MethodReturnCode, *};
#[allow(unused_imports)]
use iso8601_duration::Duration as IsoDuration;
use std::sync::{Arc, Mutex};
use tokio::sync::{broadcast{%if stinger.methods|length > 0 -%}, oneshot{%endif-%}{%if stinger.properties|length > 0 -%}, watch{%endif-%}};
use tokio::task::JoinError;
#[allow(unused_imports)]
use tracing::{debug, error, info, warn};
{% if stinger.properties|length > 0 %}
#[allow(unused_imports)]
use std::sync::atomic::{AtomicU32, Ordering};
use stinger_rwlock_watch::{RwLockWatch};
#[allow(unused_imports)]
use stinger_rwlock_watch::ReadOnlyLockWatch;
#[allow(unused_imports)]
use stinger_rwlock_watch::{WriteRequestLockWatch, CommitResult};
{%endif%}
/// This struct is used to store all the MQTTv5 subscription ids
/// for the subscriptions the client will make.
#[derive(Clone, Debug)]
struct {{stinger.name | UpperCamelCase }}SubscriptionIds {
    {%if stinger.methods | length > 0 %}
    any_method_response: u32,
    {%endif%}
    {%for sig_name, sig in stinger.signals.items()-%}
    {{sig_name | snake_case}}_signal: Option<u32>,
    {%-endfor%}
    {%for prop_name, prop in stinger.properties.items()-%}
    {{prop_name | snake_case}}_property_value: u32,
    {%endfor%}
    {%if stinger.properties | length > 0 %}
    any_property_update_response: u32,
    {%endif%}
}

/// This struct holds the tx side of a broadcast channels used when receiving signals.
/// The rx side of the broadcast channels can be created from the tx side later.
/// When {{stinger.rust.client_struct_name}} gets a message and determines that it
/// is a signal, it will send the signal payload via the tx channel that is in this struct.
#[derive(Clone)]
struct {{stinger.name | UpperCamelCase }}SignalChannels {
    {%for sig_name, sig in stinger.signals.items()-%}
    {{sig_name|snake_case}}_sender: broadcast::Sender<{%if sig.arg_list | length == 1 %}{{sig.arg_list[0].rust_type}}{%elif sig.arg_list | length == 0 %}(){% else %}{{sig_name | UpperCamelCase}}SignalPayload{% endif %}>,
    {%endfor%}
}

{%if stinger.properties | length > 0%}
#[derive(Clone)]
struct {{stinger.name | UpperCamelCase}}Properties {
    {%for prop_name, prop in stinger.properties.items()-%}
    {%-if prop.arg_list | length > 1%}
    pub {{prop_name | snake_case}}: Arc<RwLockWatch<{{prop.rust_type}}>>,
    {%-else%}
    pub {{prop_name | snake_case}}: Arc<RwLockWatch<{{prop.arg_list[0].rust_type}}>>,
    {%endif-%}
    pub {{prop_name | snake_case}}_version: Arc<AtomicU32>,
    {%endfor%}
}
{%endif%}


/// This is the struct for our API client.
#[derive(Clone)]
pub struct {{stinger.rust.client_struct_name}}<C: Mqtt5PubSub> {
    mqtt_client: C,
    {%if stinger.methods | length > 0 %}/// Temporarily holds oneshot channels for responses to method calls.
    pending_responses: Arc<Mutex<HashMap<Uuid, oneshot::Sender<MethodReturnCode>>>>,
    {%endif%}

    /// Temporarily holds the receiver for the broadcast channel.  The Receiver will be moved
    /// to a process loop when it is needed.  MQTT messages will be received with this.
    msg_streamer_rx: Arc<Mutex<Option<broadcast::Receiver<MqttMessage>>>>,

    /// The Sender side of MQTT messages that are received from the broker.  This tx
    /// side is cloned for each subscription made.
    #[allow(dead_code)]
    msg_streamer_tx: broadcast::Sender<MqttMessage>,
    {%if stinger.properties|length > 0%}
    /// Struct contains all the properties.
    properties: {{stinger.name | UpperCamelCase}}Properties,
    {%endif%}
    /// Contains all the MQTTv5 subscription ids.
    subscription_ids: {{stinger.name | UpperCamelCase }}SubscriptionIds,

    {%-if stinger.signals | length > 0 %}
    /// Holds the channels used for sending signals to the application.
    signal_channels: {{stinger.name | UpperCamelCase }}SignalChannels,
    {%endif%}
    /// Copy of MQTT Client ID
    pub client_id: String,
    {%if stinger.methods | length > 0 %}
    /// Instance ID of the server
    service_instance_id: String,
    {%endif%}
}

impl<C: Mqtt5PubSub + Clone + Send + 'static> {{stinger.rust.client_struct_name}}<C> {

    /// Creates a new {{stinger.rust.client_struct_name}} that uses an Mqtt5PubSub.
    pub async fn new(mut connection: C, discovery_info: DiscoveredService) -> Self {

        // Create a channel for messages to get from the Connection object to this {{stinger.rust.client_struct_name}} object.
        // The Connection object uses a clone of the tx side of the channel.
        let (message_received_tx, message_received_rx) = broadcast::channel(64);

        let client_id = connection.get_client_id();

        {%if stinger.methods|length > 0%}
        let topic_any_method_response = format!("{{stinger.all_methods_response_topic('{}')}}", client_id);
        let subscription_id_any_method_response = connection.subscribe(topic_any_method_response, QoS::ExactlyOnce, message_received_tx.clone()).await;
        let subscription_id_any_method_response = subscription_id_any_method_response.unwrap_or(u32::MAX);
        {%endif%}

        // Subscribe to all the topics needed for signals.
        {%for signal_name, signal in stinger.signals.items()-%}
        let topic_{{signal_name | snake_case}}_signal = format!("{{signal.topic}}", discovery_info.interface_info.instance);
        let subscription_id_{{signal_name | snake_case}}_signal = connection.subscribe(topic_{{signal_name | snake_case}}_signal, QoS::ExactlyOnce, message_received_tx.clone()).await;
        let subscription_id_{{signal_name | snake_case}}_signal = subscription_id_{{signal_name | snake_case}}_signal.unwrap_or(u32::MAX);
        debug!("Subscription (id={}) to signal topic for '{{signal_name}}'", subscription_id_{{signal_name | snake_case}}_signal);
        {%endfor%}

        // Subscribe to all the topics needed for properties.
        {% for prop_name, prop in stinger.properties.items() %}
        let topic_{{prop_name | snake_case}}_property_value = format!("{{prop.value_topic}}", discovery_info.interface_info.instance);
        let subscription_id_{{prop_name | snake_case}}_property_value = connection.subscribe(topic_{{prop_name | snake_case}}_property_value, QoS::AtLeastOnce, message_received_tx.clone()).await;
        let subscription_id_{{prop_name | snake_case}}_property_value = subscription_id_{{prop_name | snake_case}}_property_value.unwrap_or(u32::MAX);
        debug!("Subscription (id={}) to property value topic for '{{prop_name}}'", subscription_id_{{prop_name | snake_case}}_property_value);
        {% endfor %}

        {%if stinger.properties|length > 0 %}
        let topic_any_property_update_response = format!("{{stinger.all_properties_response_topic('{}')}}", client_id);
        let subscription_id_any_property_update_response = connection.subscribe(topic_any_property_update_response, QoS::AtLeastOnce, message_received_tx.clone()).await;
        let subscription_id_any_property_update_response = subscription_id_any_property_update_response.unwrap_or(u32::MAX);
        debug!("Subscription (id={}) to any property update response topic", subscription_id_any_property_update_response);
        {%endif%}

        {%if stinger.properties|length > 0%}
        let property_values = {{stinger.name | UpperCamelCase}}Properties {
        {%-for prop_name, prop in stinger.properties.items()%}
            {%if prop.arg_list | length > 1 -%}
            {{prop_name | snake_case}}: Arc::new(RwLockWatch::new(discovery_info.properties.{{prop_name | snake_case}})),
            {%-else%}
            {{prop_name | snake_case}}: Arc::new(RwLockWatch::new(discovery_info.properties.{{prop_name | snake_case}})),
            {%-endif-%}
            {{prop_name | snake_case}}_version: Arc::new(AtomicU32::new(discovery_info.properties.{{prop_name | snake_case}}_version)),
        {%-endfor%}
        };
        {%endif%}
        // Create structure for subscription ids.
        let sub_ids = {{stinger.name | UpperCamelCase }}SubscriptionIds {
            {%if stinger.methods|length > 0 -%}
            any_method_response: subscription_id_any_method_response,
            {%endif%}
            {%-for sig_name, sig in stinger.signals.items()-%}
            {{sig_name | snake_case}}_signal: Some(subscription_id_{{sig_name | snake_case}}_signal),
            {%endfor%}
            {%-for prop_name, prop in stinger.properties.items()-%}
            {{prop_name | snake_case}}_property_value: subscription_id_{{prop_name | snake_case}}_property_value,
            {%endfor%}
            {%if stinger.properties | length > 0 %}
            any_property_update_response: subscription_id_any_property_update_response,
            {%endif%}
        };

        {%if stinger.signals | length > 0 %}// Create structure for the tx side of broadcast channels for signals.
        let signal_channels = {{stinger.name | UpperCamelCase }}SignalChannels {
            {%for sig_name, sig in stinger.signals.items()-%}
            {{sig_name|snake_case}}_sender: broadcast::channel(64).0,
            {%endfor%}
        };{%endif%}

        // Create {{stinger.rust.client_struct_name}} structure.
        {{stinger.rust.client_struct_name}} {
            mqtt_client: connection,
            {%if stinger.methods | length > 0 %}pending_responses: Arc::new(Mutex::new(HashMap::new())),{%endif%}
            msg_streamer_rx: Arc::new(Mutex::new(Some(message_received_rx))),
            msg_streamer_tx: message_received_tx,
            {%if stinger.properties|length > 0 %}
            properties: property_values,
            {%endif%}
            subscription_ids: sub_ids,
            {%if stinger.signals | length > 0 %}signal_channels,{%endif%}
            client_id,
            {%if stinger.methods | length > 0 %}
            service_instance_id: discovery_info.interface_info.instance,
            {%endif%}
        }
    }

    {%for sig_name, sig in stinger.signals.items()-%}
    /// Get the RX receiver side of the broadcast channel for the {{sig_name}} signal.
    /// The signal payload, `{{sig_name | UpperCamelCase}}SignalPayload`, will be put onto the channel whenever it is received.
    pub fn get_{{sig_name|snake_case}}_receiver(&self) -> broadcast::Receiver<{%if sig.arg_list|length == 0%}(){%elif sig.arg_list|length == 1%}{{sig.arg_list[0].rust_type}}{%else%}{{sig_name | UpperCamelCase}}SignalPayload{%endif%}> {
        self.signal_channels.{{sig_name | snake_case}}_sender.subscribe()
    }
    {%endfor%}

    {%for method_name, method in stinger.methods.items()-%}

    async fn start_{{method_name|snake_case}}(
            &mut self,
            {%for arg in method.arg_list%}{{arg.name|snake_case}}: {{arg.rust_type}}, {%endfor%}
    ) -> oneshot::Receiver<MethodReturnCode> {
        // Setup tracking for the future response.
        let correlation_id = Uuid::new_v4();
        let (sender, receiver) = oneshot::channel();
        {
            let mut hashmap = self.pending_responses.lock().expect("Mutex was poisoned");
            hashmap.insert(correlation_id, sender);
        }

        let data = {{method_name | UpperCamelCase}}RequestObject {
            {%-for arg in method.arg_list%}
            {{arg.name|snake_case}},
            {%-endfor%}
        };

        let response_topic: String = format!("{{stinger.all_methods_response_topic('{}')}}", self.client_id);
        let msg = message::request(&format!("{{method.topic}}", self.service_instance_id), &data, correlation_id, response_topic).unwrap();
        info!("Sending request to topic '{}': {:?}", format!("{{method.topic}}", self.service_instance_id), data);
        let _ = self.mqtt_client.publish(msg).await;
        receiver
    }

    /// The `{{method_name}}` method.
    /// Method arguments are packed into a {{method_name | UpperCamelCase}}RequestObject structure
    /// and published to the `{{method.topic}}` MQTT topic.
    ///
    /// This method awaits on the response to the call before returning.
    pub async fn {{method_name|snake_case}}(&mut self{%if method.arg_list | length > 0 %}, {%endif%}{%for arg in method.arg_list%}{{arg.name|snake_case}}: {{arg.rust_type}}{%if not loop.last%}, {%endif%}{%endfor%})->Result<{{method.return_value_rust_type}}, MethodReturnCode> {

        let receiver = self.start_{{method_name|snake_case}}({%for arg in method.arg_list%}{{arg.name|snake_case}}{%if not loop.last%}, {%endif%}{%endfor%}).await;

        let {%if method.return_value_type is false %}{%endif%}return_code: MethodReturnCode = receiver.await.unwrap();
        info!("Received response for method '{{method_name}}': {:?}", return_code);
        match return_code {
            MethodReturnCode::Success({%if method.return_arg_list |length == 0 %}_{%else%}payload_str{%endif%}) => {
                {%if method.return_arg_list |length > 0%}
                let return_obj: {{method_name|UpperCamelCase}}ReturnValues = serde_json::from_str(payload_str.as_ref().map_or("{}", |v| v))
                    .map_err(|e| MethodReturnCode::ClientDeserializationError(e.to_string()))?;
                {%endif%}
                {%if method.return_arg_list |length == 0 %}
                Ok(())
                {%elif method.return_arg_list |length == 1 %}
                Ok(return_obj.{{method.return_arg_list[0].name}})
                {%else%}
                Ok(return_obj)
                {%endif%}
            },
            _ => {
                Err(return_code)
            }
        }
    }

    {%endfor%}
    {%for prop_name, prop in stinger.properties.items()%}
    /// Watch for changes to the `{{prop_name}}` property.
    /// This returns a watch::Receiver that can be awaited on for changes to the property value.
    pub fn watch_{{prop_name | snake_case}}(&self) -> watch::Receiver<{%if prop.arg_list | length > 1 %}{{prop.rust_type}}{% else %}{{prop.arg_list[0].rust_type}}{% endif %}> {
        self.properties.{{prop_name | snake_case}}.subscribe()
    }
    {%if not prop.read_only %}
    /// Sets the `{{prop_name}}` property and returns a oneshot that receives the acknowledgment back from the server.
    pub async fn set_{{prop_name | snake_case}}(
            &mut self, 
            value: {%if prop.arg_list | length > 1 %}{{prop.rust_type}}{% else %}{{prop.arg_list[0].rust_type}}{% endif %}
    ) -> MethodReturnCode {
        let write_request_lock = self.get_{{prop_name | snake_case}}_handle();
        let mut writer = write_request_lock.write().await;
        *writer = value;
        match writer.commit(std::time::Duration::from_secs(5)).await {
            CommitResult::Applied(_) => {
                MethodReturnCode::Success(None)
            },
            CommitResult::TimedOut => MethodReturnCode::Timeout("Timeout waiting for property update acknowledgment".to_string()),
        }   
    }

    pub fn get_{{prop_name | snake_case}}_handle(&self) -> Arc<WriteRequestLockWatch<{%if prop.arg_list | length > 1 %}{{prop.rust_type}}{%else%}{{prop.arg_list[0].rust_type}}{% endif %}>> {
        self.properties.{{prop_name | snake_case}}.write_request().into()
    }
    {%else%}
    pub fn get_{{prop_name | snake_case}}_handle(&self) -> Arc<ReadOnlyLockWatch<{%if prop.arg_list | length > 1 %}{{prop.rust_type}}{%else%}{{prop.arg_list[0].rust_type}}{%endif%}>> {
        self.properties.{{prop_name | snake_case}}.read_only().into()
    }
    {%endif%}
    {%endfor%}

    fn get_return_code_from_message(msg: &MqttMessage) -> MethodReturnCode {
        let payload = String::from_utf8_lossy(&msg.payload).to_string();
        let mut return_code: MethodReturnCode = MethodReturnCode::Success(Some(payload));
        if let Some(retval) = msg.user_properties.get("ReturnCode") {
            let opt_dbg_info = msg.user_properties.get("DebugInfo").cloned();
            if let Ok(return_code_u32) = retval.parse::<u32>() {
                if return_code_u32 != 0 {
                    return_code = MethodReturnCode::from_code(return_code_u32, opt_dbg_info);
                } else {
                    info!("Received Debug Info: {:?}", opt_dbg_info);
                }
            }
        }
        return_code
    }

    /// Starts the tasks that process messages received.
    pub async fn run_loop(&mut self) -> Result<(), JoinError> {

        {%if stinger.methods | length > 0%}// Clone the Arc pointer to the map.  This will be moved into the loop_task.
        let resp_map: Arc<Mutex<HashMap<Uuid, oneshot::Sender<MethodReturnCode>>>> = self.pending_responses.clone();
        {%endif%}
        // Take ownership of the RX channel that receives MQTT messages.  This will be moved into the loop_task.
        let mut message_receiver = {
            let mut guard = self.msg_streamer_rx.lock().expect("Mutex was poisoned");
            guard.take().expect("msg_streamer_rx should be Some")
        };
        {%if stinger.signals | length > 0 %}
        let sig_chans = self.signal_channels.clone();
        {%endif%}
        let sub_ids = self.subscription_ids.clone();
        {%-if stinger.properties|length > 0%}
        let props = self.properties.clone();
        {%-for prop_name, prop in stinger.properties.items()%}
        { // Set up property change request handling task
            let instance_id_for_{{prop_name|snake_case}}_prop = self.service_instance_id.clone();
            let client_id_for_{{prop_name|snake_case}}_prop = self.client_id.clone();
            let mut publisher_for_{{prop_name|snake_case}}_prop = self.mqtt_client.clone();
            let {{prop_name|snake_case}}_prop_version = props.{{prop_name | snake_case}}_version.clone();
            let resp_map_for_{{prop_name|snake_case}}_prop = self.pending_responses.clone();
            if let Some(mut rx_for_{{prop_name|snake_case}}_prop) = props.{{prop_name | snake_case}}.take_request_receiver() {
                tokio::spawn(async move {
                    while let Some((value, opt_responder)) = rx_for_{{prop_name|snake_case}}_prop.recv().await {
                        {%if prop.arg_list | length == 1 %}
                        let payload_obj = {{prop_name | UpperCamelCase}}Property { 
                            {{prop.arg_list[0].name}}: value
                        };
                        {%else%}
                        let payload_obj = value;
                        {%endif%}
                        let topic: String = format!("{{prop.update_topic}}", instance_id_for_{{prop_name|snake_case}}_prop);
                        if let Some(responder) = opt_responder {
                            let resp_topic = format!("{{stinger.all_properties_response_topic('{}')}}", client_id_for_{{prop_name|snake_case}}_prop);
                            let correlation_id = Uuid::new_v4();
                            let (sender, receiver) = oneshot::channel();
                            {
                                let mut hashmap = resp_map_for_{{prop_name|snake_case}}_prop.lock().expect("Mutex was poisoned");
                                hashmap.insert(correlation_id.clone(), sender);
                            }
                            match message::property_update_request(&topic, &payload_obj, {{prop_name|snake_case}}_prop_version.load(std::sync::atomic::Ordering::Relaxed), correlation_id, resp_topic) {
                                Ok(msg) => {
                                    let _publish_result = publisher_for_{{prop_name|snake_case}}_prop.publish(msg).await;
                                    let result = receiver.await;
                                    match result {
                                        Ok(MethodReturnCode::Success(opt_response_text)) => {
                                            if let Some(response_text) = opt_response_text {
                                                let resp_obj = serde_json::from_str::<{{prop_name|UpperCamelCase}}Property>(response_text.as_ref()).unwrap();
                                                {%if prop.arg_list | length == 1 %}
                                                let _ = responder.send(Some(resp_obj.{{prop.arg_list[0].name|snake_case}}));
                                                {%else%}
                                                let _ = responder.send(Some(resp_obj));
                                                {%endif%}
                                            } else {
                                                warn!("No response payload received for property update request for '{{prop_name}}'");
                                                let _ = responder.send(None);
                                            }
                                        },
                                        _ => {
                                            warn!("Property update request for '{{prop_name}}' failed: {:?}", result);
                                            let _ = responder.send(None);
                                        }
                                    };
                                }
                                Err(e) => {
                                    warn!("Failed to create property update message for '{{prop_name}}': {:?}", e);
                                    let _ = responder.send(None);
                                }
                            }
                        } else {
                            match message::property_update(&topic, &payload_obj, {{prop_name|snake_case}}_prop_version.load(std::sync::atomic::Ordering::Relaxed)) {
                                Ok(msg) => {
                                    let _publish_result = publisher_for_{{prop_name|snake_case}}_prop.publish_nowait(msg);
                                }
                                Err(e) => {
                                    warn!("Failed to create property update message for '{{prop_name}}': {:?}", e);
                                    continue;
                                }
                            }
                        }
                    };
                });
            }
        }
        {% endfor %}
        {%endif%}
        let _loop_task = tokio::spawn(async move {
            while let Ok(msg) = message_receiver.recv().await {
                {% if stinger.methods|length > 0 %}
                let opt_corr_id: Option<Uuid> = msg.correlation_data.as_ref().and_then(|b| {
                    // Try parsing as 16-byte binary UUID first
                    if b.len() == 16 {
                        Uuid::from_slice(b.as_ref()).ok()
                    } else {
                        // Try parsing as string UUID (36 bytes for hyphenated format)
                        String::from_utf8(b.to_vec())
                            .ok()
                            .and_then(|s| Uuid::parse_str(&s).ok())
                    }
                });
                let return_code = {{stinger.rust.client_struct_name}}::<C>::get_return_code_from_message(&msg);
                {%endif%} {# if there are some methods #}

                if let Some(subscription_id) = msg.subscription_id {
                    match subscription_id {
                        {%if stinger.methods|length > 0%}
                        _i if _i == sub_ids.any_method_response => {
                            debug!("Received method response message");
                            if opt_corr_id.is_some() {
                                let opt_sender = opt_corr_id
                                    .and_then(|uuid| {
                                        let mut hashmap = resp_map.lock().expect("Mutex was poisoned");
                                        hashmap.remove(&uuid)
                                    });
                                if let Some(sender) = opt_sender {
                                    let oss: oneshot::Sender<MethodReturnCode> = sender;
                                    if oss.send(return_code.clone()).is_err() {
                                        warn!("Failed to send method response  to waiting receiver");
                                    }
                                }
                            } else {
                                warn!("Received method response without correlation ID");
                            }
                        },
                        {%endif-%}
                        {%if stinger.properties|length > 0%}
                        _i if _i == sub_ids.any_property_update_response => {
                            debug!("Received property update response message");
                            if opt_corr_id.is_some() {
                                let opt_sender = opt_corr_id
                                    .and_then(|uuid| {
                                        let mut hashmap = resp_map.lock().expect("Mutex was poisoned");
                                        hashmap.remove(&uuid)
                                    });
                                if let Some(sender) = opt_sender {
                                    let oss: oneshot::Sender<MethodReturnCode> = sender;
                                    if oss.send(return_code.clone()).is_err() {
                                        warn!("Failed to send method response  to waiting receiver");
                                    }
                                }
                            } else {
                                warn!("Received method response without correlation ID");
                            }
                        },
                        {%endif%}
                        {%-for signal_name, signal in stinger.signals.items()%}
                        _i if sub_ids.{{signal_name | snake_case}}_signal == Some(_i) => {
                            debug!("Received {{signal_name}} signal message");
                            // Find broadcast channel.
                            let chan = sig_chans.{{signal_name | snake_case}}_sender.clone();
                            {%if signal.arg_list | length == 0 %}
                            // No arguments, send () to channel.
                            let _send_result = chan.send(());
                            {%elif signal.arg_list | length == 1 %}
                            // Single argument, extract it from payload and send to channel.
                            match serde_json::from_slice::<{{signal_name | UpperCamelCase}}SignalPayload>(&msg.payload) {
                                Ok(pl) => {
                                    let _send_result = chan.send(pl.{{signal.arg_list[0].name|snake_case}});
                                },
                                Err(e) => {
                                    warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                                    continue;
                                }
                            }
                            {%else%}
                            // {{signal.arg_list|length}} arguments, send the entire struct to the channel.
                            match serde_json::from_slice::<{{signal_name | UpperCamelCase}}SignalPayload>(&msg.payload) {
                                Ok(pl) => {
                                    let _send_result = chan.send(pl);
                                },
                                Err(e) => {
                                    warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                                    continue;
                                }
                            }
                            {%endif%}
                        },
                        {%endfor%}
                        {%-for prop_name, prop in stinger.properties.items()%}
                        _i if _i == sub_ids.{{prop_name | snake_case}}_property_value => {
                            debug!("Received {{prop_name|CONST_CASE}} property value");
                            // JSON deserialize into {{prop_name | UpperCamelCase}}Property struct
                            match serde_json::from_slice::<{{prop_name | UpperCamelCase}}Property>(&msg.payload) {
                                Ok(pl) => {
                                    // Get a write-guard and set the local copy of the property value.
                                    let mut guard = props.{{prop_name | snake_case}}.write().await;
                                    {%if prop.arg_list | length == 1 %}
                                    debug!("{{prop_name | CONST_CASE}} property value updated: {:?}", pl.{{prop.arg_list[0].name}});
                                    *guard = pl.{{prop.arg_list[0].name}}.clone();
                                    {%else%}
                                    debug!("{{prop_name | CONST_CASE}} property value updated: {:?}", pl);
                                    *guard = pl.clone();
                                    {%endif%}
                                    // Hold onto the write-guard while we set the local copy of the property version.
                                    if let Some(version_str) = msg.user_properties.get("PropertyVersion") {
                                        if let Ok(version_num) = version_str.parse::<u32>() {
                                            props.{{prop_name | snake_case}}_version.store(version_num, std::sync::atomic::Ordering::Relaxed);
                                        }
                                    }
                                },
                                Err(e) => {
                                    warn!("Failed to deserialize '{}' into {{signal_name | UpperCamelCase}}SignalPayload: {}", String::from_utf8_lossy(&msg.payload), e);
                                    continue;
                                }
                            }
                        },
                        {%endfor%}
                        unhandled_subscription_id => {
                            error!("Received message with unmatched subscription id: {}", unhandled_subscription_id);
                        }
                    }
                } else {
                    error!("Received message without a subscription id");
                }
            }   
        });

        Ok(())
    }
}
