using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;

namespace ErsatzTV.Services.RunOnce;

/// <summary>
///     One-time startup migration that downloads existing external-URL channel logos into the image
///     cache. Before ersatztv#525 a channel logo could be stored as a raw http(s) URL in
///     <see cref="Artwork.Path" />; the render path used to fetch it live. Now that URLs are cached on
///     save, these legacy rows are converted here. A download failure leaves the row untouched and logs
///     a warning naming the URL — re-saving the channel fixes it. Idempotent: a converted row's Path is
///     a bare cache name, so a second run selects nothing.
/// </summary>
public class ExternalLogoMigratorService(
    IServiceScopeFactory serviceScopeFactory,
    ILogger<ExternalLogoMigratorService> logger,
    SystemStartup systemStartup)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Yield();

        await systemStartup.WaitForDatabase(stoppingToken);
        if (stoppingToken.IsCancellationRequested)
        {
            return;
        }

        logger.LogInformation("Migrating external URL channel logos to the image cache");

        try
        {
            using IServiceScope scope = serviceScopeFactory.CreateScope();
            await using TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
            IRemoteLogoCacher cacher = scope.ServiceProvider.GetRequiredService<IRemoteLogoCacher>();

            await MigrateAsync(dbContext, cacher, logger, stoppingToken);

            logger.LogInformation("Done migrating external URL channel logos to the image cache");
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // shutdown mid-migration — the single trailing SaveChangesAsync never ran, so no partial
            // persist; the next boot retries idempotently.
        }
        catch (Exception ex)
        {
            // this is a run-once BackgroundService: an escaping exception trips the default
            // StopHost behavior and kills the app. A logo migration must never do that — the fetch
            // races (e.g. a channel deleted mid-run -> DbUpdateConcurrencyException) are transient
            // and self-heal on the next boot. Log and let the host keep serving.
            logger.LogError(ex, "Failed migrating external URL channel logos to the image cache; will retry next start");
        }
    }

    internal static async Task MigrateAsync(
        TvContext db,
        IRemoteLogoCacher cacher,
        ILogger logger,
        CancellationToken cancellationToken)
    {
        // IsExternalUrl is a C# predicate EF cannot translate, so load logo artwork then filter in memory.
        List<Artwork> logos = await db.Artwork
            .Where(a => a.ArtworkKind == ArtworkKind.Logo)
            .ToListAsync(cancellationToken);

        foreach (Artwork artwork in logos.Where(a => a.IsExternalUrl()))
        {
            string oldUrl = artwork.Path;
            Either<BaseError, string> result = await cacher.CacheFromUrl(new Uri(oldUrl), cancellationToken);
            result.Match(
                name =>
                {
                    artwork.Path = name;
                    artwork.DateUpdated = DateTime.UtcNow;
                },
                error => logger.LogWarning(
                    "Could not download existing channel logo {Url}; leaving it. Re-save the channel to fix. ({Error})",
                    oldUrl,
                    error.Value));
        }

        await db.SaveChangesAsync(cancellationToken);
    }
}
