using MealPlan.Api.DTOs.Recipe;
using MealPlan.Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace MealPlan.Api.Controllers;
/// Endpoints for creating recipes manually and importing from Excel.
[ApiController]
[Route("api/recipes")]
[Authorize]
public class RecipeManagementController : ControllerBase
{
private readonly IRecipeManagementService _managementService;
private readonly IRecipeExcelService _excelService;
private readonly IRecipeCalorieRecalculationService _calorieRecalculation;
public RecipeManagementController(
IRecipeManagementService managementService,
IRecipeExcelService excelService,
IRecipeCalorieRecalculationService calorieRecalculation)
{
_managementService = managementService;
_excelService = excelService;
_calorieRecalculation = calorieRecalculation;
}
/// Creates a new recipe with ingredients and steps.
[HttpPost]
[ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task Create([FromBody] CreateRecipeDto dto, CancellationToken ct)
{
var recipe = await _managementService.CreateRecipeAsync(dto, ct);
return CreatedAtAction(
nameof(RecipesController.GetRecipe),
"Recipes",
new { id = recipe.Id },
recipe);
}
/// Updates an existing recipe (replaces ingredients and steps).
[HttpPut("{id:int}")]
[ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task Update(int id, [FromBody] CreateRecipeDto dto, CancellationToken ct)
{
var recipe = await _managementService.UpdateRecipeAsync(id, dto, ct);
return recipe is null
? NotFound(new { error = $"Recipe {id} was not found." })
: Ok(recipe);
}
/// Downloads an Excel template for bulk recipe import.
[HttpGet("import/template")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public IActionResult DownloadTemplate()
{
var bytes = _excelService.GenerateTemplate();
return File(
bytes,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"DailyMeals-recipe-import-template.xlsx");
}
/// Imports recipes from an uploaded .xlsx file (Recipes + Ingredients + Steps sheets).
[HttpPost("import/excel")]
[RequestSizeLimit(10_000_000)]
[ProducesResponseType(typeof(RecipeImportResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task ImportExcel(IFormFile? file, CancellationToken ct)
{
if (file is null || file.Length == 0)
{
return BadRequest(new { error = "No file uploaded." });
}
if (!file.FileName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
{
return BadRequest(new { error = "Only .xlsx files are supported." });
}
await using var stream = file.OpenReadStream();
var result = await _excelService.ImportAsync(stream, ct);
return Ok(result);
}
/// Recalculates recipe macros from linked catalog ingredients (all active recipes).
[HttpPost("recalculate-macros")]
[ProducesResponseType(typeof(RecipeMacroRecalcResultDto), StatusCodes.Status200OK)]
public async Task RecalculateMacros(CancellationToken ct)
{
var result = await _managementService.RecalculateAllMacrosFromCatalogAsync(ct);
return Ok(result);
}
/// Scales or AI-adjusts ingredient amounts to match a calorie target.
[HttpPost("{id:int}/recalculate-calories")]
[ProducesResponseType(typeof(RecalculateRecipeCaloriesResultDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task RecalculateCalories(
int id,
[FromBody] RecalculateRecipeCaloriesRequestDto request,
CancellationToken ct)
{
try
{
var result = await _calorieRecalculation.RecalculateAsync(id, request, ct);
return result is null
? NotFound(new { error = $"Recipe {id} was not found." })
: Ok(result);
}
catch (InvalidOperationException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}