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

namespace ErsatzTV.Application.Watermarks;

public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<BaseError, UpdateWatermarkResult>>
{
    private readonly IDbContextFactory<TvContext> _dbContextFactory;
    private readonly ISearchTargets _searchTargets;

    public UpdateWatermarkHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
    {
        _dbContextFactory = dbContextFactory;
        _searchTargets = searchTargets;
    }

    public async Task<Either<BaseError, UpdateWatermarkResult>> Handle(
        UpdateWatermark request,
        CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
        Option<ChannelWatermark> maybeWatermark = await WatermarkMustExist(dbContext, request, cancellationToken);

        // must-exist maps to a NotFoundError Either directly (not via Validation, which
        // aggregates errors and loses the subtype the API layer maps to 404)
        return await maybeWatermark.Match(
            Some: async watermark =>
            {
                Validation<BaseError, string> validation = await ValidateName(dbContext, request);
                return await validation.Apply((string _) => ApplyUpdateRequest(dbContext, watermark, request));
            },
            None: () => Task.FromResult<Either<BaseError, UpdateWatermarkResult>>(
                new NotFoundError("Watermark does not exist.")));
    }

    private async Task<UpdateWatermarkResult> ApplyUpdateRequest(
        TvContext dbContext,
        ChannelWatermark p,
        UpdateWatermark update)
    {
        p.Name = update.Name;

        p.Image = null;
        p.OriginalContentType = null;
        if (update.ImageSource == ChannelWatermarkImageSource.Custom)
        {
            p.Image = update.Image?.Path;
            p.OriginalContentType = update.Image?.ContentType;
        }

        p.Mode = update.Mode;
        p.ImageSource = update.ImageSource;
        p.Location = update.Location;
        p.Size = update.Size;
        p.WidthPercent = update.Width;
        p.HorizontalMarginPercent = update.HorizontalMargin;
        p.VerticalMarginPercent = update.VerticalMargin;
        p.FrequencyMinutes = update.FrequencyMinutes;
        p.DurationSeconds = update.DurationSeconds;
        p.Opacity = update.Opacity;
        p.PlaceWithinSourceContent = update.PlaceWithinSourceContent;
        p.OpacityExpression = update.Mode is ChannelWatermarkMode.OpacityExpression ? update.OpacityExpression : null;
        p.ZIndex = update.ZIndex;

        await dbContext.SaveChangesAsync();
        _searchTargets.SearchTargetsChanged();
        return new UpdateWatermarkResult(p.Id);
    }

    private static Task<Option<ChannelWatermark>> WatermarkMustExist(
        TvContext dbContext,
        UpdateWatermark updateWatermark,
        CancellationToken cancellationToken) =>
        dbContext.ChannelWatermarks
            .SelectOneAsync(p => p.Id, p => p.Id == updateWatermark.Id, cancellationToken);

    private static async Task<Validation<BaseError, string>> ValidateName(
        TvContext dbContext,
        UpdateWatermark updateWatermark)
    {
        bool duplicateName = await dbContext.ChannelWatermarks
            .AnyAsync(wm => wm.Id != updateWatermark.Id && wm.Name == updateWatermark.Name);

        Validation<BaseError, Unit> result2 = duplicateName
            ? Fail<BaseError, Unit>("ChannelWatermark name must be unique")
            : Success<BaseError, Unit>(Unit.Default);

        Validation<BaseError, string> result1 = updateWatermark.NotEmpty(c => c.Name)
            .Bind(_ => updateWatermark.NotLongerThan(50)(c => c.Name));

        return (result1, result2).Apply((_, _) => updateWatermark.Name);
    }
}
