using Gateway.Data; using Gateway.Security; using Microsoft.AspNetCore.Mvc; using System.Text.Json; namespace Gateway.Controllers; /// /// Client-facing recommendation endpoints. /// /// Clients can view, dismiss, and resolve recommendations for their /// own campaigns. All endpoints are scoped to the authenticated client. /// /// Admin operations (rule CRUD, evaluate, cleanup) live in the /// Management API at /api/admin/recommendations. /// [ApiController] [Route("api/recommendations")] public sealed class RecommendationController : ControllerBase { private readonly SqlService _sql; private readonly ClientContext _client; private readonly AuthorizationGuard _guard; private readonly ILogger _log; public RecommendationController( SqlService sql, ClientContext client, AuthorizationGuard guard, ILogger log) { _sql = sql; _client = client; _guard = guard; _log = log; } // ──────────────────────────────────────────────── // Client-Facing: List Recommendations // ──────────────────────────────────────────────── /// /// Get active recommendations for the authenticated client's dashboard. /// Returns recommendations sorted by severity (critical first). /// [HttpGet] public async Task ListByClient( [FromQuery] string? status, [FromQuery] int? limit, CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); return await Exec(SqlNames.Procs.Recommendation, "listByClient", JsonSerializer.Serialize(new { clientId = _client.ClientId, status = status ?? "active", limit = limit ?? 50 }), ct); } /// /// Get recommendations for a specific initiative (ownership verified). /// [HttpGet("initiative/{initiativeId:long}")] public async Task ListByInitiative( long initiativeId, [FromQuery] string? status, 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.Recommendation, "listByInitiative", JsonSerializer.Serialize(new { initiativeId, status = status ?? "active" }), ct); } // ──────────────────────────────────────────────── // Client-Facing: Manage Recommendations // ──────────────────────────────────────────────── /// /// Dismiss a recommendation (user explicitly ignores it). /// [HttpPost("{recommendationId:long}/dismiss")] public async Task Dismiss(long recommendationId, CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); // Ownership check: verify the recommendation belongs to this client // The SP itself filters by recId, but we pass userId for audit trail return await Exec(SqlNames.Procs.Recommendation, "dismiss", JsonSerializer.Serialize(new { recommendationId, userId = _client.UserId }), ct); } /// /// Resolve a recommendation (action was taken to address it). /// [HttpPost("{recommendationId:long}/resolve")] public async Task Resolve(long recommendationId, CancellationToken ct) { var (ok, err) = _guard.RequireAuth(); if (!ok) return Unauthorized(new { ok = false, error = err }); return await Exec(SqlNames.Procs.Recommendation, "resolve", JsonSerializer.Serialize(new { recommendationId }), 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, "Recommendation {Action} error", action); return StatusCode(500, new { ok = false, error = "Service error" }); } } }