using System; using System.Threading; using System.Threading.Tasks; using AutoMapper; using LaDOSE.Business.Interface; using LaDOSE.DTO; using LaDOSE.Entity; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace LaDOSE.Api.Controllers { /// /// Pushes ranking tables into the club's Google Spreadsheet. /// /// The tables arrive already built: the browser reuses the same buildRanking() that produces /// the CSV export, so the sheet and the CSV cannot drift. This controller resolves the target /// from configuration, validates, and relays — see SheetsExportService. /// /// Returns IActionResult with explicit ProducesResponseType, like UsersController: the API has /// no exception middleware, so an escaping throw reaches the browser as an HTML developer page /// that the client can only report as a bare status. /// [Authorize] [Produces("application/json")] [Route("api/[controller]")] public class SheetsController : ControllerBase { private readonly ISheetsExportService _service; private readonly IMapper _mapper; private readonly ILogger _logger; public SheetsController(IMapper mapper, ISheetsExportService service, ILogger logger) { _mapper = mapper; _service = service; _logger = logger; } /// /// Whether the export is usable and where it points, so the panel can render itself and /// disable the button when the server is not set up. Carries no secret and no key path. /// [HttpGet("Config")] [ProducesResponseType(typeof(SheetsConfigDTO), StatusCodes.Status200OK)] public IActionResult GetConfig() { return Ok(_mapper.Map(_service.GetConfig())); } /// /// Writes one tab per table, in the order given — oldest event first, each tab holding the /// cumulative result up to and including its own event. /// /// Existing tabs of the same name are cleared and rewritten in place, keeping their /// formatting; anything typed into them by hand is lost. Tabs not named in the request are /// never touched or deleted. /// [HttpPost("Export")] [ProducesResponseType(typeof(SheetExportResultDTO), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status502BadGateway)] [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] public async Task Export([FromBody] SheetExportRequestDTO dto, CancellationToken ct) { if (dto?.Tabs == null || dto.Tabs.Count == 0) return BadRequest(new { message = "No table to write." }); var request = _mapper.Map(dto); try { var result = await _service.ExportAsync(request, ct); _logger.LogInformation( "Sheets export by user {User}: {TabCount} tabs to spreadsheet {SpreadsheetId} via {Writer}", User?.Identity?.Name, result.Tabs?.Count ?? 0, result.SpreadsheetId, result.Writer); return Ok(_mapper.Map(result)); } catch (SheetsExportException ex) { _logger.LogWarning(ex, "Sheets export refused ({StatusCode})", ex.StatusCode); return StatusCode(ex.StatusCode, new { message = ex.Message }); } catch (OperationCanceledException) { // The caller navigated away or hit cancel; nothing to report to them. throw; } catch (Exception ex) { // Anything unforeseen still has to arrive as JSON with a message. _logger.LogError(ex, "Sheets export failed"); return StatusCode(StatusCodes.Status502BadGateway, new { message = ex.Message }); } } } }