using Gateway.Models; using Gateway.Security; using Gateway.Services; using Microsoft.AspNetCore.Mvc; namespace Gateway.Controllers; /// /// Channel forecast endpoint for the campaign wizard. /// /// Routes to IntelligenceApi (category-aware engine container) when configured. /// Falls back to local ForecastService (General/rules-based) if IntelligenceApi /// is unreachable — ensuring the wizard never breaks during deployments. /// /// ROUTING LOGIC: /// 1. Try IntelligenceApi — passes clientCategory so the engine router /// can select the correct model (General, Franchisee, Franchisor, etc.) /// 2. If unreachable / error → fall back to local ForecastService /// /// SECURITY: Requires authenticated client session. /// [ApiController] [Route("api/forecast")] public sealed class ForecastController : ControllerBase { private readonly ForecastService _forecastService; private readonly IntelligenceApiClient _intelligenceClient; private readonly ClientContext _client; private readonly AuthorizationGuard _guard; private readonly ILogger _log; public ForecastController( ForecastService forecastService, IntelligenceApiClient intelligenceClient, ClientContext client, AuthorizationGuard guard, ILogger log) { _forecastService = forecastService; _intelligenceClient = intelligenceClient; _client = client; _guard = guard; _log = log; } /// /// Generate channel performance estimates for given targeting + budget. /// Called by the wizard AllocationStep when budget changes. /// /// POST /api/forecast/channel-estimate /// [HttpPost("channel-estimate")] public async Task ChannelEstimate( [FromBody] ChannelForecastRequest? request, CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); if (request == null) return BadRequest(new { ok = false, error = "Request body is required" }); if (request.MonthlyBudget <= 0) return BadRequest(new { ok = false, error = "monthlyBudget must be greater than zero" }); if (request.Keywords.Count == 0) return BadRequest(new { ok = false, error = "At least one keyword is required" }); _log.LogInformation( "[Forecast] Request | Category={Category} Budget={Budget} Objective={Obj}", _client.ClientCategory, request.MonthlyBudget, request.Objective); try { // ── 1. Try IntelligenceApi (category-aware) ── var result = await _intelligenceClient.GetSpendDistributionAsync( request, _client.ClientCategory, ct); if (result != null) { _log.LogInformation("[Forecast] Served by IntelligenceApi"); return Ok(result); } // ── 2. Fallback: local ForecastService (General engine equivalent) ── _log.LogInformation("[Forecast] IntelligenceApi unavailable — using local ForecastService"); var fallback = await _forecastService.ForecastAsync(request, ct); return Ok(fallback); } catch (Exception ex) { _log.LogError(ex, "[Forecast] Error"); return StatusCode(500, new { ok = false, error = "Forecast service error" }); } } }