using Gateway.Models; using System.Text.Json; namespace Gateway.Services; /// /// HTTP client for IntelligenceApi — the spend distribution engine container. /// /// The Gateway injects clientCategory from ClientContext and provider config /// before forwarding requests. The client portal never calls IntelligenceApi /// directly; all routing goes through the Gateway. /// /// FALLBACK: If IntelligenceApi is unreachable, ForecastController falls back /// to the local ForecastService (identical to the General engine output). /// This means a container restart or deployment never breaks the wizard. /// public sealed class IntelligenceApiClient { private readonly IHttpClientFactory _http; private readonly IConfiguration _cfg; private readonly ILogger _logger; private static readonly JsonSerializerOptions _jsonOpts = new(JsonSerializerDefaults.Web); public IntelligenceApiClient( IHttpClientFactory http, IConfiguration cfg, ILogger logger) { _http = http; _cfg = cfg; _logger = logger; } /// /// Forward raw census data for a ZCTA to the Intelligence container for /// market analysis derivation (age chips, income tiers, insight strings). /// Returns the raw JSON response string, or null if the container is /// unreachable — caller falls back to returning raw census data. /// public async Task GetDemographicAnalysisAsync( string zcta, JsonElement censusData, CancellationToken ct) { var baseUrl = _cfg["INTELLIGENCE_API_URL"] ?? Environment.GetEnvironmentVariable("INTELLIGENCE_API_URL"); if (string.IsNullOrWhiteSpace(baseUrl)) { _logger.LogDebug("[IntelligenceApiClient] INTELLIGENCE_API_URL not configured — skipping demographics analysis"); return null; } var internalKey = _cfg["INTELLIGENCE_INTERNAL_KEY"] ?? Environment.GetEnvironmentVariable("INTELLIGENCE_INTERNAL_KEY"); var payload = JsonSerializer.Serialize(new { zcta, census = censusData }, _jsonOpts); try { var client = _http.CreateClient(); using var msg = new HttpRequestMessage( HttpMethod.Post, $"{baseUrl.TrimEnd('/')}/api/demographics/analyze"); if (!string.IsNullOrWhiteSpace(internalKey)) msg.Headers.Add("X-Internal-Key", internalKey); msg.Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json"); _logger.LogInformation("[IntelligenceApiClient] Demographics analysis | ZCTA={Zcta}", zcta); using var resp = await client.SendAsync(msg, ct); var body = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode) { _logger.LogWarning("[IntelligenceApiClient] Demographics non-success {Status}", (int)resp.StatusCode); return null; } return body; } catch (TaskCanceledException) { _logger.LogWarning("[IntelligenceApiClient] Demographics analysis timed out | ZCTA={Zcta}", zcta); return null; } catch (Exception ex) { _logger.LogError(ex, "[IntelligenceApiClient] Demographics analysis failed | ZCTA={Zcta}", zcta); return null; } } /// /// Forward a channel forecast request to IntelligenceApi with /// clientCategory injected. Returns null if the service is unreachable /// or returns an error — caller should fall back to ForecastService. /// public async Task GetSpendDistributionAsync( ChannelForecastRequest request, string? clientCategory, CancellationToken ct) { var baseUrl = _cfg["INTELLIGENCE_API_URL"] ?? Environment.GetEnvironmentVariable("INTELLIGENCE_API_URL"); if (string.IsNullOrWhiteSpace(baseUrl)) { _logger.LogDebug("[IntelligenceApiClient] INTELLIGENCE_API_URL not configured — skipping"); return null; } var internalKey = _cfg["INTELLIGENCE_INTERNAL_KEY"] ?? Environment.GetEnvironmentVariable("INTELLIGENCE_INTERNAL_KEY"); // Build the IntelligenceApi request — forward everything from the // original wizard request, plus inject clientCategory and provider config var intelligenceRequest = new { clientCategory = clientCategory ?? "General", objective = request.Objective, businessCategory = request.BusinessCategory, keywords = request.Keywords, geoTargeting = request.GeoTargeting, audience = request.Audience, monthlyBudget = request.MonthlyBudget, channels = request.Channels, // Forward provider URLs so the engine can call providers directly providerUrls = new Dictionary { ["google_ads"] = _cfg["GOOGLE_PROVIDER_URL"] ?? Environment.GetEnvironmentVariable("GOOGLE_PROVIDER_URL") ?? "" }, internalKeys = new Dictionary { ["google_ads"] = _cfg["GOOGLE_INTERNAL_KEY"] ?? Environment.GetEnvironmentVariable("GOOGLE_INTERNAL_KEY") ?? "" } }; try { var client = _http.CreateClient(); using var msg = new HttpRequestMessage( HttpMethod.Post, $"{baseUrl.TrimEnd('/')}/api/spend-distribution"); if (!string.IsNullOrWhiteSpace(internalKey)) msg.Headers.Add("X-Internal-Key", internalKey); msg.Content = new StringContent( JsonSerializer.Serialize(intelligenceRequest, _jsonOpts), System.Text.Encoding.UTF8, "application/json"); _logger.LogInformation( "[IntelligenceApiClient] Calling engine | Category={Category} Budget={Budget}", clientCategory, request.MonthlyBudget); using var resp = await client.SendAsync(msg, ct); var body = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode) { _logger.LogWarning( "[IntelligenceApiClient] Non-success {Status}: {Body}", (int)resp.StatusCode, body[..Math.Min(body.Length, 200)]); return null; } // Map IntelligenceApi response shape → Gateway ChannelForecastResponse // The shapes are intentionally aligned so this is a straight deserialize. using var doc = JsonDocument.Parse(body); var root = doc.RootElement; if (!root.TryGetProperty("ok", out var okProp) || !okProp.GetBoolean()) { _logger.LogWarning("[IntelligenceApiClient] Engine returned ok=false"); return null; } // Re-serialize then deserialize into the Gateway model // (avoids a hard dependency on IntelligenceApi model types in Gateway) var result = JsonSerializer.Deserialize(body, _jsonOpts); _logger.LogInformation( "[IntelligenceApiClient] OK | Engine={Engine} Channels={N}", root.TryGetProperty("metadata", out var meta) && meta.TryGetProperty("engine", out var eng) ? eng.GetString() : "?", result?.Channels?.Count ?? 0); return result; } catch (TaskCanceledException) { _logger.LogWarning("[IntelligenceApiClient] Request timed out"); return null; } catch (Exception ex) { _logger.LogError(ex, "[IntelligenceApiClient] Request failed"); return null; } } }