{%- import "util.jinja.include" as util -%}
{%- set messagegroups = root.messagegroups %}
{% for messagegroupid, messagegroup in messagegroups.items() -%}
{%- set groupname = messagegroupid | pascal -%}
{%- set class_name = (groupname | strip_namespace) + "Client" %}
# {{ project_name | pascal }} - MQTT 5.0 Client

Auto-generated MQTT client for {{ groupname }} message group.

## Overview

This client library makes it easy to publish and subscribe to MQTT messages. MQTT is perfect for IoT devices, mobile apps, and any scenario requiring lightweight, reliable messaging.

## What is MQTT?

MQTT (Message Queuing Telemetry Transport) is a lightweight pub/sub messaging protocol designed for:
- **IoT devices**: Sensors, smart home, industrial equipment
- **Mobile apps**: Push notifications, chat applications
- **Low bandwidth**: Optimized for unreliable networks
- **Battery-powered devices**: Minimal overhead

Version 5.0 adds enhanced features like message expiry, user properties, and better error reporting.

## Quick Start

### 1. Connect and Subscribe

```csharp
using {{ project_name | pascal }};
using MQTTnet.Client;

// Create client
var client = new {{ class_name }}(
    "mqtt://broker.example.com:1883",
    "my-client-id",
    loggerFactory);

// Register message 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) %}
client.{{ messagename }}Async += async (topic, qos, data) =>
{
    Console.WriteLine($"Received {{ messagename }} from {topic}");
    await ProcessAsync(data);
};
{%- endif %}

// Connect to broker
await client.ConnectAsync();

// Client is now receiving messages!
```

### 2. Publish Messages

```csharp
{%- if first_message %}
var data = new {{ message_body_type }}
{
    // Set your properties
};

await client.Publish{{ messagename }}Async(
    data,
    "devices/sensor-1/telemetry",  // Topic
    MqttQualityOfServiceLevel.AtLeastOnce);
{%- endif %}
```

## Message Handlers (Subscribe)

{% 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 }}

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

**Subscribe:**
```csharp
client.{{ messagename }}Async += async (topic, qos, data) =>
{
    _logger.LogInformation("Received from topic: {Topic}, QoS: {QoS}", topic, qos);
    
    if (data != null)
    {
        await ProcessDataAsync(data);
    }
};
```

**Publish:**
```csharp
var data = new {{ message_body_type }} { /* ... */ };

await client.Publish{{ messagename }}Async(
    data,
    "your/topic/here",
    MqttQualityOfServiceLevel.AtLeastOnce);
```

{% endfor %}

## MQTT Topics

Topics are hierarchical paths for routing messages.

### Topic Patterns

```
devices/sensor-1/temperature    ← Specific device
devices/+/temperature           ← All devices (+ = single-level wildcard)
devices/#                       ← All device messages (# = multi-level wildcard)
```

### Subscribe to Topics

```csharp
// Subscribe to specific topic
await client.SubscribeAsync("devices/sensor-1/data");

// Subscribe with wildcard
await client.SubscribeAsync("devices/+/temperature");

// Subscribe to everything under a path
await client.SubscribeAsync("devices/#");
```

### Publish to Topics

```csharp
await client.Publish{{ messagegroup.messages.keys() | first | strip_dots | pascal }}Async(
    data,
    "devices/sensor-1/telemetry",
    MqttQualityOfServiceLevel.AtLeastOnce);
```

## Quality of Service (QoS)

MQTT provides three QoS levels:

### QoS 0: At Most Once (Fire and Forget)

```csharp
await client.PublishAsync(data, topic, MqttQualityOfServiceLevel.AtMostOnce);
```

- **Fastest**: No acknowledgment
- **Use for**: Non-critical data, high-frequency updates
- **Example**: Temperature readings every second

### QoS 1: At Least Once (Acknowledged)

```csharp
await client.PublishAsync(data, topic, MqttQualityOfServiceLevel.AtLeastOnce);
```

- **Reliable**: Message delivered at least once
- **May duplicate**: Receiver might get message multiple times
- **Use for**: Important data that can handle duplicates
- **Example**: Sensor alerts, status updates

### QoS 2: Exactly Once (Guaranteed)

```csharp
await client.PublishAsync(data, topic, MqttQualityOfServiceLevel.ExactlyOnce);
```

- **Most reliable**: Exactly-once delivery guaranteed
- **Slowest**: Four-way handshake
- **Use for**: Critical data, financial transactions
- **Example**: Payment confirmations, command execution

## Authentication

