using Marathon.Application.Abstractions; using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Options; namespace Marathon.UI.Services; /// /// Repository-backed implementation of . /// Composes server-side counts (no full-table materialisation) with the top few /// anomaly signals and the worker-toggle state. Scoped — captures the per-circuit /// repository scope like the other browsing services. /// public sealed class DashboardSummaryService : IDashboardSummaryService { private const int LatestSignalCount = 5; private readonly IEventRepository _events; private readonly ISnapshotRepository _snapshots; private readonly IAnomalyRepository _anomalies; private readonly IAnomalyBrowsingService _anomalyBrowsing; private readonly IPaperTradingService _paperTrading; private readonly IOptionsMonitor _workers; public DashboardSummaryService( IEventRepository events, ISnapshotRepository snapshots, IAnomalyRepository anomalies, IAnomalyBrowsingService anomalyBrowsing, IPaperTradingService paperTrading, IOptionsMonitor workers) { _events = events ?? throw new ArgumentNullException(nameof(events)); _snapshots = snapshots ?? throw new ArgumentNullException(nameof(snapshots)); _anomalies = anomalies ?? throw new ArgumentNullException(nameof(anomalies)); _anomalyBrowsing = anomalyBrowsing ?? throw new ArgumentNullException(nameof(anomalyBrowsing)); _paperTrading = paperTrading ?? throw new ArgumentNullException(nameof(paperTrading)); _workers = workers ?? throw new ArgumentNullException(nameof(workers)); } public async Task GetAsync(CancellationToken ct) { var todayStart = new DateTimeOffset(MoscowTime.Now.Date, MoscowTime.Offset); var eventsTracked = await _events.CountAsync(ct).ConfigureAwait(false); var snapshotsToday = await _snapshots.CountSinceAsync(todayStart, ct).ConfigureAwait(false); // DateTimeOffset.MinValue lower bound counts every row (year dominates the // lexical comparison regardless of offset). var anomaliesTotal = await _anomalies.CountSinceAsync(DateTimeOffset.MinValue, ct).ConfigureAwait(false); var anomaliesToday = await _anomalies.CountSinceAsync(todayStart, ct).ConfigureAwait(false); var sports = await _events.ListDistinctSportCodesAsync(ct).ConfigureAwait(false); var latest = anomaliesTotal == 0 ? (IReadOnlyList)Array.Empty() : (await _anomalyBrowsing.ListAsync(new AnomalyFilter(), ct).ConfigureAwait(false)) .Take(LatestSignalCount) .ToList(); var w = _workers.CurrentValue; // Reuse the forward-test aggregation (settled-only P&L) for the headline tile. var paper = await _paperTrading.GetAsync(ct).ConfigureAwait(false); return new DashboardSummary( EventsTracked: eventsTracked, SnapshotsToday: snapshotsToday, AnomaliesTotal: anomaliesTotal, AnomaliesToday: anomaliesToday, SportsCovered: sports.Count, LatestSignals: latest, ScheduleStatus: StageStatus(w.UpcomingPollerEnabled, eventsTracked > 0), SnapshotStatus: StageStatus(w.LivePollerEnabled, snapshotsToday > 0), DetectorStatus: StageStatus(w.AnomalyDetectionEnabled, anomaliesTotal > 0), ExportStatus: eventsTracked > 0 ? "ok" : "idle", PaperOpenCount: paper.OpenCount, PaperSettledCount: paper.SettledCount, PaperNetProfit: paper.NetProfit, PaperRoiPercent: paper.RoiPercent); } // Maps a worker stage to the PipelineStep token: disabled → idle, enabled but // not yet producing → warn (waiting), enabled and producing → ok. private static string StageStatus(bool enabled, bool producing) => !enabled ? "idle" : producing ? "ok" : "warn"; }