using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Channels; using ErsatzTV.Core; using ErsatzTV.Core.Errors; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Channel = ErsatzTV.Core.Domain.Channel; namespace ErsatzTV.Application.Channels; public class UpdateChannelNumbersHandler( IDbContextFactory dbContextFactory, ChannelWriter workerChannel) : IRequestHandler> { public async Task> Handle(UpdateChannelNumbers request, CancellationToken cancellationToken) { Option validationError = ValidateRequest(request); if (validationError.IsSome) { return validationError; } await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); try { var numberUpdates = request.Channels.ToDictionary(c => c.Id, c => c.Number); List channelIds = numberUpdates.Keys.ToList(); List channelsToUpdate = await dbContext.Channels .Where(c => channelIds.Contains(c.Id)) .ToListAsync(cancellationToken); if (channelsToUpdate.Count != channelIds.Count) { var found = channelsToUpdate.Select(c => c.Id).ToHashSet(); int missingId = channelIds.First(id => !found.Contains(id)); return new NotFoundError($"Channel {missingId} does not exist."); } List requestedNumbers = numberUpdates.Values.ToList(); bool numberConflict = await dbContext.Channels .AnyAsync( c => requestedNumbers.Contains(c.Number) && !channelIds.Contains(c.Id), cancellationToken); if (numberConflict) { return BaseError.New("Channel number must be unique"); } // give every channel a non-conflicting number foreach (var channel in channelsToUpdate) { channel.Number = $"-{channel.Id}"; } // save those changes await dbContext.SaveChangesAsync(cancellationToken); // give every channel the proper new number foreach (var channel in channelsToUpdate) { channel.Number = numberUpdates[channel.Id]; if (double.TryParse(channel.Number, CultureInfo.InvariantCulture, out double sortNumber)) { channel.SortNumber = sortNumber; } else { return BaseError.New($"Failed to parse channel number {channel.Number}"); } } // save those changes await dbContext.SaveChangesAsync(cancellationToken); // commit the transaction await transaction.CommitAsync(cancellationToken); // update channel list and xmltv await workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken); foreach (var channel in channelsToUpdate) { await workerChannel.WriteAsync(new RefreshChannelData(channel.Number), cancellationToken); } return Option.None; } catch (Exception ex) { return BaseError.New("Failed to update channel numbers: " + ex.Message); } } private static Option ValidateRequest(UpdateChannelNumbers request) { if (request.Channels.Count == 0) { return BaseError.New("At least one channel is required"); } if (request.Channels.Select(c => c.Id).Distinct().Count() != request.Channels.Count) { return BaseError.New("Channel ids must be unique"); } if (request.Channels.Select(c => c.Number).Distinct(StringComparer.Ordinal).Count() != request.Channels.Count) { return BaseError.New("Channel number must be unique"); } foreach (ChannelSortViewModel channel in request.Channels) { if (!Regex.IsMatch(channel.Number, Channel.NumberValidator)) { return BaseError.New("Invalid channel number; two decimals are allowed for subchannels"); } } return Option.None; } }