using Marathon.Application.UseCases;
using Marathon.Domain.AnomalyDetection;
using DomainEventId = Marathon.Domain.ValueObjects.EventId;
namespace Marathon.UI.Services;
///
/// Page-facing implementation of . Runs
/// the application use case and reshapes its output for the page — event title
/// strings and severity buckets are computed once from the report's payload, so
/// the service performs no repository I/O of its own.
///
public sealed class AnomalyInsightsService : IAnomalyInsightsService
{
private readonly EvaluateAnomalyOutcomesUseCase _useCase;
public AnomalyInsightsService(EvaluateAnomalyOutcomesUseCase useCase)
{
_useCase = useCase ?? throw new ArgumentNullException(nameof(useCase));
}
public async Task GetReportAsync(CancellationToken ct)
{
var report = await _useCase.ExecuteAsync(ct).ConfigureAwait(false);
var resolvedRows = report.Resolved
.Select(r => ToRow(r, report.EventTitles))
.ToList();
var unresolvedRows = report.Unresolved
.Select(r => ToRow(r, report.EventTitles))
.ToList();
return new AnomalyInsightsVm(
TotalAnomalies: report.TotalAnomalies,
ResolvedCount: report.ResolvedCount,
UnresolvedCount: report.UnresolvedCount,
HitCount: report.HitCount,
MissCount: report.MissCount,
HitRate: report.HitRate,
BySeverity: report.BySeverity,
BySport: report.BySport,
ByScoreBin: report.ByScoreBin,
ByKind: report.ByKind,
Resolved: resolvedRows,
Unresolved: unresolvedRows);
}
private static ResolvedAnomalyRow ToRow(
ResolvedAnomaly src,
IReadOnlyDictionary titles) =>
new(
AnomalyId: src.AnomalyId,
EventId: src.EventId,
EventTitle: titles.TryGetValue(src.EventId, out var t) ? t : src.EventId.Value,
DetectedAt: src.DetectedAt,
Score: src.Score,
Severity: AnomalySeverityRules.FromScore(src.Score),
Sport: src.Sport,
PreFlipFavourite: src.PreFlipFavourite,
PostFlipFavourite: src.PostFlipFavourite,
ActualWinner: src.ActualWinner,
Outcome: src.Outcome);
}