using HSchool.People; using HSchool.Schedule; using HSchool.Server.Game; namespace HSchool.Server.Api; internal static class TimetableEndpoints { private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(5); public static void MapTimetableEndpoints(this IEndpointRouteBuilder builder) { var schools = builder.MapGroup("/api/schools"); schools.MapGet("/{id:int}/timetable", ( int id, string? classId, string? personId, string? lang, GameLoopService loop) => { var published = loop.FindPeople(id); if (published is null) { return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."); } return Results.Ok(MapTimetable(published, loop.Options.SchoolWeekDays, ParseLocale(lang), classId, personId)); }) .WithName("GetSchoolTimetable"); schools.MapPost("/{id:int}/timetable/pin", async ( int id, PinLessonRequest request, string? lang, GameCommandQueue commands, GameLoopService loop, CancellationToken cancellationToken) => { if (!TryDefName(request.ClassId, "classId", out var classId, out var error) || !TryDefName(request.Subject, "subject", out var subject, out error) || !TryDefName(request.RoomId, "roomId", out var roomId, out error)) { return Problem(StatusCodes.Status400BadRequest, "invalid-query", error); } var command = new GameCommand.PinLesson( id, classId, subject, roomId, request.Day, request.Period, NewCompletion()); commands.Enqueue(command); var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken); return TimetableResult(id, outcome, loop, ParseLocale(lang), classId: null, personId: null); }) .WithName("PinSchoolLesson"); schools.MapDelete("/{id:int}/timetable/pin", async ( int id, string? classId, string? subject, int? day, int? period, string? lang, GameCommandQueue commands, GameLoopService loop, CancellationToken cancellationToken) => { if (!TryDefName(classId, "classId", out var classValue, out var error) || !TryDefName(subject, "subject", out var subjectValue, out error) || day is null || period is null) { return Problem( StatusCodes.Status400BadRequest, "invalid-query", error.Length > 0 ? error : "classId, subject, day and period are required."); } var command = new GameCommand.UnpinLesson( id, classValue, subjectValue, day.Value, period.Value, NewCompletion()); commands.Enqueue(command); var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken); return TimetableResult(id, outcome, loop, ParseLocale(lang), classId: null, personId: null); }) .WithName("UnpinSchoolLesson"); } private static TaskCompletionSource NewCompletion() => new(TaskCreationOptions.RunContinuationsAsynchronously); private static string ParseLocale(string? lang) => string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase) ? "en" : "ru"; private static TimetableResponse MapTimetable( PublishedSchoolPeople published, int weekDays, string locale, string? classId, string? personId) { var table = published.Timetable ?? new Timetable([], []); var roster = published.Roster ?? new Roster([], [], []); if (published.Catalog is null) { return new TimetableResponse(weekDays, 0, [], [], [], []); } return TimetableMapper.From( table, roster, published.Catalog, published.Map, weekDays, locale, classId, personId); } private static IResult TimetableResult( int schoolId, TimetableOutcome outcome, GameLoopService loop, string locale, string? classId, string? personId) { if (outcome.Error != TimetableError.None) { return TimetableProblem(outcome.Error); } var published = loop.FindPeople(schoolId); if (published is null) { return Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."); } return Results.Ok(MapTimetable(published, loop.Options.SchoolWeekDays, locale, classId, personId)); } private static IResult TimetableProblem(TimetableError error) => error switch { TimetableError.UnknownClass => Problem(StatusCodes.Status400BadRequest, "unknown-class", "That class is not in the school."), TimetableError.UnknownSubject => Problem(StatusCodes.Status400BadRequest, "unknown-subject", "That subject is not in the catalog."), TimetableError.UnknownRoom => Problem(StatusCodes.Status400BadRequest, "unknown-room", "That room is not on the map."), TimetableError.NoTeacher => Problem(StatusCodes.Status409Conflict, "no-teacher", "Nobody is assigned that subject."), TimetableError.PinRejected => Problem(StatusCodes.Status409Conflict, "pin-rejected", "That slot or room violates the timetable constraints."), TimetableError.UnknownLesson => Problem(StatusCodes.Status404NotFound, "unknown-lesson", "That locked lesson is not on the timetable."), _ => Problem(StatusCodes.Status404NotFound, "unknown-school", "That school does not exist."), }; private static bool TryDefName(string? value, string field, out string name, out string error) { name = value?.Trim() ?? string.Empty; if (name.Length is < 1 or > 64) { error = $"{field} is not valid."; return false; } error = string.Empty; return true; } private static IResult Problem(int statusCode, string code, string detail) { return Results.Problem( detail: detail, statusCode: statusCode, title: code, extensions: new Dictionary { ["code"] = code }); } }