using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Forum.Posts.ValueObject;
using Domain.Entities.Members;
namespace Domain.Entities.Forum.Posts;
///
/// 예측글 (d2 §③) — 종목 방향/기간 예측을 작성 시점에 확정 기록하고, 채점 배치(M4)가 상태를 확정한다.
/// 작성 시 BasePrice(작성 시점 최신 T+1 종가)와 DueDate(영업일 + HorizonDays)를 스냅샷으로 고정한다.
/// 예측글이 삭제(IsDeleted)돼도 Prediction 은 유지·채점된다(삭제 회피 어뷰징 차단).
/// Stock FK 는 걸지 않는다(D1 소유). PostID 는 1:1(UQ).
/// M2 는 Pending 생성만 — SettledPrice/SettledAt/상태 확정은 M4 PredictionSettlementService 소관.
///
public class PostPrediction
{
[ForeignKey(nameof(PostID))]
public virtual Post Post { get; private set; } = null!;
[ForeignKey(nameof(MemberID))]
public virtual Member Member { get; private set; } = null!;
[Key]
public int ID { get; private set; }
/// 예측이 속한 게시글 (1:1)
public int PostID { get; private set; }
/// 작성 회원
public int MemberID { get; private set; }
/// 예측 종목 단축코드 (char 6)
public string StockCode { get; private set; } = default!;
/// 예측 방향 (1=상승, 2=하락)
public PredictionDirection Direction { get; private set; }
/// 작성 시점 기준가 (최신 T+1 종가 스냅샷)
public decimal BasePrice { get; private set; }
/// 목표가 (선택) — 지정 시 도달 여부로 채점
public decimal? TargetPrice { get; private set; }
/// 예측 기간 (영업일 수: 5 또는 20)
public short HorizonDays { get; private set; }
/// 채점 예정일 (작성일 + HorizonDays 영업일)
public DateOnly DueDate { get; private set; }
/// 상태 (0=Pending/1=Hit/2=Miss/3=Void)
public PredictionStatus Status { get; private set; } = PredictionStatus.Pending;
/// 채점 시점 종가 (M4 확정)
public decimal? SettledPrice { get; private set; }
/// 채점 일시 (M4 확정)
public DateTime? SettledAt { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
private PostPrediction() { }
/// Pending 예측 생성 (d2 §③ M2). 채점은 M4.
public static PostPrediction Create(
int postID,
int memberID,
string stockCode,
PredictionDirection direction,
decimal basePrice,
short horizonDays,
DateOnly dueDate,
decimal? targetPrice = null)
{
if (postID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(postID));
}
if (memberID <= 0)
{
throw new ArgumentOutOfRangeException(nameof(memberID));
}
if (stockCode is not { Length: 6 } || stockCode.Any(c => !char.IsAsciiDigit(c)))
{
throw new ArgumentException("stockCode must be 6 digits", nameof(stockCode));
}
if (!Enum.IsDefined(direction))
{
throw new ArgumentOutOfRangeException(nameof(direction));
}
if (basePrice <= 0)
{
throw new ArgumentOutOfRangeException(nameof(basePrice));
}
if (horizonDays <= 0)
{
throw new ArgumentOutOfRangeException(nameof(horizonDays));
}
return new PostPrediction
{
PostID = postID,
MemberID = memberID,
StockCode = stockCode,
Direction = direction,
BasePrice = basePrice,
TargetPrice = targetPrice is > 0 ? targetPrice : null,
HorizonDays = horizonDays,
DueDate = dueDate,
Status = PredictionStatus.Pending
};
}
///
/// 채점 확정 (D2 M4 예측 채점+트랙레코드) — Pending 예측을 Hit/Miss/Void 로 확정하고 종가·채점일시를 기록.
/// Void 는 시세 결측·상폐·거래정지로 종가가 없을 수 있어 settledPrice 를 null 허용한다.
/// 이미 확정(비 Pending)된 예측은 멱등하게 무시한다 (배치 재실행 안전).
///
public void Settle(PredictionStatus status, decimal? settledPrice)
{
if (Status != PredictionStatus.Pending)
{
return;
}
if (status == PredictionStatus.Pending)
{
throw new ArgumentException("Settle status must be Hit/Miss/Void", nameof(status));
}
Status = status;
SettledPrice = settledPrice;
SettledAt = DateTime.UtcNow;
}
}