using System.Linq.Expressions; using DeepDrftData.Repositories; using DeepDrftModels.DTOs; using DeepDrftModels.Entities; using DeepDrftModels.Enums; using Microsoft.Extensions.Logging; using Models.Common; using NetBlocks.Models; namespace DeepDrftData; /// /// SQL-side release service implementing . Deliberately does NOT extend /// Manager<>: that CRUD base does not fit this read-projection + satellite-write purpose. /// The layer boundary holds — ReleaseRepository outputs entities, this manager outputs DTOs via /// TrackConverter, the single authoritative conversion path. /// public class ReleaseManager : IReleaseService { // Distinguishes "release does not exist" from a real failure so the controller can map to 404. public const string ReleaseNotFoundMessage = "Release not found."; private readonly ReleaseRepository _repository; private readonly ILogger _logger; public ReleaseManager(ReleaseRepository repository, ILogger logger) { _repository = repository; _logger = logger; } // Nulls sort to end via the coalescing sentinels, matching TrackManager's convention. private static Expression> GetOrderExpression(string? sortColumn) => sortColumn switch { "Title" => r => r.Title, "Artist" => r => r.Artist, "ReleaseDate" => r => (object)(r.ReleaseDate ?? DateOnly.MaxValue), "Medium" => r => r.Medium, _ => r => r.Id }; public async Task>> GetPagedAsync( int page, int pageSize, string? sortColumn, bool sortDescending, ReleaseMedium? medium, ReleaseFilter? filter = null, CancellationToken cancellationToken = default) { try { var parameters = new PagingParameters { Page = page, PageSize = pageSize, OrderBy = GetOrderExpression(sortColumn), IsDescending = sortDescending, }; // Collapse an all-null filter to null so the repository skips the predicate block entirely. var effectiveFilter = filter is { IsEmpty: false } ? filter : null; var entityPage = await _repository.GetPagedByMediumAsync(parameters, medium, effectiveFilter, cancellationToken); var releaseIds = entityPage.Items.Select(r => r.Id).ToList(); var counts = await _repository.GetTrackCountsByReleaseIdsAsync(releaseIds, cancellationToken); var dtos = entityPage.Items .Select(r => { var dto = TrackConverter.Convert(r); dto.TrackCount = counts.GetValueOrDefault(r.Id); return dto; }); var dtoPage = PagedResult.From(entityPage, dtos); return ResultContainer>.CreatePassResult(dtoPage); } catch (Exception e) { return ResultContainer>.CreateFailResult(e.Message); } } public async Task> GetByIdAsync(long id, CancellationToken cancellationToken = default) { try { var entity = await _repository.GetByIdWithMetadataAsync(id, cancellationToken); // TrackConverter nulls the non-matching satellite. TrackCount is not loaded for the detail // read (the Tracks collection isn't Include'd) and is not needed by detail consumers. return ResultContainer.CreatePassResult( entity is null ? null : TrackConverter.Convert(entity)); } catch (Exception e) { return ResultContainer.CreateFailResult(e.Message); } } public async Task> GetByEntryKeyAsync(string entryKey, CancellationToken cancellationToken = default) { try { var entity = await _repository.GetByEntryKeyWithMetadataAsync(entryKey, cancellationToken); return ResultContainer.CreatePassResult( entity is null ? null : TrackConverter.Convert(entity)); } catch (Exception e) { return ResultContainer.CreateFailResult(e.Message); } } public async Task>> GetTrackEntryKeysAsync(long releaseId, CancellationToken cancellationToken = default) { try { var keys = await _repository.GetTrackEntryKeysByReleaseIdAsync(releaseId, cancellationToken); return ResultContainer>.CreatePassResult(keys); } catch (Exception e) { return ResultContainer>.CreateFailResult(e.Message); } } public async Task SetSessionHeroImageAsync(long releaseId, string heroImageEntryKey, CancellationToken cancellationToken = default) { try { var release = await _repository.GetByIdWithMetadataAsync(releaseId, cancellationToken); if (release is null) return Result.CreateFailResult(ReleaseNotFoundMessage); if (release.Medium != ReleaseMedium.Session) return Result.CreateFailResult($"Release {releaseId} is not a Session medium."); await _repository.SetHeroImageEntryKeyAsync(releaseId, heroImageEntryKey, cancellationToken); return Result.CreatePassResult(); } catch (Exception e) { return Result.CreateFailResult(e.Message); } } public async Task SetMixWaveformAsync(long releaseId, string waveformEntryKey, CancellationToken cancellationToken = default) { try { var release = await _repository.GetByIdWithMetadataAsync(releaseId, cancellationToken); if (release is null) return Result.CreateFailResult(ReleaseNotFoundMessage); if (release.Medium != ReleaseMedium.Mix) return Result.CreateFailResult($"Release {releaseId} is not a Mix medium."); await _repository.SetWaveformEntryKeyAsync(releaseId, waveformEntryKey, cancellationToken); return Result.CreatePassResult(); } catch (Exception e) { return Result.CreateFailResult(e.Message); } } }