using System; using System.IO; using System.Text; using Best.HTTP; using Cysharp.Threading.Tasks; using IchniOnline.Online.Network.Models; using UnityEngine; namespace IchniOnline.Online.Network { /// /// BestHTTP-based API client singleton for IchniOnline backend communication. /// Pure HTTP layer — no business logic. /// public class IchniOnlineApiClient { private static IchniOnlineApiClient _instance; public static IchniOnlineApiClient Instance => _instance ??= new IchniOnlineApiClient(); public string BaseUrl { get; set; } = "http://localhost:5308"; public string JwtToken { get; set; } private IchniOnlineApiClient() { } public async UniTask> GetAsync(string endpoint) { string url = BuildUrl(endpoint); var request = new HTTPRequest(new Uri(url), HTTPMethods.Get); AddAuthHeader(request); try { var resp = await SendAsync(request); return ProcessResponse(resp); } catch (Exception ex) { return ApiResult.Fail(ResponseCode.InternalServerError, "Network error", ex.Message); } } public async UniTask> PostAsync(string endpoint, object body) { string url = BuildUrl(endpoint); var request = new HTTPRequest(new Uri(url), HTTPMethods.Post); request.SetHeader("Content-Type", "application/json"); AddAuthHeader(request); if (body != null) { string jsonBody = JsonUtility.ToJson(body); request.UploadSettings.UploadStream = new MemoryStream(Encoding.UTF8.GetBytes(jsonBody)); } try { var resp = await SendAsync(request); return ProcessResponse(resp); } catch (Exception ex) { return ApiResult.Fail(ResponseCode.InternalServerError, "Network error", ex.Message); } } private string BuildUrl(string endpoint) { if (string.IsNullOrEmpty(BaseUrl)) throw new InvalidOperationException("BaseUrl is not configured."); string baseUrl = BaseUrl.TrimEnd('/'); string path = endpoint.StartsWith("/") ? endpoint : $"/{endpoint}"; return baseUrl + path; } private void AddAuthHeader(HTTPRequest request) { if (!string.IsNullOrEmpty(JwtToken)) { request.SetHeader("Authorization", $"Bearer {JwtToken}"); } } private UniTask SendAsync(HTTPRequest request) { var completionSource = new UniTaskCompletionSource(); request.Callback = (req, resp) => { switch (req.State) { case HTTPRequestStates.Finished: completionSource.TrySetResult(resp); break; case HTTPRequestStates.Aborted: completionSource.TrySetCanceled(); break; default: completionSource.TrySetException(req.Exception ?? new Exception($"HTTP request failed: {req.State}")); break; } }; request.Send(); return completionSource.Task; } private ApiResult ProcessResponse(HTTPResponse resp) { string json = resp.DataAsText; if (resp.StatusCode >= 200 && resp.StatusCode < 300) { if (string.IsNullOrEmpty(json)) { return ApiResult.Fail(ResponseCode.InternalServerError, "Empty response body"); } var response = JsonUtility.FromJson(json, typeof(GlobalResponse)) as GlobalResponse; if (response == null) { return ApiResult.Fail(ResponseCode.InternalServerError, "Failed to parse response JSON"); } if (response.code == ResponseCode.Ok) { return ApiResult.Ok(response.data); } return ApiResult.Fail(response.code, response.message); } // Non-2xx: try to parse server error body if (!string.IsNullOrEmpty(json)) { var errorResponse = JsonUtility.FromJson(json, typeof(GlobalResponseBase)) as GlobalResponseBase; if (errorResponse != null) { return ApiResult.Fail(errorResponse.code, errorResponse.message); } } return ApiResult.Fail(ResponseCode.InternalServerError, $"HTTP error {resp.StatusCode}"); } } }