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;
}
}
}