using IntelligenceApi.Engines; using IntelligenceApi.Models; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace IntelligenceApi.Controllers; /// /// Internal execution endpoint — the single entry point for Gateway-routed requests. /// /// Called exclusively by the Gateway's ExecutionService via: /// POST /internal/execute /// Headers: X-Internal-Key, X-Request-Id /// Body: { "operation": "Ping" | "SpendDistribution", "payload": { ... } } /// /// Operations: /// Ping — liveness check, no payload required /// SpendDistribution — routes to EngineRouter; payload maps to SpendDistributionRequest /// [ApiController] [Route("internal/execute")] public sealed class InternalController : ControllerBase { private readonly EngineRouter _router; private readonly ILogger _log; private static readonly JsonSerializerOptions _jsonOpts = new(JsonSerializerDefaults.Web); public InternalController(EngineRouter router, ILogger log) { _router = router; _log = log; } [HttpPost] public async Task Execute( [FromBody] InternalExecuteRequest? request, CancellationToken ct) { if (request == null) return BadRequest(new { ok = false, error = "Request body required" }); var requestId = request.RequestId ?? HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("N"); var operation = (request.Operation ?? "").Trim(); _log.LogInformation( "[Internal] Operation={Operation} RequestId={RequestId}", operation, requestId); return operation.ToLowerInvariant() switch { "ping" => Ok(new { ok = true, requestId, service = "IntelligenceApi", timestamp = DateTimeOffset.UtcNow }), "spenddistribution" => await SpendDistribution(request.Payload, requestId, ct), _ => BadRequest(new { ok = false, requestId, error = $"Unknown operation: '{operation}'" }) }; } // ── Spend Distribution ─────────────────────────────────────────────────── private async Task SpendDistribution( JsonElement? payload, string requestId, CancellationToken ct) { if (payload == null || payload.Value.ValueKind == JsonValueKind.Null) return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" }); SpendDistributionRequest? distRequest; try { distRequest = JsonSerializer.Deserialize( payload.Value.GetRawText(), _jsonOpts); } catch (Exception ex) { _log.LogWarning("[Internal] Payload deserialize failed | RequestId={RequestId} Error={Error}", requestId, ex.Message); return BadRequest(new { ok = false, requestId, error = "Invalid payload shape for SpendDistribution" }); } if (distRequest == null) return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" }); if (distRequest.MonthlyBudget <= 0) return BadRequest(new { ok = false, requestId, error = "monthlyBudget must be greater than zero" }); if (distRequest.Keywords.Count == 0) return BadRequest(new { ok = false, requestId, error = "At least one keyword is required" }); try { var engine = _router.Resolve(distRequest.ClientCategory); var response = await engine.RecommendAsync(distRequest, ct); _log.LogInformation( "[Internal] SpendDistribution OK | RequestId={RequestId} Engine={Engine} Channels={N}", requestId, response.Metadata.Engine, response.Channels.Count); return Ok(response); } catch (Exception ex) { _log.LogError(ex, "[Internal] SpendDistribution error | RequestId={RequestId}", requestId); return StatusCode(500, new { ok = false, requestId, error = "Intelligence service error" }); } } } // ── Request model ──────────────────────────────────────────────────────────── /// /// Shape sent by Gateway's ExecutionService to every provider container. /// Matches the object built in ExecutionService.BuildProviderRequest(). /// public sealed class InternalExecuteRequest { public string? Operation { get; set; } public string? RequestId { get; set; } public string? TenantId { get; set; } public JsonElement? Payload { get; set; } }