using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Application.MediaCollections;

public class RenamePlaylistGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
    : IRequestHandler<RenamePlaylistGroup, Either<BaseError, PlaylistGroupViewModel>>
{
    public async Task<Either<BaseError, PlaylistGroupViewModel>> Handle(
        RenamePlaylistGroup request,
        CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
        Validation<BaseError, PlaylistGroup> validation = await Validate(dbContext, request, cancellationToken);
        return await validation.Apply(playlistGroup => Persist(dbContext, request, playlistGroup));
    }

    private static async Task<PlaylistGroupViewModel> Persist(
        TvContext dbContext,
        RenamePlaylistGroup request,
        PlaylistGroup playlistGroup)
    {
        playlistGroup.Name = request.Name;
        await dbContext.SaveChangesAsync();
        return Mapper.ProjectToViewModel(playlistGroup);
    }

    private static Task<Validation<BaseError, PlaylistGroup>> Validate(
        TvContext dbContext,
        RenamePlaylistGroup request,
        CancellationToken cancellationToken) =>
        PlaylistGroupMustExist(dbContext, request, cancellationToken)
            .BindT(PlaylistGroupMustNotBeSystem)
            .BindT(playlistGroup => ValidateName(request).Map(_ => playlistGroup))
            .BindT(playlistGroup => NameMustBeUnique(dbContext, request, playlistGroup));

    private static Task<Validation<BaseError, PlaylistGroup>> PlaylistGroupMustExist(
        TvContext dbContext,
        RenamePlaylistGroup request,
        CancellationToken cancellationToken) =>
        dbContext.PlaylistGroups
            .Include(pg => pg.Playlists)
            .SelectOneAsync(pg => pg.Id, pg => pg.Id == request.PlaylistGroupId, cancellationToken)
            .Map(o => o.ToValidation<BaseError>(
                new NotFoundError($"PlaylistGroup {request.PlaylistGroupId} does not exist.")));

    // Plain BaseError (NOT NotFoundError) so it maps to 422, mirroring DeletePlaylistGroupHandler's
    // system-group guard. A missing group still surfaces as NotFoundError (404) from PlaylistGroupMustExist.
    private static Validation<BaseError, PlaylistGroup> PlaylistGroupMustNotBeSystem(PlaylistGroup playlistGroup) =>
        playlistGroup.IsSystem
            ? BaseError.New("Cannot rename system playlist group")
            : playlistGroup;

    private static Validation<BaseError, string> ValidateName(RenamePlaylistGroup request) =>
        request.NotEmpty(x => x.Name)
            .Bind(_ => request.NotLongerThan(50)(x => x.Name));

    // Issue #458: PlaylistGroup.Name carries a global unique index, but CreatePlaylistGroupHandler
    // has no explicit duplicate guard (it relies on the DB constraint). Add one on rename so a
    // collision surfaces as a clean 422 rather than a raw DbUpdateException. Excludes the group
    // itself so a no-op rename to its own name still succeeds.
    private static async Task<Validation<BaseError, PlaylistGroup>> NameMustBeUnique(
        TvContext dbContext,
        RenamePlaylistGroup request,
        PlaylistGroup playlistGroup)
    {
        Option<PlaylistGroup> maybeExisting = await dbContext.PlaylistGroups
            .AsNoTracking()
            .FirstOrDefaultAsync(pg => pg.Id != request.PlaylistGroupId && pg.Name == request.Name)
            .Map(Optional);

        return maybeExisting.IsSome
            ? BaseError.New($"A playlist group named \"{request.Name}\" already exists")
            : Success<BaseError, PlaylistGroup>(playlistGroup);
    }
}
