{%- import "cloudevents.jinja.include" as cloudEvents %}
{%- import "mqtt.jinja.include" as mqtt %}
{%- import "util.jinja.include" as util -%}
{{ util.CommonFileHeader() }}
{%- set messagegroups = root.messagegroups %}
{%- set uses_cloudevents_message = (root | exists("envelope","CloudEvents/1.0")) %}
{%- set uses_plain_mqtt_message = (root | exists( "protocol", "mqtt" )) %}
{%- set uses_mqtt_endpoint = (root | exists( "protocol", "mqtt" )) %}

#nullable enable

using System.Text.RegularExpressions;
using MQTTnet;
using MQTTnet.Client;
using Microsoft.Extensions.Logging;
using {{ project_name | pascal }}.Tools;
{%- if uses_cloudevents_message %}
{{ mqtt.CloudEventsUsings() }}
{%- endif %}

namespace {{ project_name | pascal }}
{
    {% for messagegroupid, messagegroup in messagegroups.items() -%}
    {%- set uses_cloudevents_message = (messagegroup | exists("envelope","CloudEvents/1.0")) %}
    {%- set uses_plain_mqtt_message = (messagegroup | exists( "protocol", "mqtt" )) %}
    {%- set messagegroupname = messagegroupid  | pascal -%}
    {%- set class_name = ( messagegroupname | strip_namespace )+"Dispatcher" %}
    namespace {{ messagegroupname | pascal  }}
    {
        /// <summary>
        /// Dispatcher class for processing messages from an MQTT topic.
        /// </summary>
        public class {{ class_name }} : DispatcherBase, IDisposable
        {
            private readonly MqttSubscriber _subscriber;
            private readonly Dictionary<string, string> _topicMappings;
            private readonly Dictionary<string, Regex> _topicPatterns;
            private static readonly Regex UriTemplateRegex = new Regex(@"\{([A-Za-z0-9_]+)\}", RegexOptions.Compiled);
            {%- for messageid, message in messagegroup.messages.items() -%}
            {%- set messagename = messageid | strip_namespace | pascal -%}
            {%- set message_body_type = util.body_type(data_project_name, root, message) -%}
            {%- if message.description %}
            /// <summary>
            /// {{ message.description }}
            /// </summary>
            {%- endif %}
            {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
            {%- set isMqtt = (message | exists( "protocol", "mqtt" )) %}
            /// <summary>
            /// Event that is raised when a message of type {{ messagename }} is received.
            /// </summary>
            public event Func<MqttApplicationMessage, {% if isCloudEvent %}CloudEvent?, {% endif %}{{ message_body_type }}?, IDictionary<string, string>, Task>? {{ messagename | strip_namespace }}Async;
            {%- endfor %}
            /// <summary>
            /// Event that is raised when an unhandled message is received.
            /// </summary>
            public event Func<MqttApplicationMessage, Task>? UnhandledMessageAsync;

            /// <summary>
            /// Gets the default topic mappings for all messages in this group.
            /// </summary>
            /// <returns>A dictionary mapping message identifiers to their default topic patterns.</returns>
            public static Dictionary<string, string> GetDefaultTopicMappings()
            {
                return new Dictionary<string, string>
                {
                    {%- for messageid, message in messagegroup.messages.items() %}
                    { "{{ messageid }}", "{{ mqtt.GetDefaultTopic(message, messageid) }}" },
                    {%- endfor %}
                };
            }

            /// <summary>
            /// Gets the default subscription topics with URI template placeholders replaced by MQTT wildcards (+).
            /// </summary>
            /// <param name="topicMappings">Optional topic mappings to use. If null, uses default mappings.</param>
            /// <returns>A list of topic patterns suitable for MQTT subscription.</returns>
            public static List<string> GetSubscriptionTopics(Dictionary<string, string>? topicMappings = null)
            {
                var mappings = topicMappings ?? GetDefaultTopicMappings();
                var topics = new List<string>();
                foreach (var mapping in mappings.Values)
                {
                    // Replace URI template placeholders with MQTT wildcard (+)
                    var topic = UriTemplateRegex.Replace(mapping, "+");
                    if (!topics.Contains(topic))
                    {
                        topics.Add(topic);
                    }
                }
                return topics;
            }

            /// <summary>   
            /// Initializes a new instance of the <see cref="{{ class_name }}"/> class.
            /// </summary>
            /// <param name="subscriber">The MQTT subscriber to use.</param>
            /// <param name="topicMappings">Optional dictionary mapping message identifiers to topic patterns.
            /// Topic patterns may be URI templates with placeholders like {placeholder}.
            /// For consumers, placeholders are converted to MQTT wildcards (+) for subscription.
            /// If not provided, uses default 1:1 mapping.</param>
            /// <param name="loggerFactory">The logger factory to use.</param>
            public {{ class_name }}(MqttSubscriber subscriber, Dictionary<string, string>? topicMappings = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory?.CreateLogger<{{ class_name }}>() ?? new LoggerFactory().CreateLogger<{{ class_name }}>())
            {
                _subscriber = subscriber;
                _subscriber.Attach(this);
                _topicMappings = topicMappings ?? GetDefaultTopicMappings();
                _topicPatterns = BuildTopicPatterns(_topicMappings);
            }

            /// <summary>
            /// Topic mappings configuration
            /// </summary>
            public IReadOnlyDictionary<string, string> TopicMappings => _topicMappings;

            /// <summary>
            /// Builds regex patterns from topic mappings for matching incoming messages.
            /// </summary>
            private static Dictionary<string, Regex> BuildTopicPatterns(Dictionary<string, string> topicMappings)
            {
                var patterns = new Dictionary<string, Regex>();
                foreach (var kvp in topicMappings)
                {
                    // Convert URI template placeholders to named capture groups
                    var pattern = "^" + UriTemplateRegex.Replace(
                        Regex.Escape(kvp.Value).Replace("\\{", "{").Replace("\\}", "}"),
                        match => $"(?<{match.Groups[1].Value}>[^/]+)"
                    ) + "$";
                    patterns[kvp.Key] = new Regex(pattern, RegexOptions.Compiled);
                }
                return patterns;
            }

            /// <summary>
            /// Extracts URI template placeholder values from a topic.
            /// </summary>
            private IDictionary<string, string> ExtractTopicParameters(string topic, string messageId)
            {
                var parameters = new Dictionary<string, string>();
                if (_topicPatterns.TryGetValue(messageId, out var pattern))
                {
                    var match = pattern.Match(topic);
                    if (match.Success)
                    {
                        foreach (var groupName in pattern.GetGroupNames())
                        {
                            if (groupName != "0" && match.Groups[groupName].Success)
                            {
                                parameters[groupName] = match.Groups[groupName].Value;
                            }
                        }
                    }
                }
                return parameters;
            }

            void IDisposable.Dispose()
            {
                _subscriber.Detach(this);
            }            

            /// <summary>
            /// Processes a message from the MQTT topic
            /// </summary>
            /// <param name="messageArgs">The message to process.</param>
            internal override async Task<bool> ProcessMessageAsync(MqttApplicationMessage messageArgs)
            {
                _logger.LogInformation($"Processing MQTT message on topic {messageArgs.Topic}");
                {%- if uses_cloudevents_message %}
                if (IsCloudEvent(messageArgs))
                {
                    var cloudEvent = CloudEventFromMessage(messageArgs);
                    if (await DispatchCloudEventAsync(messageArgs, cloudEvent))
                    {
                        return true;
                    }
                }
                {%- else %}
                var message = messageArgs;
                if (message != null)
                {
                    if (await DispatchMessageAsync(messageArgs))
                    {
                        return true;
                    }
                }
                {%- endif %}
                if (UnhandledMessageAsync != null)
                {
                    await UnhandledMessageAsync(messageArgs);
                }
                return false;
            }

            {%- if uses_cloudevents_message %}
            /// <summary>
            /// Dispatches a CloudEvent to the appropriate handler.
            /// </summary>
            /// <param name="messageArgs">The message to dispatch.</param>
            /// <param name="cloudEvent">The CloudEvent to dispatch.</param>
            protected virtual async Task<bool> DispatchCloudEventAsync(MqttApplicationMessage messageArgs, CloudEvent cloudEvent)
            {
                var cloudEventType = cloudEvent.Type;
                _logger.LogInformation($"Dispatching CloudEvent of type {cloudEventType}");
                switch (cloudEventType)
                {
                    {% for messageid, message in messagegroup.messages.items() -%}
                    {%- set messagename = messageid | pascal %}
                    {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
                    {%- set message_body_type = util.body_type(data_project_name, root, message) -%}
                    {%- if "type" in message.envelopemetadata and "value" in message.envelopemetadata["type"] -%}
                    case "{{ message.envelopemetadata["type"]["value"] }}":
                    {%- else -%}
                    case "{{ messageid }}":
                    {%- endif %}
                        if ({{ messagename | strip_namespace }}Async != null)
                        {
                            var topicParams = ExtractTopicParameters(messageArgs.Topic, "{{ messageid }}");
                            await Task.WhenAll(Array.ConvertAll({{ messagename | strip_namespace }}Async.GetInvocationList(), e => (Task)(e.DynamicInvoke(messageArgs,
                            {%- if isCloudEvent %}cloudEvent, {%- endif %}
                            {%- if message_body_type != "byte[]" -%}{{ message_body_type }}.FromData(cloudEvent.Data, cloudEvent.DataContentType)
                            {%- else -%}
                            cloudEvent.Data is BinaryData binaryData ? binaryData.ToArray() : null
                            {%- endif %}, topicParams))));
                        }
                        return true;
                    {%- endfor %}
                    default:
                        return false;
                }
            }
            {%- endif %}

            {%- if uses_plain_mqtt_message %}
            /// <summary>
            /// Dispatches a message to the appropriate handler based on topic pattern matching.
            /// </summary>
            /// <param name="messageArgs">The message to dispatch.</param>
            protected virtual async Task<bool> DispatchMessageAsync(MqttApplicationMessage messageArgs)
            {
                var messageTopic = messageArgs.Topic;
                _logger.LogInformation($"Dispatching MQTT message on topic {messageTopic}");
                
                // Try to match topic against registered patterns
                {% for messageid, message in messagegroup.messages.items() if ((message | exists( "protocol", "mqtt" )) and not (message | exists("envelope","CloudEvents/1.0"))) -%}
                {%- set messagename = messageid | pascal %}
                {%- set isCloudEvent = cloudEvents.isCloudEvent(message) %}
                {%- set message_body_type = util.body_type(data_project_name, root, message) -%}
                if (_topicPatterns.TryGetValue("{{ messageid }}", out var pattern{{ loop.index }}) && pattern{{ loop.index }}.IsMatch(messageTopic))
                {
                    if ({{ messagename | strip_namespace }}Async != null)
                    {
                        var topicParams = ExtractTopicParameters(messageTopic, "{{ messageid }}");
                        await Task.WhenAll(Array.ConvertAll({{ messagename | strip_namespace }}Async.GetInvocationList(), e => (Task)(e.DynamicInvoke(messageArgs,
                        {%- if isCloudEvent %}null, {% endif %}
                        {%- if message_body_type != "byte[]" -%}
                        {{ message_body_type }}.FromData(messageArgs.Payload, messageArgs.ContentType)
                        {%- else -%}
                        messageArgs.Payload.ToObject<{{ message_body_type }}>()
                        {%- endif %}, topicParams) )));
                    }
                    return true;
                }
                {%- endfor %}
                
                return false;
            }
            {%- endif %}
        }
    }