### Username/Password

```csharp
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com", 1883)
    .WithCredentials("username", "password")
    .Build();

var client = new {{ class_name }}(options, loggerFactory);
```

### TLS/SSL

```csharp
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com", 8883)
    .WithCredentials("username", "password")
    .WithTls(new MqttClientOptionsBuilderTlsParameters
    {
        UseTls = true,
        SslProtocol = System.Security.Authentication.SslProtocols.Tls12
    })
    .Build();
```

### Client Certificates

```csharp
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com", 8883)
    .WithTls(new MqttClientOptionsBuilderTlsParameters
    {
        UseTls = true,
        Certificates = new[] { 
            new X509Certificate2("client-cert.pfx", "password") 
        }
    })
    .Build();
```

## Connection Management

### Clean Session vs Persistent Session

```csharp
// Clean session: Start fresh (default)
.WithCleanSession(true)

// Persistent session: Resume on reconnect
.WithCleanSession(false)
```

With persistent session:
- Broker remembers subscriptions
- Queues messages while disconnected (QoS 1 & 2)
- Client receives missed messages on reconnect

### Automatic Reconnection

```csharp
var options = new MqttClientOptionsBuilder()
    .WithAutomaticReconnect()
    .WithAutomaticReconnectDelay(TimeSpan.FromSeconds(5))
    .Build();
```

The client automatically reconnects if connection is lost.

### Connection Status

```csharp
client.ConnectedAsync += async e =>
{
    _logger.LogInformation("Connected to MQTT broker");
    // Re-subscribe if needed
    await client.SubscribeAsync("my/topic");
};

client.DisconnectedAsync += async e =>
{
    _logger.LogWarning("Disconnected from MQTT broker: {Reason}", e.Reason);
    // Will auto-reconnect if configured
};
```

## Last Will and Testament (LWT)

Configure a message to be sent if the client unexpectedly disconnects:

```csharp
var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.example.com")
    .WithWillTopic("devices/sensor-1/status")
    .WithWillPayload("offline")
    .WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
    .WithWillRetain(true)
    .Build();
```

If the client crashes or loses connection, the broker automatically publishes this "offline" message.

## Retained Messages

Retained messages are stored by the broker and delivered immediately to new subscribers:

```csharp
await client.PublishAsync(
    data,
    topic,
    qos,
    retain: true);  // This message is retained

// New subscribers immediately receive the last retained message
```

**Use cases:**
- Device status (online/offline)
- Configuration settings
- Current sensor values

## Message Properties (MQTT 5.0)

MQTT 5.0 adds user properties for custom metadata:

```csharp
var properties = new Dictionary<string, string>
{
    ["sensor-type"] = "temperature",
    ["unit"] = "celsius",
    ["location"] = "warehouse-A"
};

await client.PublishAsync(data, topic, qos, properties);
```

Access in handler:
```csharp
client.{{ messagegroup.messages.keys() | first | strip_dots | pascal }}Async += async (topic, qos, data, properties) =>
{
    var sensorType = properties?["sensor-type"];
    _logger.LogInformation("Sensor type: {Type}", sensorType);
};
```

## Error Handling

```csharp
client.ErrorAsync += async (exception) =>
{
    _logger.LogError(exception, "MQTT client error");
};

client.DeserializationErrorAsync += async (topic, exception) =>
{
    _logger.LogWarning(exception, "Failed to deserialize message from {Topic}", topic);
};
```

## Production-Ready Patterns

### Connection Management and Pooling

For production scenarios, implement a singleton pattern to reuse the MQTT client connection:

```csharp
public class {{ class_name }}Manager
{
    private static readonly Lazy<{{ class_name }}> _instance = 
        new Lazy<{{ class_name }}>(() => CreateClient());
    
    private static {{ class_name }} CreateClient()
    {
        var client = new {{ class_name }}(
            Environment.GetEnvironmentVariable("MQTT_BROKER_URL") ?? 
                "mqtt://broker.example.com:1883",
            Environment.GetEnvironmentVariable("MQTT_CLIENT_ID") ?? 
                $"client-{Guid.NewGuid()}",
            loggerFactory);
        
        // Register global handlers
        client.ConnectedAsync += async () => 
            _logger.LogInformation("MQTT client connected");
        
        client.DisconnectedAsync += async () => 
            _logger.LogWarning("MQTT client disconnected");
        
        return client;
    }
    
    public static {{ class_name }} Instance => _instance.Value;
}

// Usage
var client = {{ class_name }}Manager.Instance;
```

