using MeterVault.Core.Domain;
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MeterVault.Infrastructure.Ingestion;
///
/// Routes an incoming MQTT message to every enabled MQTT/Tasmota source that is bound to the
/// delivering broker and whose topic filter covers it, extracts the value (and payload
/// timestamp), and ingests it (SDD ยง6.1). Decoupled from the broker client so it can be exercised
/// directly against the database in tests.
///
///
/// The endpoint predicate is load-bearing, not defensive: topic filters routinely overlap between
/// brokers (every Tasmota install publishes tele/+/SENSOR), so matching on topic alone would
/// let a message from one broker be ingested by a source bound to another.
///
public sealed class MqttMessageRouter(
MeterVaultDbContext db, IngestionService ingestion, ILogger logger)
{
private readonly MeterVaultDbContext _db = db;
private readonly IngestionService _ingestion = ingestion;
private readonly ILogger _logger = logger;
public async Task RouteAsync(
int endpointId, string topic, string payload, CancellationToken cancellationToken = default)
{
var sources = await _db.MeterSources
.Where(s => s.IsEnabled
&& (s.SourceType == SourceType.Mqtt || s.SourceType == SourceType.Tasmota)
&& s.EndpointId == endpointId)
.ToListAsync(cancellationToken).ConfigureAwait(false);
var routed = 0;
foreach (var source in sources)
{
var config = SourceConfig.Parse(source.Config);
if (!MqttTopicMatcher.Matches(config.Topic, topic))
{
continue;
}
if (!PayloadExtractor.TryExtractValue(payload, config.Path, out var value))
{
_logger.LogDebug("Source {SourceId}: no value at path '{Path}' in topic {Topic}",
source.Id, config.Path, topic);
continue;
}
// Prefer the payload's own time; Tasmota SENSOR messages carry "Time"; else receive time.
var timePath = config.TimePath ?? (source.SourceType == SourceType.Tasmota ? "Time" : null);
var time = PayloadExtractor.TryExtractTime(payload, timePath, out var payloadTime)
? payloadTime
: DateTimeOffset.UtcNow;
await _ingestion.IngestAsync(source.Id, time, value, cancellationToken).ConfigureAwait(false);
routed++;
}
return routed;
}
}