using System.Globalization;
using SVSim.Database.Models.Config;
namespace SVSim.EmulatedEntrypoint.Models.Dtos.ArenaColosseum;
///
/// Single source of truth for projecting +
/// onto the wire block.
/// Used by both /arena_colosseum/{top,get_fee_info} and
/// /mypage/index data.colosseum_info — the home-screen tab and the lobby reads must
/// agree on the season state, so they go through one builder.
///
/// When IsColosseumPeriod = false a minimal "no event" payload is emitted; the client
/// gates every other field on that flag (Wizard/ColosseumEntryInfoTask.cs:100).
///
///
/// Round-period derivation: are walked against
/// . If a round's window covers , we emit
/// is_round_period = true + that round's window as start_time/end_time +
/// now_round. Otherwise we emit is_round_period = false + the NEXT round's
/// start (or the event start if no rounds are configured) so the client's
/// "Round X opens in HH:MM:SS" countdown points somewhere sensible. Client handles both
/// branches at ColosseumEntryInfoTask.cs:111-126.
///
///
public static class ColosseumLobbyInfoBuilder
{
private const string WireDateFormat = "yyyy-MM-dd HH:mm:ss";
public static ColosseumLobbyInfo Build(
ColosseumSeasonConfig season,
ColosseumRoundsConfig rounds,
DateTime nowUtc)
{
if (!season.IsColosseumPeriod)
{
return new ColosseumLobbyInfo { IsColosseumPeriod = false };
}
var (isRoundPeriod, nowRoundId, nextRoundId, windowStart, windowEnd) =
ResolveRoundWindow(season, rounds, nowUtc);
return new ColosseumLobbyInfo
{
IsColosseumPeriod = true,
// Wire colosseum_id — surface SeasonId so client can disambiguate cups.
// Falls back to absent when no season identity is configured.
ColosseumId = season.SeasonId == 0 ? null : season.SeasonId,
DeckFormat = (int)season.DeckFormat,
IsNormalTwoPick = season.IsNormalTwoPick ? "1" : "0",
ColosseumName = season.ColosseumName,
IsRoundPeriod = isRoundPeriod,
IsSpecialMode = season.IsSpecialMode,
CardPoolName = string.IsNullOrEmpty(season.CardPoolName) ? null : season.CardPoolName,
NowRound = isRoundPeriod ? nowRoundId : null,
NextRound = isRoundPeriod ? null : nextRoundId,
StartTime = FormatTime(windowStart),
EndTime = FormatTime(windowEnd),
// Tutorial tip toggle — wire ints 0/1 per ColosseumEntryInfoTask.cs:129.
IsDisplayTips = season.IsDisplayTips ? 1 : 0,
TipsId = season.TipsId == 0 ? null : season.TipsId,
IsAllCardEnabled = season.IsAllCardEnabled ? 1 : 0,
SalesPeriodInfo = new ColosseumSalesPeriodInfo
{
SalesPeriodTime = FormatTime(season.SalesPeriodEnd),
},
StrategyPickNum = season.StrategyPickNum > 0 ? season.StrategyPickNum : null,
};
}
///
/// Walks (ordered by RoundId) against
/// and returns the round window to emit on the wire.
///
/// - Inside a round → (true, round.RoundId, null, round.StartTime, round.EndTime)
/// - Before any round → (false, null, firstRound.RoundId, firstRound.StartTime, firstRound.EndTime)
/// - Between two rounds → (false, null, nextRound.RoundId, nextRound.StartTime, nextRound.EndTime)
/// - After last round → (false, null, lastRound.RoundId, lastRound.StartTime, lastRound.EndTime)
/// — client renders "ended" UI; keeps the field non-null so the parser doesn't NRE.
/// - No rounds configured → fall back to the event-level start/end window. Keeps the
/// wire well-formed even when rounds haven't been seeded yet.
///
///
private static (bool IsRoundPeriod, int? NowRoundId, int? NextRoundId, DateTime Start, DateTime End)
ResolveRoundWindow(ColosseumSeasonConfig season, ColosseumRoundsConfig rounds, DateTime nowUtc)
{
if (rounds.Rounds.Count == 0)
{
return (IsRoundPeriod: false, NowRoundId: null, NextRoundId: null,
Start: season.EventStartTime, End: season.EventEndTime);
}
var ordered = rounds.Rounds.OrderBy(r => r.RoundId).ToList();
var current = ordered.FirstOrDefault(r => nowUtc >= r.StartTime && nowUtc <= r.EndTime);
if (current is not null)
{
return (IsRoundPeriod: true, NowRoundId: current.RoundId, NextRoundId: null,
Start: current.StartTime, End: current.EndTime);
}
var upcoming = ordered.FirstOrDefault(r => r.StartTime > nowUtc);
if (upcoming is not null)
{
return (IsRoundPeriod: false, NowRoundId: null, NextRoundId: upcoming.RoundId,
Start: upcoming.StartTime, End: upcoming.EndTime);
}
// Past last round — anchor to the last round's window so the wire stays well-formed.
var last = ordered[^1];
return (IsRoundPeriod: false, NowRoundId: null, NextRoundId: last.RoundId,
Start: last.StartTime, End: last.EndTime);
}
private static string FormatTime(DateTime t) =>
t == default ? "" : t.ToString(WireDateFormat, CultureInfo.InvariantCulture);
}