### Retry Logic with Exponential Backoff

Implement retry for publish operations:

```csharp
public async Task PublishWithRetryAsync<T>(
    T data,
    string topic,
    MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce,
    int maxRetries = 3)
{
    int retryCount = 0;
    TimeSpan delay = TimeSpan.FromMilliseconds(100);
    
    while (true)
    {
        try
        {
            await client.PublishAsync(data, topic, qos);
            return;
        }
        catch (Exception ex) when (retryCount < maxRetries)
        {
            retryCount++;
            _logger.LogWarning(ex, 
                "Publish failed (attempt {Attempt}/{Max}). Retrying after {Delay}ms", 
                retryCount, maxRetries, delay.TotalMilliseconds);
            
            await Task.Delay(delay);
            delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff
        }
    }
}
```

### Circuit Breaker Pattern

Prevent cascading failures when the broker is unavailable:

```csharp
public class MqttCircuitBreaker
{
    private int _failureCount = 0;
    private DateTime _lastFailureTime = DateTime.MinValue;
    private readonly int _failureThreshold = 5;
    private readonly TimeSpan _timeout = TimeSpan.FromSeconds(60);
    
    public enum CircuitState { Closed, Open, HalfOpen }
    
    public CircuitState State
    {
        get
        {
            if (_failureCount >= _failureThreshold)
            {
                if (DateTime.UtcNow - _lastFailureTime > _timeout)
                    return CircuitState.HalfOpen;
                return CircuitState.Open;
            }
            return CircuitState.Closed;
        }
    }
    
    public void RecordSuccess()
    {
        _failureCount = 0;
    }
    
    public void RecordFailure()
    {
        _failureCount++;
        _lastFailureTime = DateTime.UtcNow;
    }
    
    public async Task<bool> ExecuteAsync(Func<Task> operation)
    {
        if (State == CircuitState.Open)
        {
            _logger.LogWarning("Circuit breaker is OPEN. Operation not executed");
            return false;
        }
        
        try
        {
            await operation();
            RecordSuccess();
            return true;
        }
        catch (Exception ex)
        {
            RecordFailure();
            _logger.LogError(ex, "Operation failed. Circuit breaker failure count: {Count}", 
                _failureCount);
            throw;
        }
    }
}

// Usage
var circuitBreaker = new MqttCircuitBreaker();
await circuitBreaker.ExecuteAsync(async () => 
    await client.Publish{{ messagegroup.messages.keys() | first | strip_dots | pascal }}Async(data, topic));
```

### Rate Limiting

Control publish rate to prevent broker overload:

```csharp
public class RateLimitedPublisher
{
    private readonly SemaphoreSlim _semaphore;
    private readonly {{ class_name }} _client;
    
    public RateLimitedPublisher({{ class_name }} client, int maxConcurrent = 10)
    {
        _client = client;
        _semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
    }
    
    public async Task PublishAsync<T>(T data, string topic, 
        MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce)
    {
        await _semaphore.WaitAsync();
        try
        {
            await _client.PublishAsync(data, topic, qos);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

// Usage
var publisher = new RateLimitedPublisher(client, maxConcurrent: 5);
await publisher.PublishAsync(data, topic);
```

### Observability with Distributed Tracing

Integrate OpenTelemetry for distributed tracing:

```csharp
using System.Diagnostics;
using OpenTelemetry;
using OpenTelemetry.Trace;

// Configure tracing
var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource("{{ project_name | pascal }}.Mqtt")
    .AddConsoleExporter()
    .Build();

public class TracedMqttPublisher
{
    private static readonly ActivitySource _activitySource = 
        new ActivitySource("{{ project_name | pascal }}.Mqtt");
    
    public async Task PublishWithTracingAsync<T>(
        T data, 
        string topic,
        MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce)
    {
        using var activity = _activitySource.StartActivity("mqtt.publish", ActivityKind.Producer);
        
        activity?.SetTag("messaging.system", "mqtt");
        activity?.SetTag("messaging.destination", topic);
        activity?.SetTag("messaging.protocol", "mqtt");
        activity?.SetTag("messaging.protocol_version", "5.0");
        
        try
        {
            // Add trace context to user properties
            var properties = new Dictionary<string, string>();
            if (activity != null)
            {
                properties["traceparent"] = activity.Id ?? "";
                properties["tracestate"] = activity.TraceStateString ?? "";
            }
            
            await client.PublishAsync(data, topic, qos, properties);
            
            activity?.SetTag("messaging.message_id", Activity.Current?.Id);
            activity?.SetStatus(ActivityStatusCode.Ok);
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}
```