    {% endfor %}

    {%- if root.endpoints -%}
    {%- for endpointid, endpoint in root.endpoints.items() -%}
    {%- set endpointname = endpointid | default(endpointid) | pascal -%}
    {%- if endpoint.usage and "consumer" in endpoint.usage -%}
    {%- set protocol = endpoint.protocol | lower -%}
    {%- if protocol.startswith("mqtt") -%}
    {%- set options = endpoint.protocoloptions -%}
    {%- set endpoints = endpoint.endpoints -%}
    {%- set messagegroupuris = endpoint.messagegroups %}
    
    {%- macro MessageGroupArgs() -%}
    {%- for uri in messagegroupuris %}
    {%- set messagegroup = schema_object( root, uri ) -%}
    {%- if messagegroup %}
    {%- set messagegroupname = messagegroup.messagegroupid | pascal -%}
    {{ messagegroupname }}EventDispatcher? {{ messagegroupname | strip_namespace | camel }}Dispatcher = null
    {%- if not loop.last -%}, {%- endif -%}
    {%- endif %}
    {%- endfor %}
    {%- endmacro -%}
    
    {%- macro MessageGroupHooks(arg) -%}
    Task.WhenAll(
    {%- for uri in messagegroupuris %}
    {%- set messagegroup = schema_object( root, uri ) -%}
    {% if messagegroup %}
    {%- set messagegroupname = messagegroup.messagegroupid | pascal -%}
    {{- messagegroupname | strip_namespace | camel }}Dispatcher != null?{{- messagegroupname | strip_namespace | camel }}Dispatcher.ProcessMessageAsync({{ arg }}):Task.CompletedTask
    {%- if not loop.last -%}, {%- endif -%}
    {%- endif %}
    {%- endfor %}
    )
    {%- endmacro %}

