using Gateway.Data; using Gateway.Security; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace Gateway.Controllers; /// /// Campaign intelligence endpoints: health overview, budget pacing, /// and post-campaign analysis. /// /// SECURITY MODEL: /// - Every endpoint requires authenticated session (via middleware) /// - Initiative endpoints verify ownership before data access /// - Client-level endpoints scoped via injected ClientContext /// - ClientId is always injected server-side, never from request body /// [ApiController] [Route("api/intelligence")] public sealed class CampaignIntelligenceController : ControllerBase { private readonly SqlService _sql; private readonly ClientContext _client; private readonly AuthorizationGuard _guard; private readonly ILogger _log; public CampaignIntelligenceController( SqlService sql, ClientContext client, AuthorizationGuard guard, ILogger log) { _sql = sql; _client = client; _guard = guard; _log = log; } // ──────────────────────────────────────────────── // Campaign Health Overview // ──────────────────────────────────────────────── /// /// Get health overview for all active initiatives. /// Returns green/yellow/red status per channel campaign based on active recommendations. /// [HttpGet("health")] public async Task Health(CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); return await Exec(SqlNames.Procs.CampaignIntelligence, "health", JsonSerializer.Serialize(new { clientId = _client.ClientId }), ct); } // ──────────────────────────────────────────────── // Budget Pacing // ──────────────────────────────────────────────── /// /// Get budget pacing analysis for an initiative. /// Shows actual vs expected spend velocity with projections. /// [HttpGet("{initiativeId:long}/pacing")] public async Task Pacing(long initiativeId, CancellationToken ct) { var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct); if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error }); return await Exec(SqlNames.Procs.CampaignIntelligence, "pacing", JsonSerializer.Serialize(new { initiativeId }), ct); } // ──────────────────────────────────────────────── // Post-Campaign Report // ──────────────────────────────────────────────── /// /// Comprehensive post-campaign analysis. /// Cross-platform comparison with daily trends, efficiency metrics, /// and recommendation history. /// [HttpGet("{initiativeId:long}/report")] public async Task PostCampaignReport(long initiativeId, CancellationToken ct) { var ownership = await _guard.VerifyInitiativeOwnerAsync(initiativeId, ct); if (!ownership.IsAllowed) return NotFound(new { ok = false, error = ownership.Error }); return await Exec(SqlNames.Procs.CampaignIntelligence, "postCampaign", JsonSerializer.Serialize(new { initiativeId }), ct); } // ──────────────────────────────────────────────── // Metric Snapshots (internal / polling service) // ──────────────────────────────────────────────── /// /// Record an intraday metric snapshot for pacing analysis. /// Called by the background polling service between daily aggregations. /// Admin-only endpoint. /// [HttpPost("snapshot")] public async Task Snapshot([FromBody] SnapshotRequest request, CancellationToken ct) { var (ok, err) = _guard.RequireServiceKey(); if (!ok) return StatusCode(403, new { ok = false, error = err }); return await Exec(SqlNames.Procs.CampaignIntelligence, "snapshot", JsonSerializer.Serialize(new { channelCampaignId = request.ChannelCampaignId, date = request.Date, impressions = request.Impressions, clicks = request.Clicks, spend = request.Spend, conversions = request.Conversions }), ct); } /// /// Batch insert intraday snapshots. /// Admin-only endpoint. /// [HttpPost("snapshot/batch")] public async Task SnapshotBatch([FromBody] SnapshotBatchRequest request, CancellationToken ct) { var (ok, err) = _guard.RequireServiceKey(); if (!ok) return StatusCode(403, new { ok = false, error = err }); return await Exec(SqlNames.Procs.CampaignIntelligence, "snapshotBatch", JsonSerializer.Serialize(new { snapshots = request.Snapshots }), ct); } // ──────────────────────────────────────────────── // Helpers // ──────────────────────────────────────────────── private async Task Exec(string proc, string action, string rqst, CancellationToken ct) { try { var resp = await _sql.ExecProcAsync(proc, action, rqst, ct: ct); if (string.IsNullOrWhiteSpace(resp)) return StatusCode(500, new { ok = false, error = "Service unavailable" }); using var doc = JsonDocument.Parse(resp); var root = doc.RootElement; if (root.TryGetProperty("ok", out var okProp) && !okProp.GetBoolean()) { var error = root.TryGetProperty("error", out var errProp) ? errProp.GetString() : "Unknown error"; if (error?.Contains("not found", StringComparison.OrdinalIgnoreCase) == true) return NotFound(JsonSerializer.Deserialize(resp)); return BadRequest(JsonSerializer.Deserialize(resp)); } return Content(resp, "application/json"); } catch (Exception ex) { _log.LogError(ex, "CampaignIntelligence {Action} error", action); return StatusCode(500, new { ok = false, error = "Service error" }); } } } // ── DTOs ── public sealed class SnapshotRequest { public long? ChannelCampaignId { get; set; } public string? Date { get; set; } public long? Impressions { get; set; } public long? Clicks { get; set; } public decimal? Spend { get; set; } public decimal? Conversions { get; set; } } public sealed class SnapshotBatchRequest { public object[]? Snapshots { get; set; } }