using System.Text.Json;
using Server.WatchBody.Core.Dal.Ref.Repository;
using Server.WatchBody.Core.Dal.Src.Domain;
using Server.WatchBody.Core.Dal.Src.Repository;
using Server.WatchBody.Core.Programs;
namespace Server.WatchBody.Import;
/// Нарушение формата пакета program-import.json (spec 03 §2).
public class ImportPackageException(string message) : Exception(message);
///
/// Импорт пакета программы (spec 03 m06, формат program-import-format.md):
/// program по external_id, версия через ProgramStore (повтор пакета —
/// новая версия max+1), отсутствующие упражнения создаются. 🛡
/// Существующие версии не переписываются; статус draft.
///
public class ProgramPackageImporter(
IProgramRepository programs,
IExerciseRepository exercises,
IExerciseGroupRepository groups,
ProgramStore store)
{
public Guid Import(int athleteId, string packageJson)
{
JsonDocument doc;
try
{
doc = JsonDocument.Parse(packageJson);
}
catch (JsonException e)
{
throw new ImportPackageException($"пакет не JSON: {e.Message}");
}
using (doc)
{
var root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Object ||
!root.TryGetProperty("weeks", out var weeks) ||
weeks.ValueKind != JsonValueKind.Array || weeks.GetArrayLength() == 0)
throw new ImportPackageException("пакет без weeks — это не программа");
if (!root.TryGetProperty("program_id", out var programIdEl) ||
programIdEl.GetString() is not { Length: > 0 } externalId)
throw new ImportPackageException("пакет без program_id");
var author = root.TryGetProperty("author", out var authorEl)
? authorEl.GetString() : null;
if (author is not ("user" or "admin"))
throw new ImportPackageException($"author '{author}' не допускается (user|admin)");
// программа по external_id (З-35) или создание
var program = programs.Select(athleteId).FirstOrDefault(p => p.ExternalId == externalId)
?? CreateProgram(athleteId, externalId);
// отсутствующие упражнения библиотеки (format разд. 2)
var groupByFocus = GroupByFocus();
EnsureExercises(athleteId, weeks, groupByFocus);
var rationale = root.TryGetProperty("rationale", out var rationaleEl) &&
rationaleEl.ValueKind == JsonValueKind.String
? rationaleEl.GetString() ?? string.Empty
: string.Empty;
var version = store.CreateDraft(athleteId, root.GetRawText(), author, rationale);
return version.Id;
}
}
private Program CreateProgram(int athleteId, string externalId)
{
var id = programs.Insert(new Program
{
AthleteId = athleteId,
Title = externalId,
ExternalId = externalId,
});
return new Program { Id = id, AthleteId = athleteId, Title = externalId, ExternalId = externalId };
}
private Dictionary GroupByFocus() =>
groups.Select().ToDictionary(g => g.Code, g => g.Id, StringComparer.Ordinal);
private void EnsureExercises(int athleteId, JsonElement weeks, Dictionary groupIds)
{
var known = exercises.Select(new ExerciseFilter { AthleteId = athleteId })
.Select(e => e.Name).ToHashSet(StringComparer.Ordinal);
foreach (var session in weeks.EnumerateArray()
.SelectMany(w => w.TryGetProperty("sessions", out var s)
? s.EnumerateArray() : Enumerable.Empty()))
{
var focus = session.TryGetProperty("focus", out var f) ? f.GetString() ?? "" : "";
var groupId = FocusGroup(focus, groupIds);
if (!session.TryGetProperty("exercises", out var exerciseNodes)) continue;
foreach (var exercise in exerciseNodes.EnumerateArray())
{
if (!exercise.TryGetProperty("exercise_id", out var idEl) ||
idEl.GetString() is not { Length: > 0 } name || known.Contains(name)) continue;
exercises.Insert(new Exercise
{
Id = Guid.NewGuid(),
AthleteId = athleteId,
Name = name,
GroupId = groupId,
IsBase = true,
IsActive = true,
ReplacementIds = [],
});
known.Add(name);
}
}
}
private static int FocusGroup(string focus, Dictionary groupIds)
{
foreach (var (code, id) in groupIds)
if (focus.Contains(code, StringComparison.OrdinalIgnoreCase))
return id;
return groupIds.GetValueOrDefault("core", 4);
}
}