using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ErsatzTV.Infrastructure.Scheduling;

public class PlayoutTimeShifter(
    IDbContextFactory<TvContext> dbContextFactory,
    IFFmpegSegmenterService segmenterService,
    ILogger<PlayoutTimeShifter> logger)
    : IPlayoutTimeShifter
{
    public async Task<List<string>> TimeShift(
        int playoutId,
        DateTimeOffset now,
        bool force,
        CancellationToken cancellationToken)
    {
        await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);

        Option<Channel> maybeChannel = await dbContext.Playouts
            .AsNoTracking()
            .Include(p => p.Channel)
            .SelectOneAsync(p => p.Id, p => p.Id == playoutId, cancellationToken)
            .MapT(p => p.Channel);

        foreach (Channel channel in maybeChannel.Where(c => c.PlayoutMode is ChannelPlayoutMode.OnDemand))
        {
            Option<Playout> maybePlayout = await dbContext.Playouts
                .Include(p => p.Channel)
                .Include(p => p.Items)
                .Include(p => p.Anchor)
                .Include(p => p.ProgramScheduleAnchors)
                .Include(p => p.PlayoutHistory)
                .SelectOneAsync(p => p.ChannelId, p => p.ChannelId == channel.Id, cancellationToken);

            foreach (Playout playout in maybePlayout)
            {
                if (playout.Channel.PlayoutMode is not ChannelPlayoutMode.OnDemand)
                {
                    return [];
                }

                if (!force && segmenterService.IsActive(playout.Channel.Number))
                {
                    logger.LogDebug(
                        "Will not time shift on demand playout that is active for channel {Number} - {Name}",
                        playout.Channel.Number,
                        playout.Channel.Name);

                    return [];
                }

                if (playout.Items.Count == 0)
                {
                    logger.LogDebug(
                        "Unable to time shift empty playout for channel {Number} - {Name}",
                        playout.Channel.Number,
                        playout.Channel.Name);

                    return [];
                }

                if (playout.OnDemandCheckpoint is null)
                {
                    logger.LogDebug(
                        "Time shifting unwatched playout for channel {Number} - {Name}",
                        playout.Channel.Number,
                        playout.Channel.Name);

                    playout.OnDemandCheckpoint = playout.Items.Min(p => p.StartOffset);
                }

                TimeSpan toOffset = now - playout.OnDemandCheckpoint.IfNone(now);

                logger.LogDebug(
                    "Time shifting playout for channel {Number} - {Name} forward by {Time}",
                    playout.Channel.Number,
                    playout.Channel.Name,
                    toOffset);

                // time shift history
                foreach (PlayoutHistory history in playout.PlayoutHistory)
                {
                    history.When += toOffset;
                    history.Finish += toOffset;
                }

                // time shift items
                foreach (PlayoutItem playoutItem in playout.Items)
                {
                    playoutItem.Start += toOffset;
                    playoutItem.Finish += toOffset;

                    if (playoutItem.GuideStart.HasValue)
                    {
                        playoutItem.GuideStart += toOffset;
                    }

                    if (playoutItem.GuideFinish.HasValue)
                    {
                        playoutItem.GuideFinish += toOffset;
                    }
                }

                // time shift anchors
                foreach (PlayoutProgramScheduleAnchor anchor in playout.ProgramScheduleAnchors)
                {
                    if (anchor.AnchorDate.HasValue)
                    {
                        anchor.AnchorDate += toOffset;
                    }
                }

                // time shift anchor
                if (playout.Anchor is not null)
                {
                    playout.Anchor.NextStart += toOffset;
                }

                playout.OnDemandCheckpoint = now;

                await dbContext.SaveChangesAsync(cancellationToken);

                // a non-zero shift moved every PlayoutItem (and its guide window), so the cached
                // XMLTV fragment for this channel is now stale; report the channel number so the
                // caller can rebuild the guide and keep it in sync with playback
                if (toOffset != TimeSpan.Zero)
                {
                    // mirror channels relay this channel's timeline (shifted by their PlayoutOffset),
                    // so their cached guides are stale too — refresh them alongside, mirroring the
                    // fan-out BuildPlayoutHandler already does after it time-shifts
                    List<string> mirrorNumbers = await dbContext.Channels
                        .AsNoTracking()
                        .Filter(c => c.MirrorSourceChannelId == channel.Id)
                        .Map(c => c.Number)
                        .ToListAsync(cancellationToken);

                    return [playout.Channel.Number, .. mirrorNumbers];
                }
            }
        }

        return [];
    }
}
