using Server.WatchBody.Core.Dal.Src.Domain;
using Server.WatchBody.Core.Dal.Src.Repository;
namespace Server.WatchBody.Core.Programs;
/// Нарушение правил жизненного цикла программы (спецификации m03).
public class ProgramStoreException(string message) : Exception(message);
///
/// Единая точка работы с программами атлета (m03): версионный JSON,
/// жизненный цикл draft → active → archived, «active — ровно одна».
/// 🛡 Версия после создания неизменяема (меняется только статус);
/// переходы только вперёд; автоматической смены активной версии нет.
///
public class ProgramStore(IProgramRepository programs, IProgramVersionRepository versions)
{
public static readonly IReadOnlyList AllowedAuthors = ["user", "admin", "rules"];
/// Программа атлета (создаёт при отсутствии; одна на атлета).
public Program EnsureProgram(int athleteId, string title)
{
var existing = programs.Select(athleteId).FirstOrDefault();
if (existing != null) return existing;
var id = programs.Insert(new Program { AthleteId = athleteId, Title = title });
return new Program { Id = id, AthleteId = athleteId, Title = title };
}
///
/// Новая draft-версия: based_on_version = актуальная максимальная
/// (или явная, равная ей); version считает БД (max+1).
///
public ProgramVersion CreateDraft(
int athleteId, string body, string author, string rationale, int? basedOnVersion = null)
{
if (string.IsNullOrWhiteSpace(body))
throw new ProgramStoreException("body версии обязателен");
if (!AllowedAuthors.Contains(author))
throw new ProgramStoreException($"author '{author}' не допускается в v1 ({string.Join("/", AllowedAuthors)}; llm — v2)");
var program = EnsureProgram(athleteId, "Программа");
var latest = LatestVersion(athleteId, program.Id);
if (basedOnVersion.HasValue && basedOnVersion != latest)
throw new ProgramStoreException(
$"based_on_version {basedOnVersion} не равна актуальной версии {latest}");
var effectiveBasedOn = basedOnVersion ?? (latest == 0 ? null : latest);
var id = Guid.NewGuid();
versions.Insert(athleteId, new ProgramVersion
{
Id = id,
ProgramId = program.Id,
Version = 0, // номер считает БД (max+1)
BasedOnVersion = effectiveBasedOn,
Status = "draft", // выставит БД
Author = author,
Body = body,
Rationale = rationale ?? string.Empty,
});
return versions.Get(athleteId, id)!;
}
/// draft → active; прежняя активная → archived (гарантия БД).
public void Activate(int athleteId, Guid versionId)
{
var version = GetOrThrow(athleteId, versionId);
if (version.Status != "draft")
throw new ProgramStoreException(
$"активировать можно только draft-версию (текущий статус '{version.Status}')");
versions.UpdateStatus(athleteId, versionId, "active");
}
/// active → archived.
public void Archive(int athleteId, Guid versionId)
{
var version = GetOrThrow(athleteId, versionId);
if (version.Status != "active")
throw new ProgramStoreException(
$"архивировать можно только active-версию (текущий статус '{version.Status}')");
versions.UpdateStatus(athleteId, versionId, "archived");
}
/// Активная версия с body (или null).
public ProgramVersion? GetActive(int athleteId)
{
var program = programs.Select(athleteId).FirstOrDefault();
if (program == null) return null;
var meta = versions.Select(new ProgramVersionFilter
{
AthleteId = athleteId,
ProgramId = program.Id,
Status = "active",
}).FirstOrDefault();
return meta == null ? null : versions.Get(athleteId, meta.Id);
}
/// Версии программы атлета (без body), по возрастанию номера.
public IReadOnlyList ListVersions(int athleteId)
{
var program = programs.Select(athleteId).FirstOrDefault();
if (program == null) return [];
var list = versions.Select(new ProgramVersionFilter
{
AthleteId = athleteId,
ProgramId = program.Id,
}).OrderBy(v => v.Version).ToList();
foreach (var v in list) v.Body = string.Empty; // body select не возвращает
return list;
}
/// Версия с body; чужая/несуществующая → null.
public ProgramVersion? GetVersion(int athleteId, Guid versionId) =>
versions.Get(athleteId, versionId);
private int LatestVersion(int athleteId, int programId) =>
versions.Select(new ProgramVersionFilter
{
AthleteId = athleteId,
ProgramId = programId,
}).Select(v => v.Version).DefaultIfEmpty(0).Max();
private ProgramVersion GetOrThrow(int athleteId, Guid versionId)
{
var version = versions.Get(athleteId, versionId);
if (version == null)
throw new ProgramStoreException(
$"версия {versionId} не найдена у атлета {athleteId} (изоляция 🛡)");
return version;
}
}