### Metrics Collection

Track MQTT operations with counters and gauges:

```csharp
using System.Diagnostics.Metrics;

public class MqttMetrics
{
    private static readonly Meter _meter = new Meter("{{ project_name | pascal }}.Mqtt");
    
    private static readonly Counter<long> _publishCounter = 
        _meter.CreateCounter<long>("mqtt.publish.count", "messages", 
            "Number of MQTT messages published");
    
    private static readonly Counter<long> _publishErrorCounter = 
        _meter.CreateCounter<long>("mqtt.publish.errors", "errors", 
            "Number of MQTT publish errors");
    
    private static readonly Histogram<double> _publishDuration = 
        _meter.CreateHistogram<double>("mqtt.publish.duration", "ms", 
            "Duration of MQTT publish operations");
    
    public async Task PublishWithMetricsAsync<T>(
        T data, 
        string topic,
        MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            await client.PublishAsync(data, topic, qos);
            
            _publishCounter.Add(1, new KeyValuePair<string, object?>("topic", topic));
            _publishDuration.Record(sw.Elapsed.TotalMilliseconds, 
                new KeyValuePair<string, object?>("topic", topic));
        }
        catch (Exception ex)
        {
            _publishErrorCounter.Add(1, 
                new KeyValuePair<string, object?>("topic", topic),
                new KeyValuePair<string, object?>("error", ex.GetType().Name));
            throw;
        }
    }
}
```

### Batch Publishing

Publish multiple messages efficiently:

```csharp
public async Task PublishBatchAsync<T>(
    IEnumerable<(T data, string topic)> messages,
    MqttQualityOfServiceLevel qos = MqttQualityOfServiceLevel.AtLeastOnce)
{
    var tasks = messages.Select(async msg => 
    {
        try
        {
            await client.PublishAsync(msg.data, msg.topic, qos);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to publish to {Topic}", msg.topic);
            return false;
        }
    });
    
    var results = await Task.WhenAll(tasks);
    var successCount = results.Count(r => r);
    _logger.LogInformation("Published {Success}/{Total} messages", 
        successCount, results.Length);
}

// Usage
var messages = new[]
{
    (sensorData1, "sensors/temp/room1"),
    (sensorData2, "sensors/temp/room2"),
    (sensorData3, "sensors/temp/room3")
};
await PublishBatchAsync(messages);
```

## Testing

```bash
dotnet test
```

Use a public test broker for development:
- `test.mosquitto.org` (port 1883)
- `broker.hivemq.com` (port 1883)

```csharp
var client = new {{ class_name }}(
    "mqtt://test.mosquitto.org:1883",
    "test-client",
    loggerFactory);
```

## Performance Tips

1. **Use QoS 0 for high-frequency data**: Fastest, no acknowledgment overhead
2. **Batch messages**: Send multiple data points in one message
3. **Use persistent sessions carefully**: Can accumulate many queued messages
4. **Keep-alive interval**: Balance between responsiveness and network traffic
5. **Message size**: Keep under 256MB (MQTT limit), ideally much smaller

## Configuration Example

```json
{
  "Mqtt": {
    "BrokerUrl": "mqtt://broker.example.com:1883",
    "ClientId": "my-app-instance-1",
    "Username": "user",
    "Password": "pass",
    "CleanSession": false,
    "KeepAlivePeriod": 60
  }
}
```

## Troubleshooting

**Can't connect**: Check broker URL, port, and firewall rules

**Not receiving messages**: Verify subscription topics and wildcards

**Duplicate messages**: Expected with QoS 1; use QoS 2 or make handlers idempotent

**Connection drops**: Check keep-alive settings and network stability

**Retained messages piling up**: Clear with empty retained message: `Publish("", topic, retain: true)`

## Learn More

- [MQTT 5.0 Specification](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html)
- [MQTTnet Documentation](https://github.com/dotnet/MQTTnet)
- [CloudEvents for MQTT](https://github.com/cloudevents/spec/blob/v1.0/mqtt-protocol-binding.md)
- [xRegistry CLI Documentation](https://github.com/xregistry/codegen)

## Dependencies

- `MQTTnet` - MQTT client library
- `CloudNative.CloudEvents` - CloudEvents support
- `Microsoft.Extensions.Logging` - Logging

## Generated Code

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

**Message Group:** {{ groupname }}  
**Protocol:** MQTT 5.0  
**Envelope:** CloudEvents 1.0
{% endfor %}
