using System.Net.Http.Json; using System.Text; using System.Text.Json; using FreeCode.Core.Enums; using FreeCode.Core.Models; namespace FreeCode.Bridge; public sealed class BridgeApiClient { private readonly HttpClient _httpClient; public BridgeApiClient(HttpClient? httpClient = null) { _httpClient = httpClient ?? new HttpClient(); _httpClient.Timeout = TimeSpan.FromSeconds(30); } public async Task GetStatusAsync(BridgeConfig config, CancellationToken ct = default) { using var response = await _httpClient.GetAsync(Root(config, "bridge/status"), ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await ReadJsonAsync(response, ct).ConfigureAwait(false) ?? new BridgeStatusInfo(BridgeStatus.Idle); } public async Task PostEventAsync(BridgeConfig config, object payload, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/events"), payload, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task SendSessionUpdateAsync(BridgeConfig config, string sessionId, object payload, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/update"), payload, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task RequestPermissionAsync(BridgeConfig config, string sessionId, object payload, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/permissions/request"), payload, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await ReadJsonAsync(response, ct).ConfigureAwait(false); } public async Task RegisterBridgeEnvironmentAsync(BridgeConfig config, BridgeEnvironment environment, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/environments"), environment, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await ReadJsonAsync(response, ct).ConfigureAwait(false) ?? new BridgeStatusInfo(BridgeStatus.Registered); } public async Task PollForWorkAsync(BridgeConfig config, string environmentId, CancellationToken ct = default) { using var response = await _httpClient.GetAsync(Root(config, $"bridge/environments/{Uri.EscapeDataString(environmentId)}/work"), ct).ConfigureAwait(false); if (response.StatusCode == System.Net.HttpStatusCode.NoContent) { return null; } response.EnsureSuccessStatusCode(); return await ReadJsonAsync(response, ct).ConfigureAwait(false); } public async Task AcknowledgeWorkAsync(BridgeConfig config, string workId, string sessionToken, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}/ack"), new Dictionary { ["sessionToken"] = sessionToken }, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task SpawnSessionAsync(BridgeConfig config, SessionSpawnOptions options, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, "bridge/sessions"), options, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); return await ReadJsonAsync(response, ct).ConfigureAwait(false) ?? new SessionHandle(options.Environment.Id, string.Empty); } public async Task SendPermissionResponseAsync(BridgeConfig config, string sessionId, PermissionResponse responseBody, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/sessions/{Uri.EscapeDataString(sessionId)}/permissions"), responseBody, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task HeartbeatAsync(BridgeConfig config, string workId, string sessionToken, CancellationToken ct = default) { using var response = await SendJsonAsync(HttpMethod.Post, Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}/heartbeat"), new Dictionary { ["sessionToken"] = sessionToken }, ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task StopWorkAsync(BridgeConfig config, string workId, CancellationToken ct = default) { using var response = await _httpClient.DeleteAsync(Root(config, $"bridge/work/{Uri.EscapeDataString(workId)}"), ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } public async Task DeregisterEnvironmentAsync(BridgeConfig config, string environmentId, CancellationToken ct = default) { using var response = await _httpClient.DeleteAsync(Root(config, $"bridge/environments/{Uri.EscapeDataString(environmentId)}"), ct).ConfigureAwait(false); response.EnsureSuccessStatusCode(); } private async Task SendJsonAsync(HttpMethod method, Uri uri, object payload, CancellationToken ct) { var json = payload switch { JsonElement element => JsonSerializer.Serialize(element, SourceGenerationContext.Default.JsonElement), Dictionary dictionary => JsonSerializer.Serialize(dictionary), BridgeEnvironment environment => JsonSerializer.Serialize(environment, SourceGenerationContext.Default.BridgeEnvironment), SessionSpawnOptions options => JsonSerializer.Serialize(options, SourceGenerationContext.Default.SessionSpawnOptions), PermissionResponse permissionResponse => JsonSerializer.Serialize(permissionResponse, SourceGenerationContext.Default.PermissionResponse), RemoteConnectedEvent remoteConnectedEvent => JsonSerializer.Serialize(remoteConnectedEvent, SourceGenerationContext.Default.RemoteConnectedEvent), RemoteDisconnectedEvent remoteDisconnectedEvent => JsonSerializer.Serialize(remoteDisconnectedEvent, SourceGenerationContext.Default.RemoteDisconnectedEvent), RemoteMessageEvent remoteMessageEvent => JsonSerializer.Serialize(remoteMessageEvent, SourceGenerationContext.Default.RemoteMessageEvent), _ => throw new NotSupportedException($"Unsupported bridge JSON payload type: {payload.GetType().FullName}") }; var request = new HttpRequestMessage(method, uri) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; return await _httpClient.SendAsync(request, ct).ConfigureAwait(false); } private static Uri Root(BridgeConfig config, string relativePath) { var baseUri = new Uri(config.BaseUrl.TrimEnd('/') + "/", UriKind.Absolute); return new Uri(baseUri, relativePath); } private static async Task ReadJsonAsync(HttpResponseMessage response, CancellationToken ct) { await using var stream = await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false); return (T?)await response.Content.ReadFromJsonAsync(typeof(T), SourceGenerationContext.Default, ct).ConfigureAwait(false); } }