using Application.Abstractions.Data;
using Application.Abstractions.Notification;
using Application.Helpers;
using Domain.Entities.Forum.Posts.ValueObject;
using Domain.Entities.Notifications.ValueObject;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Infrastructure.StockData;
///
/// 예측 채점 배치 (D2 M4 예측 채점+트랙레코드, d2 §⑥) — 매 영업일 18:00 KST(D1 T+1 종가 적재 이후 여유).
/// DueDate 도래한 Pending 예측을 종가로 채점(PredictionSettlementEngine)하고, 영향 회원 트랙레코드를 재집계한 뒤
/// 작성자에게 결과 알림을 best-effort 로 보낸다.
///
/// BackgroundJobs:PredictionSettlement 플래그로 토글(기본 false), Web.Api 전용(중복 실행 방지).
/// 채점 로직은 순수 엔진에 위임 — 여기서는 스케줄/알림만 담당한다.
///
internal sealed class PredictionSettlementService(
IServiceScopeFactory scopeFactory,
ILogger logger
) : DailyScheduledService(logger)
{
/// KST 실행 시각 — D1 T+1 종가 적재(13시+) 이후 여유.
private static readonly TimeOnly RunTime = new(18, 0);
protected override string JobName => "PredictionSettlement";
protected override TimeOnly TargetTime => RunTime;
protected override async Task RunOnceAsync(DateOnly todayKst, CancellationToken ct)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var (result, settled) = await PredictionSettlementEngine.RunAsync(db, todayKst, ct);
Logger.LogInformation("[{Job}] 채점 완료 — asOf={AsOf}, considered={Considered}, hits={Hits}, misses={Misses}, voids={Voids}, members={Members}",
JobName, todayKst, result.Considered, result.Hits, result.Misses, result.Voids, result.MembersRecomputed);
if (settled.Count == 0)
{
return true;
}
// 작성자 알림 — best-effort (실패해도 채점은 이미 커밋됨). Void 는 알림 생략.
var notifier = scope.ServiceProvider.GetService();
if (notifier is not null)
{
foreach (var s in settled)
{
if (s.Status is not (PredictionStatus.Hit or PredictionStatus.Miss))
{
continue;
}
try
{
var hit = s.Status == PredictionStatus.Hit;
await notifier.SendAsync(
s.MemberID,
NotificationType.SystemAnnounce,
hit ? "예측 적중" : "예측 종료",
hit ? $"{s.StockCode} 예측이 적중했습니다." : $"{s.StockCode} 예측이 빗나갔습니다.",
actionUrl: $"/posts/{s.PostID}",
relatedType: nameof(Domain.Entities.Forum.Posts.PostPrediction),
relatedID: s.PredictionID,
ct: ct);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "[{Job}] 작성자 알림 실패 — predictionID={PredictionID}", JobName, s.PredictionID);
}
}
}
return true;
}
}