using MeterVault.Core.Domain;
using MeterVault.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace MeterVault.Infrastructure.Dashboard;
///
/// Read model for the meter-detail view (SDD §8.6). Bounds the raw-reading and consumption pulls
/// (this is the one place the UI touches raw rows) and gathers source status, the applicable tariff
/// timeline and lifecycle events. DbContext factory keeps it Blazor-circuit safe.
///
public sealed class MeterDetailService(IDbContextFactory contextFactory)
{
private const int MaxRows = 200;
private readonly IDbContextFactory _contextFactory = contextFactory;
public async Task GetAsync(int meterId, CancellationToken cancellationToken = default)
{
await using var db = await _contextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var meter = await db.Meters.AsNoTracking()
.Include(m => m.EnergyType)
.Include(m => m.Sources)
.FirstOrDefaultAsync(m => m.Id == meterId, cancellationToken).ConfigureAwait(false);
if (meter is null)
{
return null;
}
var readingCount = await db.Readings.AsNoTracking().CountAsync(r => r.MeterId == meterId, cancellationToken).ConfigureAwait(false);
var consumptionCount = await db.Consumption.AsNoTracking().CountAsync(c => c.MeterId == meterId, cancellationToken).ConfigureAwait(false);
var first = await db.Readings.AsNoTracking().Where(r => r.MeterId == meterId)
.OrderBy(r => r.Time).Select(r => new { r.Time, r.Value })
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
var last = await db.Readings.AsNoTracking().Where(r => r.MeterId == meterId)
.OrderByDescending(r => r.Time).Select(r => new { r.Time, r.Value })
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
var totalConsumption = await db.Consumption.AsNoTracking()
.Where(c => c.MeterId == meterId && c.Kind == ConsumptionKind.Consumption)
.SumAsync(c => (double?)c.Amount, cancellationToken).ConfigureAwait(false) ?? 0;
var totalGeneration = await db.Consumption.AsNoTracking()
.Where(c => c.MeterId == meterId && c.Kind == ConsumptionKind.Generation)
.SumAsync(c => (double?)c.Amount, cancellationToken).ConfigureAwait(false) ?? 0;
var recentReadings = await db.Readings.AsNoTracking()
.Where(r => r.MeterId == meterId)
.OrderByDescending(r => r.Time).Take(MaxRows)
.Select(r => new ReadingRow(r.Time, r.Value, r.Quality, r.Flags))
.ToListAsync(cancellationToken).ConfigureAwait(false);
var recentConsumption = await db.Consumption.AsNoTracking()
.Where(c => c.MeterId == meterId)
.OrderByDescending(c => c.Time).Take(MaxRows)
.Select(c => new ConsumptionDetailRow(c.Time, c.Amount, c.Kind, c.Quality))
.ToListAsync(cancellationToken).ConfigureAwait(false);
var events = await db.MeterEvents.AsNoTracking()
.Where(e => e.MeterId == meterId)
.OrderByDescending(e => e.Time)
.Select(e => new EventRow(e.Time, e.EventType, e.Amount, e.PrevValue, e.NewValue, e.Unit, e.Notes))
.ToListAsync(cancellationToken).ConfigureAwait(false);
var energyTypeId = meter.EnergyTypeId;
var tariffs = await db.Tariffs.AsNoTracking()
.Where(t => t.ScopeType == TariffScope.Global
|| (t.ScopeType == TariffScope.EnergyType && t.ScopeId == energyTypeId)
|| (t.ScopeType == TariffScope.Meter && t.ScopeId == meterId))
.OrderBy(t => t.Component).ThenBy(t => t.ValidFrom)
.Select(t => new TariffRow(t.ScopeType, t.ScopeId, t.Component, t.Value, t.Unit, t.ValidFrom, t.ValidTo))
.ToListAsync(cancellationToken).ConfigureAwait(false);
return new MeterDetailView(
meter.Id, meter.Name, meter.EnergyType?.DisplayName ?? "—", meter.Mode, meter.Unit,
meter.Location, meter.SerialNumber, meter.Manufacturer, meter.Model, meter.InitialBaseline, meter.IsActive,
readingCount, consumptionCount,
first?.Time, last?.Time, first?.Value, last?.Value,
totalConsumption, totalGeneration,
recentReadings, recentConsumption, events, tariffs, meter.Sources.Count);
}
}