using System.Text.Json;
namespace Server.WatchBody.Web.Services;
///
/// Таймлайн тренировки из facts_json (spec 03 §2 m08): по Lap —
/// длительность/avg/peak пульс/отдых; пульсограмма records с
/// прореживанием до 600 точек.
///
public static class FactsTimeline
{
public static (List> Timeline, List> Curve) Parse(
string? factsJson, int sessionDurationS)
{
var timeline = new List>();
var curve = new List>();
if (string.IsNullOrWhiteSpace(factsJson)) return (timeline, curve);
try
{
using var doc = JsonDocument.Parse(factsJson);
var root = doc.RootElement;
var records = new List<(int t, int hr)>();
if (root.TryGetProperty("records", out var recordNodes))
foreach (var r in recordNodes.EnumerateArray())
if (r.TryGetProperty("t_offset_s", out var t) && r.TryGetProperty("hr", out var hr))
records.Add((t.GetInt32(), hr.GetInt32()));
// пульсограмма: не более 600 точек
if (records.Count > 0)
{
var step = Math.Max(1, (int)Math.Ceiling(records.Count / 600.0));
for (var i = 0; i < records.Count; i += step)
curve.Add(new Dictionary { ["t"] = records[i].t, ["hr"] = records[i].hr });
}
if (!root.TryGetProperty("laps", out var lapNodes)) return (timeline, curve);
var laps = new List<(DateTime start, int durationS)>();
foreach (var l in lapNodes.EnumerateArray())
if (l.TryGetProperty("start", out var s) && l.TryGetProperty("duration_s", out var d) &&
DateTime.TryParse(s.GetString(), null, System.Globalization.DateTimeStyles.AssumeUniversal |
System.Globalization.DateTimeStyles.AdjustToUniversal, out var start))
laps.Add((start, d.GetInt32()));
var sessionStart = laps.Count > 0 ? laps.Min(l => l.start)
: records.Count > 0 ? DateTime.MinValue : DateTime.MinValue;
for (var i = 0; i < laps.Count; i++)
{
var offset = (int)(laps[i].start - sessionStart).TotalSeconds;
var end = offset + laps[i].durationS;
var nextStart = i + 1 < laps.Count ? (int)(laps[i + 1].start - sessionStart).TotalSeconds : end;
var inLap = records.Where(r => r.t >= offset && r.t <= end).Select(r => r.hr).ToList();
timeline.Add(new Dictionary
{
["index"] = i,
["startOffsetS"] = offset,
["durationS"] = laps[i].durationS,
["avgHr"] = inLap.Count > 0 ? (int)Math.Round(inLap.Average(), MidpointRounding.AwayFromZero) : null,
["peakHr"] = inLap.Count > 0 ? inLap.Max() : null,
["restAfterS"] = Math.Max(0, nextStart - end),
});
}
}
catch (JsonException)
{
// битые факты — пустой таймлайн
}
return (timeline, curve);
}
}