{%- import "cloudevents.jinja.include" as cloudEvents %}
{%- import "kafka.jinja.include" as kafka %}
{%- 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_kafka_message = (root | exists( "protocol", "kafka" )) %}
{%- set uses_kafka_endpoint = (root | exists( "protocol", "kafka" )) %}
// This code was generated by the xRegistry tool.
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.

#nullable enable

using Confluent.Kafka;
using Microsoft.Extensions.Logging;
using {{ project_name | pascal }}.Tools;
{%- if uses_cloudevents_message %}
{{ kafka.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_kafka_message = (messagegroup | existswithout( "binding", "kafka", "format", "cloudevents" )) %}
    {%- set messagegroupname = messagegroupid  | pascal -%}
    {%- set class_name = ( messagegroupname | strip_namespace )+"EventDispatcher" %}
    namespace {{ messagegroupname | pascal  }}
    {
        public class {{ class_name }} : DispatcherBase
        {
            {%- if (messagegroup | exists("envelope","CloudEvents/1.0")) %}

            {%- endif %}
            {%- 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 isKafka = (message | existswithout( "binding", "kafka", "format", "cloudevents" )) %}
            public event Func<ConsumeResult<byte[], byte[]>, {% if isCloudEvent %}CloudEvent?, {% endif %}{{ message_body_type }}?, Task>? {{ messagename | strip_namespace }}Async;
            {%- endfor %}
            public event Func<ConsumeResult<byte[], byte[]>, Task>? UnhandledMessageAsync;

            public {{ class_name }}(ILoggerFactory? loggerFactory = null) : base(loggerFactory?.CreateLogger<{{ class_name }}>() ?? new LoggerFactory().CreateLogger<{{ class_name }}>())
            {
            }

            internal override async Task<bool> ProcessMessageAsync(ConsumeResult<byte[], byte[]> messageArgs)
            {
                _logger.LogInformation($"Processing message from partition {messageArgs.Partition}");
                {%- if uses_cloudevents_message %}
                if (IsCloudEvent(messageArgs.Message))
                {
                    var cloudEvent = CloudEventFromMessage(messageArgs.Message);
                    if (await DispatchCloudEventAsync(messageArgs, cloudEvent))
                    {
                        return true;
                    }
                }
                {%- else %}
                var message = messageArgs.Message;
                if (message != null)
                {
                    if (await DispatchMessageAsync(messageArgs))
                    {
                        return true;
                    }
                }
                {%- endif %}
                if (UnhandledMessageAsync != null)
                {
                    await UnhandledMessageAsync(messageArgs);
                }
                return false;
            }

            {%- if uses_cloudevents_message %}
            protected virtual async Task<bool> DispatchCloudEventAsync(ConsumeResult<byte[], byte[]> 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 tasks = new List<Task>();
                            foreach (var handler in {{ messagename | strip_namespace }}Async.GetInvocationList())
                            {
                                if ( handler == null ) continue;
                                var t = (Task?)(handler.DynamicInvoke(
                                    messageArgs,
                                    cloudEvent,
                                    {%- if message_body_type != "byte[]" -%}
                                    {{ message_body_type }}.FromData(cloudEvent.Data, cloudEvent.DataContentType)
                                    {%- else -%}
                                    cloudEvent.Data is BinaryData binaryData ? binaryData.ToArray() : null
                                    {%- endif %}));
                                if (t != null)
                                {
                                    tasks.Add(t);
                                }
                            }
                            await Task.WhenAll(tasks);
                        }
                        return true;
                    {%- endfor %}
                    default:
                        return false;
                }
            }
            {%- endif %}

            {%- if uses_plain_kafka_message %}
            protected virtual async Task<bool> DispatchMessageAsync(ConsumeResult<byte[], byte[]> messageArgs)
            {
                var messageSubject = messageArgs.Message.Headers.FirstOrDefault(h => h.Key == "subject")?.GetValueBytes() != null
                    ? Encoding.UTF8.GetString(messageArgs.Message.Headers.FirstOrDefault(h => h.Key == "subject")?.GetValueBytes())
                    : null;
                _logger.LogInformation($"Dispatching message with subject {messageSubject}");
                switch (messageSubject)
                {
                {% for messageid, message in messagegroup.messages.items() if ((message | exists( "protocol", "kafka" )) 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) -%}
                    case "{{ messageid }}":
                        if ({{ messagename | strip_namespace }}Async != null)
                        {
                            var tasks = new List<Task>();
                            foreach (var handler in {{ messagename | strip_namespace }}Async.GetInvocationList())
                            {
                                if ( handler == null ) continue;
                                var t = (Task?)(handler.DynamicInvoke(
                                    messageArgs,
                                    {%- if message_body_type != "byte[]" -%}
                                    {{ message_body_type }}.FromData(messageArgs.Message.Value, messageArgs.Message.Headers.FirstOrDefault(h => h.Key == "content-type")?.GetValueBytes() != null ? Encoding.UTF8.GetString(messageArgs.Message.Headers.FirstOrDefault(h => h.Key == "content-type")?.GetValueBytes()) : null)
                                    {%- else -%}
                                    messageArgs.Message.Value
                                    {%- endif %}));
                                if (t != null)
                                {
                                    tasks.Add(t);
                                }
                            }
                            await Task.WhenAll(tasks);
                        }
                        return true;
                {%- endfor %}
                    default:
                        return false;
                }
            }
            {%- endif %}

            public KafkaProcessor<byte[], byte[]> CreateKafkaProcessor(ConsumerConfig config, string topicName)
            {
                var consumer = new ConsumerBuilder<byte[], byte[]>(config).Build();
                var processor = new KafkaProcessor<byte[], byte[]>(consumer, topicName);
                processor.Attach(this);
                return processor;
            }
        }
    }

    {% endfor %}

    {% set dispatcherBase = "global::"+( project_name | pascal )+".Tools.DispatcherBase" %}
    public static class {{ project_name | pascal | strip_dots}}ProcessorExtensions
    {
        private static readonly Dictionary<KafkaProcessor<byte[], byte[]>, List<{{ dispatcherBase }}>> Dispatchers =
            new Dictionary<KafkaProcessor<byte[], byte[]>, List<{{ dispatcherBase }}>>();

        public static void Attach(this KafkaProcessor<byte[], byte[]> processor, {{ dispatcherBase }} dispatcher)
        {
            if (!Dispatchers.ContainsKey(processor))
            {
                Dispatchers[processor] = new List<{{ dispatcherBase }}>();
                processor.ProcessMessage += async (messageArgs) => await ProcessMessageAsync(processor, messageArgs);
                processor.ProcessError += async (error) => await ProcessErrorAsync(processor, error);
            }
            Dispatchers[processor].Add(dispatcher);
        }

        public static void Detach(this KafkaProcessor<byte[], byte[]> processor, {{ dispatcherBase }} dispatcher)
        {
            if (Dispatchers.ContainsKey(processor))
            {
                Dispatchers[processor].Remove(dispatcher);
                if (Dispatchers[processor].Count == 0)
                {
                    Dispatchers.Remove(processor);
                }
            }
        }

        private static async Task ProcessMessageAsync(KafkaProcessor<byte[], byte[]> processor, ConsumeResult<byte[], byte[]> messageArgs)
        {
            foreach (var dispatcher in Dispatchers[processor])
            {
                if (await dispatcher.ProcessMessageAsync(messageArgs))
                {
                    return;
                }
            }
        }

        private static async Task ProcessErrorAsync(KafkaProcessor<byte[], byte[]> processor, Exception error)
        {
            foreach (var dispatcher in Dispatchers[processor])
            {
                await dispatcher.ProcessErrorAsync(error);
            }
        }
    }

    {%- 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("kafka") -%}
    {%- 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 messagegroupid = uri.split('/')[-1] -%}
    {%- set messagegroupname = messagegroupid | pascal -%}
    global::{{ messagegroupname | concat_namespace(project_name) | pascal }}.{{ messagegroupname | strip_namespace }}EventDispatcher? {{ messagegroupname | strip_namespace | camel }}Dispatcher = null
    {%- if not loop.last -%}, {%- endif -%}
    {%- endif %}
    {%- endfor %}
    {%- endmacro -%}
    {%- macro MessageGroupHooks() -%}
    {%- for uri in messagegroupuris %}
    {%- set messagegroup = schema_object( root, uri ) -%}
    {% if messagegroup %}
    {%- set messagegroupid = uri.split('/')[-1] -%}
    {%- set messagegroupname = messagegroupid | pascal -%}
        if ({{ messagegroupname | strip_namespace | camel }}Dispatcher != null)
        {
            processor.Attach( {{ messagegroupname | strip_namespace | camel }}Dispatcher);
        }
    {% endif %}
    {%- endfor %}
    {%- endmacro -%}
    {%- macro createforbody(class_name, endpoints, options) -%}
        {%- set kafkatopic = geturlpath(endpoints[0].uri)[1:] -%}
        var consumer = new ConsumerBuilder<byte[], byte[]>(config).Build();
        var processor = new KafkaProcessor<byte[], byte[]>(consumer, topicName, _loggerFactory);
        {{ MessageGroupHooks() }}
        return processor;
    {%- endmacro %}

    namespace {{ endpointname | concat_namespace(project_name) | pascal }}
    {
        {%- set class_name = ( endpointname | strip_namespace )+"KafkaProcessorFactory" %}
        public class {{ class_name }}
        {
            public static KafkaProcessor<byte[], byte[]> CreateKafkaProcessorFor{{ endpointid | pascal | strip_namespace }}(ConsumerConfig config, 
                {%- if not (options and "topic" in options) %}
                string topicName, 
                {%- endif %}
                {%- if not (options and "consumergroup" in options) %}
                string consumerGroup, 
                {%- endif %}
                {{ MessageGroupArgs() }}, ILoggerFactory? _loggerFactory = null)
            {
                {%- if options and "consumergroup" in options %}
                config.GroupId = "{{ options.consumergroup }}";
                {%- else %}
                config.GroupId = consumerGroup ?? throw new ArgumentNullException(nameof(consumerGroup), "Consumer group must be specified either in endpoint metadata or as a parameter");
                {%- endif %}
                {%- if options and "topic" in options %}
                string topicName = "{{ options.topic }}";
                {%- else %}
                if (string.IsNullOrEmpty(topicName))
                {
                    throw new ArgumentNullException(nameof(topicName), "Topic must be specified either in endpoint metadata or as a parameter");
                }
                {%- endif %}
                {{ createforbody(class_name, endpoints, options) | indent(8) }}
            }
        }
    }

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