using System.Net.Http.Json; 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) { const int pageSize = 900; var specialtyCodes = string.IsNullOrWhiteSpace(request.SpecialtyCode) ? [] : request.SpecialtyCode .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var typeIds = request.TypeId ?? []; var body = new Dictionary { ["size"] = pageSize, ["timeMin"] = request.TimeMin, ["timeMax"] = request.TimeMax, ["specialtyCode"] = specialtyCodes }; if (typeIds.Count > 0) body["typeId"] = typeIds; var response = await _http.PostAsJsonAsync("/api/proxy/events/search", body); var requestJson = JsonSerializer.Serialize(body); await EnsureSuccessAsync(response, "Modeus events search", BuildEventsRequestSummary(pageSize, specialtyCodes, request.TimeMin, request.TimeMax, typeIds, requestJson)); return await ReadJsonAsync(response, "Modeus events search", BuildEventsRequestSummary(pageSize, specialtyCodes, request.TimeMin, request.TimeMax, typeIds, 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( int size, IReadOnlyList specialtyCodes, DateTime? timeMin, DateTime? timeMax, IReadOnlyList typeIds, string requestJson) { var typeFilter = typeIds.Count > 0 ? $"typeId=[{string.Join(", ", typeIds)}]" : "typeId="; return $"size={size}, specialtyCode=[{string.Join(", ", specialtyCodes)}], timeMin={timeMin:O}, timeMax={timeMax:O}, {typeFilter}. Request JSON: {requestJson}"; } 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(); } }