{%- import "util.jinja.include" as util -%}
{%- import "cloudevents.jinja.include" as cloudEvents -%}
{%- set messagegroups = root.messagegroups %}
{%- set uses_cloudevents_message = cloudEvents.usesCloudEvents(root) %}
{% for messagegroupid, messagegroup in messagegroups.items() -%}
{%- set groupname = messagegroupid | pascal -%}
{%- set class_name = (groupname | strip_namespace) + "EventConsumer" %}
# {{ project_name | pascal }} - AMQP Consumer

Auto-generated AMQP consumer for {{ groupname }} message group.

## Table of Contents

- [Overview](#overview)
- [Quick Start](#quick-start)
- [Available Event Handlers](#available-event-handlers)
- [Message Disposition](#message-disposition)
- [Authentication](#authentication)
- [Error Handling](#error-handling)
- [Connection Management](#connection-management)
- [Lifecycle Management](#lifecycle-management)
- [Configuration Options](#configuration-options)
- [Advanced Topics](#advanced-topics)
- [Testing](#testing)
- [Logging](#logging)
- [Dependencies](#dependencies)

## Overview

This consumer provides event handlers for incoming AMQP messages using the CloudEvents specification.

## Quick Start

### Using Builder Pattern (Recommended)

```csharp
using {{ project_name | pascal }};
using CloudNative.CloudEvents;
using Microsoft.Extensions.Logging;

// Create consumer using fluent builder
var consumer = {{ class_name }}.CreateBuilder()
    .WithEndpoint("amqp://localhost:5672")
    .WithCredential(new PlainEndpointCredential("username", "password"))
    .WithNode("/queue/events")
    .WithLogger(loggerFactory)
    .Build();

// Register event handlers
{%- set first_message = messagegroup.messages.items() | first %}
{%- if first_message %}
{%- set messageid, message = first_message %}
{%- set messagename = messageid | strip_dots | pascal %}
{%- set message_body_type = util.body_type(data_project_name, root, message) %}
consumer.{{ messagename }}Async += async (cloudEvent, data, context) =>
{
    Console.WriteLine($"Received {{ messagename }} event");
    // Process data
    
    // Accept the message to acknowledge successful processing
    context.Accept();
    
    // Or release the message for redelivery if processing failed
    // context.Release();
    
    // Or reject the message as permanently failed
    // context.Reject();
};
{%- endif %}

// Register error handlers
consumer.MessageProcessingErrorAsync += async (cloudEvent, exception) =>
{
    Console.WriteLine($"Error processing message: {exception.Message}");
};

consumer.DeserializationErrorAsync += async (cloudEvent, exception) =>
{
    Console.WriteLine($"Error deserializing message: {exception.Message}");
};

// Start consuming
await consumer.StartAsync();

// Keep running until cancellation
await Task.Delay(-1, cancellationToken);

// Stop and cleanup
await consumer.DisposeAsync();
```

### Using Factory Method

```csharp
var consumer = {{ class_name }}.CreateConsumer(
    new PlainEndpointCredential("username", "password"),
    new Uri("amqp://localhost:5672"),
    loggerFactory,
    new Dictionary<string, string> { ["node"] = "/queue/events" });

// Register handlers
consumer.{{ messagegroup.messages.keys() | first | strip_dots | pascal }}Async += async (ce, data, context) => { 
    // Process message
    context.Accept(); 
};

await consumer.StartAsync();
```

## Available Event Handlers

Subscribe to handlers for specific event types:

{% for messageid, message in messagegroup.messages.items() -%}
{%- set messagename = messageid | strip_dots | pascal %}
{%- set message_body_type = util.body_type(data_project_name, root, message) %}
### {{ messagename }}

**Event Type:** `{{ messageid }}`  
**Data Type:** `{{ message_body_type }}`

```csharp
consumer.{{ messagename }}Async += async (CloudEvent cloudEvent, {{ message_body_type }}? data, MessageContext context) =>
{
    if (data != null)
    {
        // Process the event data
        Console.WriteLine($"Received {{ messagename }}");
        
        // Accept the message when processing is successful
        context.Accept();
    }
    else
    {
        // Reject malformed messages
        context.Reject();
    }
};
```

{% endfor %}

### Unhandled Events

Handle events that don't match any registered type:

```csharp
consumer.UnhandledEventAsync += async (CloudEvent cloudEvent, MessageContext context) =>
{
    Console.WriteLine($"Received unhandled event type: {cloudEvent.Type}");
    // Reject unhandled messages or accept them to prevent redelivery
    context.Reject();
};
```

## Message Disposition

Every message handler receives a `MessageContext` parameter that provides control over message acknowledgment. You must call one of the disposition methods to complete message processing:

### Accept

Indicates the message was successfully processed and should be removed from the queue:

```csharp
consumer.MyEventAsync += async (cloudEvent, data, context) =>
{
    // Process successfully
    await ProcessAsync(data);
    
    // Acknowledge success
    context.Accept();
};
```

### Release

Returns the message to the queue for redelivery (e.g., temporary processing failure):

```csharp
consumer.MyEventAsync += async (cloudEvent, data, context) =>
{
    try
    {
        await ProcessAsync(data);
        context.Accept();
    }
    catch (TemporaryException ex)
    {
        // Release for retry
        context.Release();
    }
};
```

### Reject

Permanently rejects the message, typically moving it to a dead-letter queue:

```csharp
consumer.MyEventAsync += async (cloudEvent, data, context) =>
{
    if (!IsValid(data))
    {
        // Reject invalid messages
        context.Reject();
        return;
    }
    
    await ProcessAsync(data);
    context.Accept();
};
```

### Modify

Advanced disposition that allows fine-grained control:

```csharp
consumer.MyEventAsync += async (cloudEvent, data, context) =>
{
    try
    {
        await ProcessAsync(data);
        context.Accept();
    }
    catch (Exception)
    {
        // Mark as delivery failed, but allow redelivery here
        context.Modify(deliveryFailed: true, undeliverableHere: false);
    }
};
```

**Important:** Each message must have exactly one disposition method called. Calling multiple methods or not calling any method will result in undefined behavior.

## Authentication

Configure authentication credentials to connect to the AMQP broker.

### Plain Credentials

```csharp
var credential = new PlainEndpointCredential("username", "password");
```

### Token-Based Authentication

```csharp
var credential = new TokenEndpointCredential(async () => 
{
    // Fetch and return access token
    return await GetAccessTokenAsync();
});
```

## Error Handling

### Message Processing Errors

Handle errors that occur while processing messages:

```csharp
consumer.MessageProcessingErrorAsync += async (CloudEvent cloudEvent, Exception exception) =>
{
    logger.LogError(exception, "Error processing event {EventType}", cloudEvent.Type);
    
    // Optionally send to dead letter queue
    await SendToDeadLetterQueueAsync(cloudEvent);
};
```

### Deserialization Errors

Handle errors when deserializing event data:

```csharp
consumer.DeserializationErrorAsync += async (CloudEvent cloudEvent, Exception exception) =>
{
    logger.LogError(exception, "Error deserializing event {EventType}", cloudEvent.Type);
    
    // Log raw event for investigation
    logger.LogWarning("Raw CloudEvent: {CloudEvent}", cloudEvent);
};
```

### Complete Error Handling Example

```csharp
var consumer = {{ class_name }}.CreateBuilder()
    .WithEndpoint("amqp://localhost:5672")
    .WithCredential(credential)
    .Build();

// Handle processing errors with retry logic
consumer.MessageProcessingErrorAsync += async (cloudEvent, exception) =>
{
    logger.LogError(exception, 
        "Failed to process event {EventType} with ID {EventId}", 
        cloudEvent.Type, cloudEvent.Id);
    
    // Implement retry strategy
    if (exception is TransientException)
    {
        await RetryAsync(cloudEvent);
    }
    else
    {
        await SendToDeadLetterAsync(cloudEvent);
    }
};

// Handle deserialization errors
consumer.DeserializationErrorAsync += async (cloudEvent, exception) =>
{
    logger.LogError(exception, 
        "Failed to deserialize event {EventType}", 
        cloudEvent.Type);
    
    // Save raw data for investigation
    await SaveRawEventAsync(cloudEvent);
};
```

{%- set first_message = messagegroup.messages.items() | first %}
{%- if first_message %}
{%- set messageid, message = first_message %}
{%- set messagename = messageid | strip_dots | pascal %}

```csharp
// Handle business logic errors in individual handlers
consumer.{{ messagename }}Async += async (cloudEvent, data, context) =>
{
    try
    {
        await ProcessBusinessLogicAsync(data);
        context.Accept();
    }
    catch (ValidationException ex)
    {
        logger.LogWarning(ex, "Validation failed for event {EventId}", cloudEvent.Id);
        context.Reject(); // Don't retry invalid messages
    }
    catch (TransientException ex)
    {
        logger.LogWarning(ex, "Temporary failure for event {EventId}", cloudEvent.Id);
        context.Release(); // Retry later
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Unexpected error for event {EventId}", cloudEvent.Id);
        context.Reject(); // Move to DLQ
    }
};
```
{%- endif %}

## Connection Management

### Connection Retry Logic

Implement automatic reconnection when connection is lost:

```csharp
public async Task<{{ class_name }}> CreateConsumerWithRetryAsync(
    ILoggerFactory loggerFactory,
    CancellationToken cancellationToken = default)
{
    var maxRetries = 5;
    var retryDelay = TimeSpan.FromSeconds(5);
    
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            var consumer = {{ class_name }}.CreateBuilder()
                .WithEndpoint("amqp://localhost:5672")
                .WithCredential(new PlainEndpointCredential("username", "password"))
                .WithNode("/queue/events")
                .WithLogger(loggerFactory)
                .Build();
            
            await consumer.StartAsync();
            logger.LogInformation("Successfully connected to AMQP broker");
            return consumer;
        }
        catch (Exception ex) when (attempt < maxRetries)
        {
            logger.LogWarning(ex, 
                "Failed to connect (attempt {Attempt}/{MaxRetries}). Retrying in {Delay}s...", 
                attempt, maxRetries, retryDelay.TotalSeconds);
            
            await Task.Delay(retryDelay, cancellationToken);
            retryDelay = TimeSpan.FromSeconds(retryDelay.TotalSeconds * 2); // Exponential backoff
        }
    }
    
    throw new InvalidOperationException("Failed to connect after maximum retries");
}
```

### Handling Connection Drops

Monitor and recover from connection failures:

```csharp
var consumer = {{ class_name }}.CreateBuilder()
    .WithEndpoint("amqp://localhost:5672")
    .WithCredential(credential)
    .Build();

// Monitor connection health
var connectionLost = false;

consumer.MessageProcessingErrorAsync += async (cloudEvent, exception) =>
{
    if (exception is Amqp.AmqpException)
    {
        connectionLost = true;
        logger.LogError("Connection to AMQP broker lost");
        
        // Trigger reconnection
        await ReconnectAsync(consumer);
    }
};

async Task ReconnectAsync({{ class_name }} consumer)
{
    await consumer.DisposeAsync();
    
    // Wait before reconnecting
    await Task.Delay(TimeSpan.FromSeconds(5));
    
    // Create new consumer instance
    consumer = await CreateConsumerWithRetryAsync(loggerFactory);
    
    // Re-register handlers
    RegisterHandlers(consumer);
    
    connectionLost = false;
}
```

### Graceful Shutdown with CancellationToken

Properly handle application shutdown:

```csharp
public async Task RunConsumerAsync(CancellationToken cancellationToken)
{
    var consumer = {{ class_name }}.CreateBuilder()
        .WithEndpoint("amqp://localhost:5672")
        .WithCredential(new PlainEndpointCredential("username", "password"))
        .WithNode("/queue/events")
        .WithLogger(loggerFactory)
        .Build();
```

{%- set first_message = messagegroup.messages.items() | first %}
{%- if first_message %}
{%- set messageid, message = first_message %}
{%- set messagename = messageid | strip_dots | pascal %}

```csharp    
    // Register handlers
    consumer.{{ messagename }}Async += async (cloudEvent, data, context) =>
    {
        // Check cancellation before processing
        cancellationToken.ThrowIfCancellationRequested();
        
        await ProcessEventAsync(data, cancellationToken);
        context.Accept();
    };
```
{%- endif %}

```csharp    
    try
    {
        await consumer.StartAsync();
        logger.LogInformation("Consumer started successfully");
        
        // Wait for shutdown signal
        await Task.Delay(Timeout.Infinite, cancellationToken);
    }
    catch (OperationCanceledException)
    {
        logger.LogInformation("Shutdown requested, stopping consumer gracefully...");
    }
    finally
    {
        // Graceful cleanup
        try
        {
            await consumer.DisposeAsync();
            logger.LogInformation("Consumer stopped successfully");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error during consumer shutdown");
        }
    }
}

// Usage in Program.cs
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
    e.Cancel = true;
    cts.Cancel();
    logger.LogInformation("Shutdown initiated...");
};

await RunConsumerAsync(cts.Token);
```

## Lifecycle Management

### Start and Stop

```csharp
// Start consuming messages
await consumer.StartAsync();

// Stop consuming (graceful shutdown)
await consumer.StopAsync();
```

### Async Disposal

The consumer implements `IAsyncDisposable` for proper cleanup:

```csharp
await using var consumer = {{ class_name }}.CreateBuilder()
    .WithEndpoint("amqp://localhost:5672")
    .WithCredential(credential)
    .Build();

await consumer.StartAsync();

// Use consumer...

// Disposed automatically (calls StopAsync internally)
```

## Configuration Options

| Option | Description | Example |
|--------|-------------|---------|
| `node` | AMQP queue or topic name | `/queue/events` |
| `filter` | Message filter expression | `priority = 'high'` |

```csharp
consumer = {{ class_name }}.CreateBuilder()
    .WithNode("/queue/events")
    .WithOption("filter", "priority = 'high'")
    .Build();
```

## Logging

The consumer uses `Microsoft.Extensions.Logging` for diagnostic output. Configure logging to monitor consumer health and troubleshoot issues.

### Log Levels

The consumer logs at different levels:

| Level | Events |
|-------|--------|
| **Information** | Connection established, consumer started/stopped, message received |
| **Warning** | Connection retry attempts, transient errors, invalid message format |
| **Error** | Connection failures after max retries, handler exceptions, deserialization failures |
| **Debug** | Message disposition (Accept/Release/Reject), handler invocation details |

### Configuring Logging

```csharp
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information)
        // Add filters for specific namespaces
        .AddFilter("{{ project_name | pascal }}", LogLevel.Debug);
});

var consumer = {{ class_name }}.CreateBuilder()
    .WithEndpoint("amqp://localhost:5672")
    .WithCredential(credential)
    .WithLogger(loggerFactory)
    .Build();
```

### What Gets Logged

**Connection Events:**
```
[Information] Connecting to AMQP broker at amqp://localhost:5672
[Information] Successfully connected to AMQP broker
[Warning] Connection lost, attempting reconnection (attempt 1/5)
[Error] Failed to connect after 5 attempts
```

**Message Processing:**
```
[Debug] Received message with type: com.example.OrderCreated
[Debug] Invoking handler for OrderCreated
[Debug] Message disposition: Accept
[Warning] Handler threw TransientException, releasing message for retry
```

**Errors:**
```
[Error] Failed to deserialize message: Invalid JSON format
[Error] Handler exception for event 12345: NullReferenceException
```

### Logging Best Practices

1. **Production Environments:** Use `LogLevel.Information` or `LogLevel.Warning` to reduce log volume
2. **Development/Staging:** Use `LogLevel.Debug` for detailed diagnostics
3. **Structured Logging:** Include event IDs and correlation IDs for tracing

{%- set first_message = messagegroup.messages.items() | first %}
{%- if first_message %}
{%- set messageid, message = first_message %}
{%- set messagename = messageid | strip_dots | pascal %}

```csharp
consumer.{{ messagename }}Async += async (cloudEvent, data, context) =>
{
    using (logger.BeginScope(new Dictionary<string, object>
    {
        ["EventId"] = cloudEvent.Id,
        ["EventType"] = cloudEvent.Type,
        ["Source"] = cloudEvent.Source
    }))
    {
        logger.LogInformation("Processing event {EventType}", cloudEvent.Type);
        
        try
        {
            await ProcessAsync(data);
            context.Accept();
            logger.LogInformation("Successfully processed event");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process event");
            context.Reject();
        }
    }
};
```
{%- endif %}

## Advanced Topics

### Multiple Handlers

Register multiple handlers for the same event type:

```csharp
{%- set first_msg_name = messagegroup.messages.keys() | first | strip_dots | pascal %}
// Handler 1: Log event
consumer.{{ first_msg_name }}Async += async (ce, data) =>
{
    logger.LogInformation("Event received: {EventId}", ce.Id);
};

// Handler 2: Process business logic
consumer.{{ first_msg_name }}Async += async (ce, data) =>
{
    await ProcessEventAsync(data);
};

// Handler 3: Send metrics
consumer.{{ first_msg_name }}Async += async (ce, data) =>
{
    await metrics.RecordEventAsync(ce.Type);
};

// All handlers execute in parallel
```

## Testing

The generated test project includes examples using Apache Artemis:

```csharp
[Fact]
public async Task TestConsumeMessage()
{
    // Arrange
    using var artemis = new ArtemisContainer();
    await artemis.StartAsync();
    
    var consumer = {{ class_name }}.CreateBuilder()
        .WithEndpoint(artemis.GetBrokerAddress())
        .WithCredential(new PlainEndpointCredential("guest", "guest"))
        .WithNode("/queue/test")
        .Build();
    
    var receivedEvent = new TaskCompletionSource<bool>();
    consumer.{{ first_msg_name }}Async += async (ce, data) =>
    {
        receivedEvent.SetResult(true);
    };
    
    await consumer.StartAsync();
    
    // Act: Send test message
    await SendTestMessageAsync(artemis);
    
    // Assert
    Assert.True(await receivedEvent.Task);
    
    await consumer.DisposeAsync();
}
```

## Dependencies

- `CloudNative.CloudEvents` >= 2.8.0
- `CloudNative.CloudEvents.Amqp` >= 2.8.0
- `AMQPNetLite` >= 2.4.11
- `Microsoft.Extensions.Logging` >= 9.0.0

## Generated Code

This code was auto-generated by [xRegistry CLI](https://github.com/xregistry/codegen).

**Message Group:** {{ groupname }}  
**Protocol:** AMQP 1.0  
**Envelope:** CloudEvents 1.0

## See Also

- [CloudEvents Specification](https://cloudevents.io/)
- [AMQP 1.0 Protocol](https://www.amqp.org/)
- [xRegistry Documentation](https://github.com/xregistry/codegen/blob/main/README.md)
{% endfor %}
