using System.Text.Json; using MealPlan.Api.DTOs.Generators; using MealPlan.Api.Extensions; using MealPlan.Api.Services.Generators; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace MealPlan.Api.Controllers; [ApiController] [Route("api/recipe-generator")] [Authorize] public class RecipeGeneratorController : ControllerBase { private readonly IRecipeGeneratorService _service; public RecipeGeneratorController(IRecipeGeneratorService service) => _service = service; [HttpPost("sessions")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status201Created)] public async Task CreateSession([FromBody] RecipeGeneratorConstraintsDto dto, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; var session = await _service.CreateSessionAsync(userId, dto, ct); return CreatedAtAction(nameof(GetSession), new { id = session.Id }, session); } [HttpGet("sessions/{id:int}")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetSession(int id, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; var session = await _service.GetSessionAsync(userId, id, ct); return session is null ? NotFound() : Ok(session); } [HttpPost("sessions/{id:int}/generate")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] public async Task Generate(int id, [FromBody] GenerateRecipesRequestDto? request, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; try { var session = await _service.GenerateAsync(userId, id, request, ct); return session is null ? NotFound() : Ok(session); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } } [HttpPost("sessions/{id:int}/regenerate")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] public async Task Regenerate(int id, [FromBody] RegenerateRecipePartDto dto, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; try { var session = await _service.RegeneratePartAsync(userId, id, dto, ct); return session is null ? NotFound() : Ok(session); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } catch (JsonException) { return BadRequest(new { error = "Could not parse AI recipe response. Try again." }); } } [HttpPut("sessions/{id:int}/draft")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] public async Task UpdateDraft(int id, [FromBody] GeneratedRecipeDraftDto dto, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; var session = await _service.UpdateDraftAsync(userId, id, dto, ct); return session is null ? NotFound() : Ok(session); } [HttpDelete("sessions/{id:int}/drafts/{draftId}")] [ProducesResponseType(typeof(RecipeGeneratorSessionDto), StatusCodes.Status200OK)] public async Task RemoveDraft(int id, string draftId, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; var session = await _service.RemoveDraftAsync(userId, id, draftId, ct); return session is null ? NotFound() : Ok(session); } [HttpPost("sessions/{id:int}/commit")] [ProducesResponseType(typeof(CommitRecipesResultDto), StatusCodes.Status200OK)] public async Task Commit(int id, [FromBody] CommitRecipesRequestDto? request, CancellationToken ct) { if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized; try { var result = await _service.CommitAsync(userId, id, request, ct); if (result is null) { return NotFound(new { error = "Nothing to save. The session may have expired, or the selected drafts were not found.", }); } return Ok(result); } catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); } } }