using System.Net.Http.Json; using System.Net; using System.Net.Http.Headers; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using UniVerse.Application.DTOs.Sync; using UniVerse.Application.Interfaces; namespace UniVerse.Infrastructure.ExternalServices; public class ModeusApiClient : IModeusApiClient { private readonly HttpClient _http; private readonly ILogger _logger; public ModeusApiClient(HttpClient http, IConfiguration config, ILogger logger) { _http = http; _logger = logger; var apiKey = config["ModeusApi:ApiKey"]; if (!string.IsNullOrEmpty(apiKey)) _http.DefaultRequestHeaders.Add("X-API-Key", apiKey); } public async Task SearchEventsAsync(SyncScheduleRequest request) { var pageSize = request.Size is > 0 ? request.Size.Value : 900; var body = new Dictionary { ["size"] = pageSize, ["timeMin"] = request.TimeMin, ["timeMax"] = request.TimeMax }; AddNonEmpty(body, "roomId", request.RoomId); AddNonEmpty(body, "attendeePersonId", request.AttendeePersonId); AddNonEmpty(body, "courseUnitRealizationId", request.CourseUnitRealizationId); AddNonEmpty(body, "cycleRealizationId", request.CycleRealizationId); AddNonEmpty(body, "specialtyCode", request.SpecialtyCode); AddNonEmpty(body, "learningStartYear", request.LearningStartYear); AddNonEmpty(body, "profileName", request.ProfileName); AddNonEmpty(body, "curriculumId", request.CurriculumId); AddNonEmpty(body, "typeId", request.TypeId); var response = await _http.PostAsJsonAsync("/api/proxy/events/search", body); var requestJson = JsonSerializer.Serialize(body); await EnsureSuccessAsync(response, "Modeus events search", BuildEventsRequestSummary(requestJson)); return await ReadJsonAsync(response, "Modeus events search", BuildEventsRequestSummary(requestJson)) ?? new ModeusEventsResponse(); } public async Task SearchRoomsAsync() { const int pageSize = 100; var allRooms = new List(); var page = 0; var totalPages = 1; do { var body = new { name = "", sort = "+building.name,+name", size = pageSize, page, deleted = false }; var response = await _http.PostAsJsonAsync("/api/proxy/rooms/search", body); await EnsureSuccessAsync(response, "Modeus rooms search", $"name=, sort=+building.name,+name, size={pageSize}, page={page}, deleted=false"); var payload = await ReadJsonAsync(response, "Modeus rooms search", $"name=, sort=+building.name,+name, size={pageSize}, page={page}, deleted=false") ?? new ModeusRoomsResponse(); allRooms.AddRange(payload.RoomItems); totalPages = payload.Page?.TotalPages ?? page + 1; page++; } while (page < totalPages); return new ModeusRoomsResponse { Rooms = allRooms }; } private static async Task EnsureSuccessAsync(HttpResponseMessage response, string operation, string requestSummary) { if (response.IsSuccessStatusCode) return; var responseBody = await response.Content.ReadAsStringAsync(); throw new HttpRequestException( $"{operation} failed with HTTP {(int)response.StatusCode} {response.ReasonPhrase}. Request: {requestSummary}. Response body: {Truncate(responseBody)}", null, response.StatusCode); } private static string BuildEventsRequestSummary(string requestJson) => $"Request JSON: {requestJson}"; private static void AddNonEmpty( IDictionary body, string key, IReadOnlyList? values) { if (values is { Count: > 0 }) body[key] = values; } private static async Task ReadJsonAsync(HttpResponseMessage response, string operation, string requestSummary) { var responseBody = await response.Content.ReadAsStringAsync(); var contentType = response.Content.Headers.ContentType?.ToString() ?? ""; var contentLength = response.Content.Headers.ContentLength?.ToString() ?? ""; if (string.IsNullOrWhiteSpace(responseBody)) { throw new HttpRequestException( $"{operation} returned HTTP {(int)response.StatusCode} {response.ReasonPhrase} with an empty response body. Request: {requestSummary}. Content-Type: {contentType}. Content-Length: {contentLength}.", null, response.StatusCode); } try { return JsonSerializer.Deserialize(responseBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (JsonException ex) { throw new InvalidOperationException( $"{operation} returned invalid JSON. Request: {requestSummary}. Content-Type: {contentType}. Response body: {Truncate(responseBody)}", ex); } } private static string Truncate(string value) => value.Length > 2000 ? string.Concat(value.AsSpan(0, 2000), "...") : value; public async Task> SearchEmployeeAsync(string fullname) { var response = await _http.GetFromJsonAsync>( $"/api/schedule/searchemployee?fullname={Uri.EscapeDataString(fullname)}"); return response ?? new(); } public async Task GetSubIdByFullNameAsync(string fullname, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage( HttpMethod.Get, $"/api/universe/subid?fullname={Uri.EscapeDataString(fullname)}"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain")); using var response = await _http.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) return null; await EnsureSuccessAsync(response, "Universe user sub lookup", $"fullname={fullname}"); var body = await response.Content.ReadAsStringAsync(cancellationToken); return string.IsNullOrWhiteSpace(body) ? null : body.Trim(); } }