    namespace {{ endpointname | concat_namespace(project_name) | pascal }}
    {
        {%- set class_name = ( endpointname | strip_namespace )+"MqttProcessorFactory" %}
        /// <summary>
        /// Factory class for creating MqttSubscriber instances for the {{ endpointname }} endpoint.
        /// </summary>
        public class {{ class_name }}
        {
            /// <summary>
            /// Creates an MqttSubscriber instance for the {{ endpointname }} endpoint.
            /// </summary>
            /// <param name="options">The MqttClientOptions to use for the MqttSubscriber.</param>
            /// <param name="topics">The topics to subscribe to.</param>
            public static MqttSubscriber CreateMqttProcessorFor{{ endpointid | pascal | strip_namespace }}(IMqttClient client, string[] topics, {{ MessageGroupArgs() }})
            {
                var subscriber = new MqttSubscriber(client, options, topics);
                {%- for uri in messagegroupuris %}
                {%- set messagegroup = schema_object( root, uri ) -%}
                {%- if messagegroup %}
                {%- set messagegroupname = messagegroup.messagegroupid | pascal -%}
                if ({{ messagegroupname | strip_namespace | camel }}Dispatcher != null)
                {
                    subscriber.Attach({{ messagegroupname | strip_namespace | camel }}Dispatcher);
                }
                {%- endif %}
                {%- endfor %}
                return subscriber;
            }
        }
    }

