using JdeScoping.Api.Hubs; using JdeScoping.Core.Interfaces; using JdeScoping.Core.Models; using JdeScoping.Core.Models.Enums; using JdeScoping.Core.Models.Search; using JdeScoping.Core.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace JdeScoping.Api.Controllers; /// /// Search management controller /// [Route("api/search")] [ApiController] [Authorize] public class SearchController : ApiControllerBase { private readonly ILotFinderRepository _repository; private readonly IHubContext _hubContext; private readonly ILogger _logger; public SearchController( ILotFinderRepository repository, IHubContext hubContext, ILogger logger) { _repository = repository; _hubContext = hubContext; _logger = logger; } /// /// Gets all searches for the current user /// [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetSearches(CancellationToken ct) { var searches = await _repository.GetUserSearchesAsync(CurrentUserName!, ct); var viewModels = searches .OrderByDescending(s => s.StartDt) .Select(s => new SearchViewModel(s)); return Ok(viewModels); } /// /// Gets all queued searches /// [HttpGet("queue")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetQueuedSearches(CancellationToken ct) { var searches = await _repository.GetQueuedSearchesAsync(ct); var viewModels = searches.Select(s => new SearchViewModel(s)); return Ok(viewModels); } /// /// Gets a single search by ID /// [HttpGet("{id:int}")] [ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetSearch(int id, CancellationToken ct) { var search = await _repository.GetSearchAsync(id, ct); if (search is null) { return NotFound(); } return Ok(new SearchViewModel(search)); } /// /// Copies an existing search for the current user (returns copy without persisting) /// [HttpGet("{id:int}/copy")] [ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> CopySearch(int id, CancellationToken ct) { var original = await _repository.GetSearchAsync(id, ct); if (original is null) { return NotFound(); } // Return a copy with reset status/timestamps (not persisted until user submits) var copy = new Search { Id = 0, UserName = CurrentUserName!, Name = original.Name, Status = SearchStatus.New, SubmitDt = null, StartDt = null, EndDt = null, CriteriaJson = original.CriteriaJson }; return Ok(new SearchViewModel(copy)); } /// /// Creates a new search /// [HttpPost] [ProducesResponseType(typeof(int), StatusCodes.Status201Created)] public async Task> CreateSearch( [FromBody] SearchViewModel viewModel, CancellationToken ct) { var search = viewModel.ToEntity(); search.UserName = CurrentUserName!; var searchId = await _repository.SubmitSearchAsync(search, ct); // Publish to SignalR (best-effort, swallow exceptions) try { var searchUpdate = new SearchUpdate { Id = searchId, UserName = CurrentUserName!, Name = search.Name, Status = search.Status, Timestamp = DateTime.UtcNow }; await _hubContext.Clients.All.SendAsync("searchUpdate", searchUpdate, ct); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to publish search update to SignalR"); } return CreatedAtAction(nameof(GetSearch), new { id = searchId }, searchId); } /// /// Downloads search results as an Excel file /// [HttpGet("{id:int}/results")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetResults(int id, CancellationToken ct) { var data = await _repository.GetSearchResultsAsync(id, ct); if (data is null || data.Length == 0) { return NotFound(); } return File( data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "search_results.xlsx"); } }