using System.Net; using System.Text; using System.Text.Json; using Microsoft.Extensions.Options; using Tau.Acuvim.Portal.Configuration; using Tau.Acuvim.Portal.DTOs; namespace Tau.Acuvim.Portal.Services; public sealed class FleetPushResult { public bool Succeeded { get; init; } public int Accepted { get; init; } public int Rejected { get; init; } public HttpStatusCode StatusCode { get; init; } public string? Error { get; init; } public TimeSpan? RetryAfter { get; init; } } // HttpClient wrapper for POSTing batches to the Admin ingest endpoint. public sealed class FleetPushClient( HttpClient http, IOptions options, ILogger log) { private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web); public Task PushSitesAsync(IReadOnlyList rows, DateTime cursor, CancellationToken ct) => PushAsync("sites", rows, cursor, ct); public Task PushDevicesAsync(IReadOnlyList rows, DateTime cursor, CancellationToken ct) => PushAsync("devices", rows, cursor, ct); public Task PushMeasurementsAsync(IReadOnlyList rows, DateTime cursor, CancellationToken ct) => PushAsync("measurements", rows, cursor, ct); private async Task PushAsync( string batchType, IReadOnlyList rows, DateTime cursor, CancellationToken ct) { var opts = options.Value; if (string.IsNullOrWhiteSpace(opts.Url) || string.IsNullOrWhiteSpace(opts.Token)) { return new FleetPushResult { Succeeded = false, Error = "FleetIngest URL or Token not configured." }; } var json = JsonSerializer.Serialize(rows, JsonOpts); var bytes = Encoding.UTF8.GetBytes(json); if (bytes.Length > opts.BatchMaxBytes) { return new FleetPushResult { Succeeded = false, StatusCode = HttpStatusCode.RequestEntityTooLarge, Error = $"Local pre-check: batch size {bytes.Length} > {opts.BatchMaxBytes}. Halve batch and retry." }; } using var req = new HttpRequestMessage(HttpMethod.Post, opts.Url); req.Headers.Add("X-Customer-Token", opts.Token); req.Headers.Add("X-Batch-Type", batchType); req.Headers.Add("X-Push-Cursor", cursor.ToString("O")); req.Content = new ByteArrayContent(bytes); req.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); try { using var resp = await http.SendAsync(req, ct); var status = resp.StatusCode; if (status == HttpStatusCode.OK) { try { var body = await resp.Content.ReadAsStringAsync(ct); var result = JsonSerializer.Deserialize(body, JsonOpts); return new FleetPushResult { Succeeded = true, Accepted = result?.Accepted ?? rows.Count, Rejected = result?.Rejected ?? 0, StatusCode = status }; } catch { return new FleetPushResult { Succeeded = true, Accepted = rows.Count, StatusCode = status }; } } TimeSpan? retryAfter = null; if (resp.Headers.RetryAfter?.Delta is { } d) retryAfter = d; return new FleetPushResult { Succeeded = false, StatusCode = status, RetryAfter = retryAfter, Error = $"HTTP {(int)status} {status}" }; } catch (TaskCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) { log.LogWarning(ex, "Fleet push transport failure to {Url}", opts.Url); return new FleetPushResult { Succeeded = false, Error = ex.Message }; } } }