    {%- endif -%}
    {%- endif -%}
    {%- endfor -%}
    {%- endif %}

    {%- set dispatcherBase = "global::"+( project_name | pascal )+".Tools.DispatcherBase" %}
    /// <summary>
    /// Extensions for the MqttSubscriber class to attach and detach dispatchers.
    /// </summary>
    public static class {{ project_name | pascal | strip_dots }}SubscriberExtensions
    {
        private static readonly Dictionary<MqttSubscriber, List<{{ dispatcherBase }}>> Dispatchers =
            new Dictionary<MqttSubscriber, List<{{ dispatcherBase }}>>();

        /// <summary>
        /// Attaches a dispatcher to the MqttSubscriber.
        /// </summary>
        /// <param name="subscriber">The MqttSubscriber to attach the dispatcher to.</param>
        /// <param name="dispatcher">The dispatcher to attach.</param>
        public static void Attach(this MqttSubscriber subscriber, {{ dispatcherBase }} dispatcher)
        {
            if (!Dispatchers.ContainsKey(subscriber))
            {
                Dispatchers[subscriber] = new List<{{ dispatcherBase }}>();
                subscriber.ProcessMessage += async (messageArgs) => await ProcessMessageAsync(subscriber, messageArgs);
                subscriber.ProcessError += async (error) => await ProcessErrorAsync(subscriber, error);
            }
            Dispatchers[subscriber].Add(dispatcher);
        }

        /// <summary>
        /// Detaches a dispatcher from the MqttSubscriber.
        /// </summary>
        /// <param name="subscriber">The MqttSubscriber to detach the dispatcher from.</param>
        /// <param name="dispatcher">The dispatcher to detach.</param>
        public static void Detach(this MqttSubscriber subscriber, {{ dispatcherBase }} dispatcher)
        {
            if (Dispatchers.ContainsKey(subscriber))
            {
                Dispatchers[subscriber].Remove(dispatcher);
                if (Dispatchers[subscriber].Count == 0)
                {
                    Dispatchers.Remove(subscriber);
                }
            }
        }

        private static async Task ProcessMessageAsync(MqttSubscriber subscriber, MqttApplicationMessage messageArgs)
        {
            foreach (var dispatcher in Dispatchers[subscriber])
            {
                if (await dispatcher.ProcessMessageAsync(messageArgs))
                {
                    return;
                }
            }
        }

        private static async Task ProcessErrorAsync(MqttSubscriber subscriber, Exception error)
        {
            foreach (var dispatcher in Dispatchers[subscriber])
            {
                await dispatcher.ProcessErrorAsync(error);
            }
        }
    }
 
}
