using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Domain;

namespace Tornado3_2026Election.Services;

public sealed class ChannelScheduleEngine
{
    private const int PreviewFrame = -1;
    private static readonly TimeSpan MinimumNextPreviewWindow = TimeSpan.FromSeconds(2.5);
    private readonly ITornado3Adapter _adapter;
    private readonly IDataRefreshGate _dataRefreshGate;
    private readonly Func<BroadcastStationProfile> _stationProvider;
    private readonly Func<string> _imageRootProvider;
    private readonly Func<VideoWallLayoutPreset> _videoWallLayoutPresetProvider;
    private readonly Func<string, FormatTemplateDefinition?> _templateResolver;
    private readonly LogService _logService;
    private readonly SemaphoreSlim _executionLock = new(1, 1);
    private CancellationTokenSource? _playbackCts;
    private TaskCompletionSource<bool>? _advanceSignal;
    private Guid? _lastPlaybackItemId;
    private Guid? _skipCurrentItemId;
    private ChannelScheduleItem? _directPlaybackItem;
    private PreparedCutFrame? _preparedCutFrame;

    public ChannelScheduleEngine(
        BroadcastChannel channel,
        ObservableCollection<ChannelScheduleItem> queue,
        ITornado3Adapter adapter,
        IDataRefreshGate dataRefreshGate,
        Func<BroadcastStationProfile> stationProvider,
        Func<string> imageRootProvider,
        Func<VideoWallLayoutPreset> videoWallLayoutPresetProvider,
        Func<string, FormatTemplateDefinition?> templateResolver,
        LogService logService)
    {
        Channel = channel;
        Queue = queue;
        _adapter = adapter;
        _dataRefreshGate = dataRefreshGate;
        _stationProvider = stationProvider;
        _imageRootProvider = imageRootProvider;
        _videoWallLayoutPresetProvider = videoWallLayoutPresetProvider;
        _templateResolver = templateResolver;
        _logService = logService;
    }

    public BroadcastChannel Channel { get; }

    public ObservableCollection<ChannelScheduleItem> Queue { get; }

    public bool LoopEnabled { get; set; }

    public EmptyScheduleBehavior EmptyScheduleBehavior { get; set; } = EmptyScheduleBehavior.ImmediateOut;

    public bool IsRunning { get; private set; }

    public ChannelScheduleItem? ActivePlaybackItem =>
        _directPlaybackItem?.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending
            ? _directPlaybackItem
            : Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending);

    public event EventHandler? QueueChanged;

    public bool IsPreparedItem(ChannelScheduleItem item)
    {
        return _preparedCutFrame?.Item.Id == item.Id;
    }

    public async Task StartAsync()
    {
        if (IsRunning)
        {
            await AdvanceToNextAsync().ConfigureAwait(false);
            return;
        }

        if (_preparedCutFrame is { Item: var preparedItem } && !Queue.Contains(preparedItem))
        {
            ClearPreparedFrame(resetState: true);
        }

        _lastPlaybackItemId = null;
        _playbackCts = new CancellationTokenSource();
        IsRunning = true;
        RefreshQueueMarkers();
        _ = RunAsync(_playbackCts.Token);
        await Task.CompletedTask;
    }

    public async Task StopAsync(bool takeOutputOff = true)
    {
        if (!IsRunning)
        {
            if (_preparedCutFrame is null)
            {
                return;
            }

            if (takeOutputOff)
            {
                await _adapter.OutAsync(Channel, CancellationToken.None).ConfigureAwait(false);
            }

            ClearPreparedFrame(resetState: true);
            _lastPlaybackItemId = null;
            _skipCurrentItemId = null;
            RefreshQueueMarkers();
            QueueChanged?.Invoke(this, EventArgs.Empty);
            return;
        }

        _playbackCts?.Cancel();
        _advanceSignal?.TrySetResult(true);
        if (takeOutputOff)
        {
            await _adapter.OutAsync(Channel, CancellationToken.None).ConfigureAwait(false);
        }

        foreach (var item in Queue.Where(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending))
        {
            item.State = ScheduleQueueItemState.Queued;
            item.CurrentRegionLabel = string.Empty;
            item.ClearRenderedPreview();
            item.ClearInternalNextPreview();
        }

        _lastPlaybackItemId = null;
        _skipCurrentItemId = null;
        ClearPreparedFrame(resetState: false);
        IsRunning = false;
        RefreshQueueMarkers();
        QueueChanged?.Invoke(this, EventArgs.Empty);
    }

    public async Task PrepareNextAsync(CancellationToken cancellationToken)
    {
        if (IsRunning)
        {
            return;
        }

        await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            _lastPlaybackItemId = null;
            ClearPreparedFrame(resetState: true);
            RefreshQueueMarkers();

            var next = GetNextPlayableItem();
            if (next is null)
            {
                _logService.Warning($"[{Channel}] 준비할 스케줄 컷이 없습니다.");
                return;
            }

            var template = _templateResolver(next.FormatId);
            if (template is null)
            {
                next.State = ScheduleQueueItemState.Error;
                next.LastError = "포맷을 찾을 수 없습니다.";
                _logService.Error($"[{Channel}] Missing template: {next.FormatId}");
                RefreshQueueMarkers();
                return;
            }

            await PrepareFirstCutAsync(next, template, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _executionLock.Release();
            QueueChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    public async Task PrepareDirectAsync(
        ChannelScheduleItem item,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            ClearPreparedFrame(resetState: true);
            _directPlaybackItem = item;
            QueueChanged?.Invoke(this, EventArgs.Empty);
            await PrepareFirstCutAsync(item, template, cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            if (_directPlaybackItem == item)
            {
                _directPlaybackItem = null;
            }

            item.State = ScheduleQueueItemState.Queued;
            item.CurrentRegionLabel = string.Empty;
            throw;
        }
        finally
        {
            _executionLock.Release();
            QueueChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    public void ClearDirectPlayback()
    {
        ClearPreparedFrame(resetState: true);
        if (_directPlaybackItem is not null)
        {
            _directPlaybackItem.State = ScheduleQueueItemState.Queued;
            _directPlaybackItem.CurrentRegionLabel = string.Empty;
            _directPlaybackItem.ClearRenderedPreview();
            _directPlaybackItem.ClearInternalNextPreview();
            _directPlaybackItem = null;
        }

        QueueChanged?.Invoke(this, EventArgs.Empty);
    }

    public async Task PlayDirectAsync(
        ChannelScheduleItem item,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            _directPlaybackItem = item;
            QueueChanged?.Invoke(this, EventArgs.Empty);
            await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);
            await PlayItemAsync(item, template, cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            item.State = ScheduleQueueItemState.Queued;
            item.CurrentRegionLabel = string.Empty;
        }
        finally
        {
            _directPlaybackItem = null;
            _executionLock.Release();
            QueueChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    public void Reset()
    {
        _lastPlaybackItemId = null;
        ClearPreparedFrame(resetState: false);
        foreach (var item in Queue)
        {
            item.State = ScheduleQueueItemState.Queued;
            item.LastError = string.Empty;
            item.CurrentRegionLabel = string.Empty;
            item.ClearRenderedPreview();
            item.ClearInternalNextPreview();
        }

        RefreshQueueMarkers();
    }

    public async Task ForceNextAsync()
    {
        await AdvanceToNextAsync().ConfigureAwait(false);
    }

    public async Task ForceQueueNextAsync()
    {
        await ForceNextAsync().ConfigureAwait(false);
    }

    public Task AdvanceToNextAsync()
    {
        if (!IsRunning)
        {
            return Task.CompletedTask;
        }

        var activeItem = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending);
        if (activeItem is not null)
        {
            _skipCurrentItemId = activeItem.Id;
            _lastPlaybackItemId = activeItem.Id;
            activeItem.State = ScheduleQueueItemState.Queued;
            activeItem.LastError = string.Empty;
            activeItem.CurrentRegionLabel = string.Empty;
            activeItem.ClearInternalNextPreview();
        }

        RefreshQueueMarkers();
        QueueChanged?.Invoke(this, EventArgs.Empty);
        _advanceSignal?.TrySetResult(true);
        return Task.CompletedTask;
    }

    public bool Remove(ChannelScheduleItem? item)
    {
        if (item is null || !item.CanDelete)
        {
            return false;
        }

        var removed = Queue.Remove(item);
        if (removed)
        {
            if (_lastPlaybackItemId == item.Id)
            {
                _lastPlaybackItemId = null;
            }

            RefreshQueueMarkers();
        }

        return removed;
    }

    public void Enqueue(ChannelScheduleItem item)
    {
        item.State = ScheduleQueueItemState.Queued;
        Queue.Add(item);
        RefreshQueueMarkers();
    }

    public bool MoveUp(ChannelScheduleItem? item) => Move(item, -1);

    public bool MoveDown(ChannelScheduleItem? item) => Move(item, 1);

    public bool PromoteToNext(ChannelScheduleItem? item)
    {
        if (item is null)
        {
            return false;
        }

        var anchorItem = ActivePlaybackItem ?? GetLastPlaybackItem();
        var targetIndex = anchorItem is null ? 0 : Queue.IndexOf(anchorItem) + 1;
        var currentIndex = Queue.IndexOf(item);
        if (currentIndex < 0 || targetIndex < 0 || targetIndex > Queue.Count || currentIndex == targetIndex)
        {
            return false;
        }

        if (currentIndex < targetIndex)
        {
            targetIndex--;
        }

        Queue.Move(currentIndex, targetIndex);
        RefreshQueueMarkers();
        return true;
    }

    private bool Move(ChannelScheduleItem? item, int delta)
    {
        if (item is null)
        {
            return false;
        }

        var currentIndex = Queue.IndexOf(item);
        var targetIndex = currentIndex + delta;
        if (currentIndex < 0 || targetIndex < 0 || targetIndex >= Queue.Count)
        {
            return false;
        }

        Queue.Move(currentIndex, targetIndex);
        RefreshQueueMarkers();
        return true;
    }

    private async Task RunAsync(CancellationToken cancellationToken)
    {
        await _executionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                var next = GetNextPlayableItem();
                if (next is null)
                {
                    if (EmptyScheduleBehavior == EmptyScheduleBehavior.ImmediateOut)
                    {
                        await _adapter.OutAsync(Channel, cancellationToken).ConfigureAwait(false);
                    }

                    IsRunning = false;
                    RefreshQueueMarkers();
                    QueueChanged?.Invoke(this, EventArgs.Empty);
                    return;
                }

                await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);

                var template = _templateResolver(next.FormatId);
                if (template is null)
                {
                    next.State = ScheduleQueueItemState.Error;
                    next.LastError = "포맷을 찾을 수 없습니다.";
                    _lastPlaybackItemId = next.Id;
                    _logService.Error($"[{Channel}] Missing template: {next.FormatId}");
                    RefreshQueueMarkers();
                    continue;
                }

                await PlayItemAsync(next, template, cancellationToken).ConfigureAwait(false);
                QueueChanged?.Invoke(this, EventArgs.Empty);
            }
        }
        catch (OperationCanceledException)
        {
            IsRunning = false;
            RefreshQueueMarkers();
        }
        catch (Exception ex)
        {
            IsRunning = false;
            var sendingItem = Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Sending);
            if (sendingItem is not null)
            {
                sendingItem.State = ScheduleQueueItemState.Error;
                sendingItem.LastError = ex.Message;
                sendingItem.CurrentRegionLabel = string.Empty;
                sendingItem.ClearInternalNextPreview();
                ClearSkipCurrentItem(sendingItem);
            }

            _logService.Error($"[{Channel}] Schedule playback stopped: {ex.Message}");
            RefreshQueueMarkers();
            QueueChanged?.Invoke(this, EventArgs.Empty);
        }
        finally
        {
            _executionLock.Release();
        }
    }

    private async Task PrepareFirstCutAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        queueItem.State = ScheduleQueueItemState.Sending;
        queueItem.LastError = string.Empty;
        queueItem.ClearInternalNextPreview();
        RefreshQueueMarkers();
        QueueChanged?.Invoke(this, EventArgs.Empty);

        var station = _stationProvider();
        var imageRootPath = _imageRootProvider();
        CutPreviewFrame? previewFrame;
        try
        {
            previewFrame = await TryBuildPreviewFrameAsync(queueItem, template, station, imageRootPath, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            queueItem.State = ScheduleQueueItemState.Error;
            queueItem.LastError = ex.Message;
            queueItem.CurrentRegionLabel = string.Empty;
            RefreshQueueMarkers();
            _logService.Warning($"[{Channel}] 준비 컷 데이터 구성 실패: {queueItem.DisplayName} / {ex.Message}");
            return;
        }

        if (previewFrame is null)
        {
            queueItem.State = ScheduleQueueItemState.Error;
            queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
            queueItem.CurrentRegionLabel = string.Empty;
            RefreshQueueMarkers();
            _logService.Warning($"[{Channel}] 준비할 수 있는 컷 데이터가 없습니다: {queueItem.DisplayName}");
            return;
        }

        queueItem.CurrentRegionLabel = previewFrame.RegionLabel;

        await _adapter.EnsureConnectedAsync(cancellationToken).ConfigureAwait(false);
        await _adapter
            .ApplyCutAsync(Channel, template, previewFrame.Cut, previewFrame.Snapshot, station, imageRootPath, cancellationToken)
            .ConfigureAwait(false);
        await _adapter.PrepareAsync(Channel, cancellationToken).ConfigureAwait(false);

        _preparedCutFrame = new PreparedCutFrame(
            queueItem,
            template,
            previewFrame.Cut,
            previewFrame.Snapshot,
            station,
            imageRootPath,
            previewFrame.RegionLabel);

        await CaptureCurrentPreviewAsync(queueItem, template, cancellationToken).ConfigureAwait(false);

        RefreshQueueMarkers();
    }

    private async Task PlayItemAsync(ChannelScheduleItem queueItem, FormatTemplateDefinition template, CancellationToken cancellationToken)
    {
        var station = _stationProvider();
        var imageRootPath = _imageRootProvider();
        var resolvedCuts = ResolveCuts(template, station);
        var hasEndScene = KarismaSceneResolver.HasEndScene(template, imageRootPath);
        var regionTargets = await _dataRefreshGate
            .ResolveScheduleRegionTargetsAsync(queueItem, template, station, cancellationToken)
            .ConfigureAwait(false);

        if (regionTargets.Count == 0)
        {
            queueItem.State = ScheduleQueueItemState.Error;
            queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
            queueItem.CurrentRegionLabel = string.Empty;
            _lastPlaybackItemId = queueItem.Id;
            RefreshQueueMarkers();
            return;
        }

        var playedAny = false;
        var lastFailure = string.Empty;

        if (ShouldUseAggregateScheduleSnapshot(template))
        {
            var aggregateRegionGroups = ResolveAggregateScheduleRegionGroups(template, regionTargets);
            for (var groupIndex = 0; groupIndex < aggregateRegionGroups.Count; groupIndex++)
            {
                var aggregateRegionGroup = aggregateRegionGroups[groupIndex];
                ElectionDataSnapshot aggregateSnapshot;
                try
                {
                    await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);
                    aggregateSnapshot = await _dataRefreshGate
                        .GetAggregateScheduleSnapshotAsync(queueItem, template, station, aggregateRegionGroup, cancellationToken)
                        .ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    lastFailure = $"{ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup)}: {ex.Message}";
                    _logService.Warning($"[{Channel}] 집계형 송출 데이터 수신 실패: {lastFailure}");
                    continue;
                }

                if (!_dataRefreshGate.ValidateSnapshotForFormat(template, aggregateSnapshot, out var aggregateValidationError))
                {
                    lastFailure = $"{ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup)}: {aggregateValidationError}";
                    _logService.Warning($"[{Channel}] 집계형 송출 데이터 검증 실패: {lastFailure}");
                    continue;
                }

                queueItem.CurrentRegionLabel = ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup);
                var isLastGroup = groupIndex == aggregateRegionGroups.Count - 1;

                var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, aggregateSnapshot, hasEndScene && isLastGroup);
                for (var cutIndex = 0; cutIndex < playbackCuts.Count; cutIndex++)
                {
                    var cut = playbackCuts[cutIndex];
                    Func<CancellationToken, Task<CutPreviewFrame?>>? nextPreviewFactory = null;
                    if (cutIndex + 1 < playbackCuts.Count)
                    {
                        var nextCut = playbackCuts[cutIndex + 1];
                        var nextRegionLabel = queueItem.CurrentRegionLabel;
                        nextPreviewFactory = _ => Task.FromResult<CutPreviewFrame?>(
                            new CutPreviewFrame(nextCut, aggregateSnapshot, nextRegionLabel));
                    }
                    else if (groupIndex + 1 < aggregateRegionGroups.Count)
                    {
                        var nextGroupIndex = groupIndex + 1;
                        nextPreviewFactory = token => TryBuildAggregatePreviewFrameAsync(
                            queueItem,
                            template,
                            station,
                            resolvedCuts,
                            aggregateRegionGroups,
                            nextGroupIndex,
                            hasEndScene,
                            token);
                    }

                    await PlayCutFrameAsync(queueItem, template, cut, aggregateSnapshot, station, imageRootPath, nextPreviewFactory, cancellationToken)
                        .ConfigureAwait(false);
                    if (ShouldSkipCurrentItem(queueItem))
                    {
                        break;
                    }
                }

                playedAny = true;
                if (ShouldSkipCurrentItem(queueItem))
                {
                    break;
                }
            }

            queueItem.CurrentRegionLabel = string.Empty;
            queueItem.ClearInternalNextPreview();
            _lastPlaybackItemId = queueItem.Id;
            queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
            queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
            ClearSkipCurrentItem(queueItem);
            RefreshQueueMarkers();
            return;
        }

        for (var regionIndex = 0; regionIndex < regionTargets.Count; regionIndex++)
        {
            var regionTarget = regionTargets[regionIndex];
            ElectionDataSnapshot snapshot;
            try
            {
                await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);
                snapshot = await _dataRefreshGate
                    .GetScheduleSnapshotAsync(queueItem, template, regionTarget, cancellationToken)
                    .ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                lastFailure = $"{regionTarget.DisplayName}: {ex.Message}";
                _logService.Warning($"[{Channel}] 스케줄 지역 수신 실패: {lastFailure}");
                continue;
            }

            if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out var validationError))
            {
                lastFailure = $"{regionTarget.DisplayName}: {validationError}";
                _logService.Warning($"[{Channel}] 스케줄 지역 검증 실패: {lastFailure}");
                continue;
            }

            queueItem.CurrentRegionLabel = regionTarget.DisplayName;
            var isLastRegion = regionIndex == regionTargets.Count - 1;
            var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, snapshot, hasEndScene && isLastRegion);
            queueItem.TotalCuts = playbackCuts.Count;

            for (var cutIndex = 0; cutIndex < playbackCuts.Count; cutIndex++)
            {
                var cut = playbackCuts[cutIndex];
                Func<CancellationToken, Task<CutPreviewFrame?>>? nextPreviewFactory = null;
                if (cutIndex + 1 < playbackCuts.Count)
                {
                    var nextCut = playbackCuts[cutIndex + 1];
                    var nextRegionLabel = regionTarget.DisplayName;
                    nextPreviewFactory = _ => Task.FromResult<CutPreviewFrame?>(
                        new CutPreviewFrame(nextCut, snapshot, nextRegionLabel));
                }
                else if (regionIndex + 1 < regionTargets.Count)
                {
                    var nextRegionIndex = regionIndex + 1;
                    nextPreviewFactory = token => TryBuildRegionPreviewFrameAsync(
                        queueItem,
                        template,
                        station,
                        imageRootPath,
                        resolvedCuts,
                        regionTargets,
                        nextRegionIndex,
                        hasEndScene,
                        token);
                }

                await PlayCutFrameAsync(queueItem, template, cut, snapshot, station, imageRootPath, nextPreviewFactory, cancellationToken)
                    .ConfigureAwait(false);
                if (ShouldSkipCurrentItem(queueItem))
                {
                    break;
                }
            }

            playedAny = true;
            if (ShouldSkipCurrentItem(queueItem))
            {
                break;
            }
        }

        queueItem.CurrentRegionLabel = string.Empty;
        queueItem.ClearInternalNextPreview();
        _lastPlaybackItemId = queueItem.Id;
        queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
        queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
        ClearSkipCurrentItem(queueItem);
        RefreshQueueMarkers();
    }

    private async Task PlayCutFrameAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        FormatCutDefinition cut,
        ElectionDataSnapshot snapshot,
        BroadcastStationProfile station,
        string imageRootPath,
        Func<CancellationToken, Task<CutPreviewFrame?>>? nextInternalPreviewFrameFactory,
        CancellationToken cancellationToken)
    {
        queueItem.State = ScheduleQueueItemState.Sending;
        RefreshQueueMarkers();
        QueueChanged?.Invoke(this, EventArgs.Empty);

        var preparedFrame = TryConsumePreparedFrame(queueItem, template, cut);
        var playbackCut = preparedFrame?.Cut ?? cut;
        var playbackSnapshot = preparedFrame?.Snapshot ?? snapshot;
        var playbackStation = preparedFrame?.Station ?? station;
        var playbackImageRootPath = preparedFrame?.ImageRootPath ?? imageRootPath;
        if (preparedFrame is not null)
        {
            queueItem.CurrentRegionLabel = preparedFrame.RegionLabel;
        }
        else
        {
            await _adapter.EnsureConnectedAsync(cancellationToken).ConfigureAwait(false);
            await _adapter.ApplyCutAsync(Channel, template, cut, snapshot, station, imageRootPath, cancellationToken).ConfigureAwait(false);
            await _adapter.PrepareAsync(Channel, cancellationToken).ConfigureAwait(false);
        }

        await _adapter.TakeAsync(Channel, cancellationToken).ConfigureAwait(false);

        var onAirAt = DateTimeOffset.Now;
        queueItem.State = ScheduleQueueItemState.OnAir;
        queueItem.LastPlayedAt = onAirAt;
        RefreshQueueMarkers();
        QueueChanged?.Invoke(this, EventArgs.Empty);

        var signal = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        _advanceSignal = signal;
        if (ShouldSkipCurrentItem(queueItem))
        {
            signal.TrySetResult(true);
        }

        var durationSeconds = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(cut.DurationSeconds, template);
        var playbackDuration = TimeSpan.FromSeconds(durationSeconds);
        await CaptureCurrentPreviewAsync(queueItem, template, cancellationToken).ConfigureAwait(false);
        QueueChanged?.Invoke(this, EventArgs.Empty);
        if (!ShouldSkipCurrentItem(queueItem))
        {
            var remainingForPreview = playbackDuration - (DateTimeOffset.Now - onAirAt);
            if (remainingForPreview > MinimumNextPreviewWindow)
            {
                CutPreviewFrame? nextInternalPreviewFrame = null;
                if (nextInternalPreviewFrameFactory is not null)
                {
                    try
                    {
                        nextInternalPreviewFrame = await nextInternalPreviewFrameFactory(cancellationToken).ConfigureAwait(false);
                    }
                    catch (OperationCanceledException)
                    {
                        throw;
                    }
                    catch (Exception ex)
                    {
                        _logService.Warning($"[{Channel}] 다음 지역 프리뷰 데이터 준비 실패: {queueItem.DisplayName} / {ex.Message}");
                    }
                }

                await CaptureNextPreviewAsync(queueItem, template, nextInternalPreviewFrame, station, imageRootPath, cancellationToken).ConfigureAwait(false);
                QueueChanged?.Invoke(this, EventArgs.Empty);
            }
            else
            {
                MarkQueueNextPreviewStatus(queueItem, $"빠른 송출 중 프리뷰 생략 {DateTimeOffset.Now:HH:mm:ss}");
            }
        }

        if (ShouldSkipCurrentItem(queueItem))
        {
            return;
        }

        var remainingDuration = playbackDuration - (DateTimeOffset.Now - onAirAt);
        if (remainingDuration <= TimeSpan.Zero)
        {
            return;
        }

        var delayTask = Task.Delay(remainingDuration, cancellationToken);
        await Task.WhenAny(delayTask, signal.Task).ConfigureAwait(false);
    }

    private async Task CaptureCurrentPreviewAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        if (!_adapter.IsLiveCg)
        {
            return;
        }

        var size = ThumbnailLayoutResolver.ResolveGenerationSize(template, _videoWallLayoutPresetProvider());
        var previewPath = CutPreviewAssetCatalog.CreateCapturePath(Channel, queueItem.Id, "current");
        var captured = await _adapter.TryCapturePendingCutPreviewAsync(
            Channel,
            previewPath,
            size.Width,
            size.Height,
            PreviewFrame,
            cancellationToken).ConfigureAwait(false);

        if (!captured)
        {
            await UiDispatcher.EnqueueAsync(() =>
                queueItem.UpdateRenderedPreviewStatus($"현재 화면 캡처 지연 {DateTimeOffset.Now:HH:mm:ss}")).ConfigureAwait(false);
            return;
        }

        await UiDispatcher.EnqueueAsync(() =>
            queueItem.UpdateRenderedPreview(previewPath, $"현재 변수 적용 캡처 {DateTimeOffset.Now:HH:mm:ss}")).ConfigureAwait(false);
    }

    private async Task CaptureNextPreviewAsync(
        ChannelScheduleItem activeItem,
        FormatTemplateDefinition activeTemplate,
        CutPreviewFrame? internalNextPreviewFrame,
        BroadcastStationProfile station,
        string imageRootPath,
        CancellationToken cancellationToken)
    {
        if (!_adapter.IsLiveCg)
        {
            return;
        }

        if (internalNextPreviewFrame is not null)
        {
            await CaptureInternalNextPreviewAsync(
                activeItem,
                activeTemplate,
                internalNextPreviewFrame,
                station,
                imageRootPath,
                cancellationToken).ConfigureAwait(false);
            return;
        }

        activeItem.ClearInternalNextPreview();
        var nextItem = GetPreviewNextItem(activeItem);
        if (nextItem is null)
        {
            return;
        }

        var template = _templateResolver(nextItem.FormatId);
        if (template is null)
        {
            return;
        }

        CutPreviewFrame? previewFrame;
        try
        {
            previewFrame = await TryBuildPreviewFrameAsync(nextItem, template, station, imageRootPath, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logService.Warning($"[{Channel}] 다음 컷 프리뷰 데이터 준비 실패: {nextItem.DisplayName} / {ex.Message}");
            return;
        }

        if (previewFrame is null)
        {
            return;
        }

        var size = ThumbnailLayoutResolver.ResolveGenerationSize(template, _videoWallLayoutPresetProvider());
        var previewPath = CutPreviewAssetCatalog.CreateCapturePath(Channel, nextItem.Id, "next");
        var captured = await _adapter.TryCaptureCutPreviewAsync(
            Channel,
            template,
            previewFrame.Cut,
            previewFrame.Snapshot,
            station,
            imageRootPath,
            previewPath,
            size.Width,
            size.Height,
            PreviewFrame,
            cancellationToken).ConfigureAwait(false);

        if (!captured)
        {
            await UiDispatcher.EnqueueAsync(() =>
                nextItem.UpdateRenderedPreviewStatus($"다음 프리뷰 캡처 지연 {DateTimeOffset.Now:HH:mm:ss}")).ConfigureAwait(false);
            return;
        }

        await UiDispatcher.EnqueueAsync(() =>
            nextItem.UpdateRenderedPreview(previewPath, $"다음 변수 적용 캡처 {DateTimeOffset.Now:HH:mm:ss}")).ConfigureAwait(false);
    }

    private void MarkQueueNextPreviewStatus(ChannelScheduleItem activeItem, string statusLabel)
    {
        var nextItem = GetPreviewNextItem(activeItem);
        if (nextItem is null)
        {
            return;
        }

        UiDispatcher.Enqueue(() => nextItem.UpdateRenderedPreviewStatus(statusLabel));
    }

    private async Task CaptureInternalNextPreviewAsync(
        ChannelScheduleItem activeItem,
        FormatTemplateDefinition template,
        CutPreviewFrame previewFrame,
        BroadcastStationProfile station,
        string imageRootPath,
        CancellationToken cancellationToken)
    {
        var size = ThumbnailLayoutResolver.ResolveGenerationSize(template, _videoWallLayoutPresetProvider());
        var previewPath = CutPreviewAssetCatalog.CreateCapturePath(Channel, activeItem.Id, "internal-next");
        var captured = await _adapter.TryCaptureCutPreviewAsync(
            Channel,
            template,
            previewFrame.Cut,
            previewFrame.Snapshot,
            station,
            imageRootPath,
            previewPath,
            size.Width,
            size.Height,
            PreviewFrame,
            cancellationToken).ConfigureAwait(false);

        if (!captured)
        {
            activeItem.ClearInternalNextPreview();
            return;
        }

        var regionLabel = string.IsNullOrWhiteSpace(previewFrame.RegionLabel)
            ? activeItem.SelectionRegionLabel
            : previewFrame.RegionLabel;
        var displayName = $"{activeItem.FormatName} / {regionLabel}";
        await UiDispatcher.EnqueueAsync(() =>
            activeItem.UpdateInternalNextPreview(previewPath, displayName, $"다음 지역 변수 적용 캡처 {DateTimeOffset.Now:HH:mm:ss}")).ConfigureAwait(false);
    }

    private async Task<CutPreviewFrame?> TryBuildPreviewFrameAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        string imageRootPath,
        CancellationToken cancellationToken)
    {
        await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);

        var resolvedCuts = ResolveCuts(template, station);
        if (resolvedCuts.Count == 0)
        {
            return null;
        }

        var hasEndScene = KarismaSceneResolver.HasEndScene(template, imageRootPath);
        var regionTargets = await _dataRefreshGate
            .ResolveScheduleRegionTargetsAsync(queueItem, template, station, cancellationToken)
            .ConfigureAwait(false);
        if (regionTargets.Count == 0)
        {
            return null;
        }

        if (ShouldUseAggregateScheduleSnapshot(template))
        {
            var aggregateRegionGroups = ResolveAggregateScheduleRegionGroups(template, regionTargets);
            for (var groupIndex = 0; groupIndex < aggregateRegionGroups.Count; groupIndex++)
            {
                var aggregateRegionGroup = aggregateRegionGroups[groupIndex];
                var snapshot = await _dataRefreshGate
                    .GetAggregateScheduleSnapshotAsync(queueItem, template, station, aggregateRegionGroup, cancellationToken)
                    .ConfigureAwait(false);
                if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out _))
                {
                    continue;
                }

                var isLastGroup = groupIndex == aggregateRegionGroups.Count - 1;
                var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, snapshot, hasEndScene && isLastGroup);
                if (playbackCuts.Count == 0)
                {
                    continue;
                }

                var cut = playbackCuts[0];
                return new CutPreviewFrame(cut, snapshot, ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup));
            }

            return null;
        }

        for (var regionIndex = 0; regionIndex < regionTargets.Count; regionIndex++)
        {
            var regionTarget = regionTargets[regionIndex];
            var snapshot = await _dataRefreshGate
                .GetScheduleSnapshotAsync(queueItem, template, regionTarget, cancellationToken)
                .ConfigureAwait(false);
            if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out _))
            {
                continue;
            }

            var isLastRegion = regionIndex == regionTargets.Count - 1;
            var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, snapshot, hasEndScene && isLastRegion);
            if (playbackCuts.Count == 0)
            {
                continue;
            }

            return new CutPreviewFrame(playbackCuts[0], snapshot, regionTarget.DisplayName);
        }

        return null;
    }

    private async Task<CutPreviewFrame?> TryBuildRegionPreviewFrameAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        string imageRootPath,
        IReadOnlyList<FormatCutDefinition> resolvedCuts,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        int startRegionIndex,
        bool hasEndScene,
        CancellationToken cancellationToken)
    {
        for (var regionIndex = startRegionIndex; regionIndex < regionTargets.Count; regionIndex++)
        {
            var regionTarget = regionTargets[regionIndex];
            await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);
            var snapshot = await _dataRefreshGate
                .GetScheduleSnapshotAsync(queueItem, template, regionTarget, cancellationToken)
                .ConfigureAwait(false);
            if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out _))
            {
                continue;
            }

            var isLastRegion = regionIndex == regionTargets.Count - 1;
            var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, snapshot, hasEndScene && isLastRegion);
            if (playbackCuts.Count == 0)
            {
                continue;
            }

            return new CutPreviewFrame(playbackCuts[0], snapshot, regionTarget.DisplayName);
        }

        return null;
    }

    private async Task<CutPreviewFrame?> TryBuildAggregatePreviewFrameAsync(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        IReadOnlyList<FormatCutDefinition> resolvedCuts,
        IReadOnlyList<IReadOnlyList<ScheduleRegionTarget>> aggregateRegionGroups,
        int startGroupIndex,
        bool hasEndScene,
        CancellationToken cancellationToken)
    {
        for (var groupIndex = startGroupIndex; groupIndex < aggregateRegionGroups.Count; groupIndex++)
        {
            var aggregateRegionGroup = aggregateRegionGroups[groupIndex];
            await _dataRefreshGate.WaitForRefreshAsync(cancellationToken).ConfigureAwait(false);
            var snapshot = await _dataRefreshGate
                .GetAggregateScheduleSnapshotAsync(queueItem, template, station, aggregateRegionGroup, cancellationToken)
                .ConfigureAwait(false);
            if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out _))
            {
                continue;
            }

            var isLastGroup = groupIndex == aggregateRegionGroups.Count - 1;
            var playbackCuts = ResolvePlaybackCuts(template, resolvedCuts, snapshot, hasEndScene && isLastGroup);
            if (playbackCuts.Count == 0)
            {
                continue;
            }

            var cut = playbackCuts[0];
            return new CutPreviewFrame(cut, snapshot, ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup));
        }

        return null;
    }

    private ChannelScheduleItem? GetPreviewNextItem(ChannelScheduleItem activeItem)
    {
        return GetSequentialNextItem(activeItem, allowWrap: LoopEnabled);
    }

    private bool ShouldSkipCurrentItem(ChannelScheduleItem queueItem)
    {
        return _skipCurrentItemId == queueItem.Id;
    }

    private PreparedCutFrame? TryConsumePreparedFrame(
        ChannelScheduleItem queueItem,
        FormatTemplateDefinition template,
        FormatCutDefinition cut)
    {
        if (_preparedCutFrame is not { } preparedFrame)
        {
            return null;
        }

        if (preparedFrame.Item.Id != queueItem.Id ||
            !string.Equals(preparedFrame.Template.Id, template.Id, StringComparison.Ordinal) ||
            !CutsMatch(preparedFrame.Cut, cut))
        {
            if (preparedFrame.Item.Id == queueItem.Id)
            {
                ClearPreparedFrame(resetState: false);
            }

            return null;
        }

        _preparedCutFrame = null;
        return preparedFrame;
    }

    private static bool CutsMatch(FormatCutDefinition left, FormatCutDefinition right)
    {
        return string.Equals(left.Name, right.Name, StringComparison.Ordinal) &&
               left.CandidateStartIndex == right.CandidateStartIndex &&
               left.UseEndScene == right.UseEndScene &&
               string.Equals(left.SceneIdOverride, right.SceneIdOverride, StringComparison.Ordinal);
    }

    private void ClearPreparedFrame(bool resetState)
    {
        if (_preparedCutFrame is not { } preparedFrame)
        {
            return;
        }

        _preparedCutFrame = null;
        if (!resetState || preparedFrame.Item.State != ScheduleQueueItemState.Sending)
        {
            return;
        }

        preparedFrame.Item.State = ScheduleQueueItemState.Queued;
        preparedFrame.Item.CurrentRegionLabel = string.Empty;
        preparedFrame.Item.ClearRenderedPreview();
        preparedFrame.Item.ClearInternalNextPreview();
    }

    private void ClearSkipCurrentItem(ChannelScheduleItem queueItem)
    {
        if (_skipCurrentItemId == queueItem.Id)
        {
            _skipCurrentItemId = null;
        }
    }

    private static bool ShouldUseAggregateScheduleSnapshot(FormatTemplateDefinition template)
    {
        if (IsCurrentLeaderTemplate(template))
        {
            return true;
        }

        if (IsTopPanseTemplate(template))
        {
            return true;
        }

        if (IsNormalPanseMapTemplate(template))
        {
            return true;
        }

        if (IsBottomWinnerTemplate(template) || IsBasicCouncilWinnerTemplate(template))
        {
            return true;
        }

        if (ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template.Name))
        {
            return true;
        }

        if (template.RecommendedChannel == BroadcastChannel.Bottom)
        {
            return IsBottomTurnoutBoardTemplate(template);
        }

        return IsNormalPreElectionTurnoutDistrictBoardTemplate(template) ||
               IsTopTurnoutDistrictBoardTemplate(template);
    }

    private static IReadOnlyList<IReadOnlyList<ScheduleRegionTarget>> ResolveAggregateScheduleRegionGroups(
        FormatTemplateDefinition template,
        IReadOnlyList<ScheduleRegionTarget> regionTargets)
    {
        if (IsCurrentLeaderTemplate(template))
        {
            if (IsBottomCurrentLeaderTemplate(template))
            {
                return [regionTargets];
            }

            return ChunkRegionTargets(regionTargets, ResolveCurrentLeaderPageSize(template));
        }

        if (IsBottomWinnerTemplate(template))
        {
            return [regionTargets];
        }

        if (IsTopTurnoutDistrictBoardTemplate(template))
        {
            return regionTargets
                .GroupBy(ResolveCouncilSeatTableRegionKey, StringComparer.OrdinalIgnoreCase)
                .SelectMany(group => ChunkRegionTargets(group.ToArray(), 3))
                .ToArray();
        }

        if (IsNormalPreElectionTurnoutDistrictBoardTemplate(template))
        {
            return regionTargets
                .GroupBy(ResolveCouncilSeatTableRegionKey, StringComparer.OrdinalIgnoreCase)
                .SelectMany(group => ChunkRegionTargets(group.ToArray(), 7))
                .ToArray();
        }

        if (!IsBasicCouncilWinnerTemplate(template) &&
            !ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template.Name))
        {
            return [regionTargets];
        }

        return regionTargets
            .GroupBy(ResolveCouncilSeatTableRegionKey, StringComparer.OrdinalIgnoreCase)
            .Select(group => (IReadOnlyList<ScheduleRegionTarget>)group.ToArray())
            .ToArray();
    }

    private static IReadOnlyList<IReadOnlyList<ScheduleRegionTarget>> ChunkRegionTargets(
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        int pageSize)
    {
        pageSize = Math.Max(1, pageSize);
        var groups = new List<IReadOnlyList<ScheduleRegionTarget>>();
        for (var index = 0; index < regionTargets.Count; index += pageSize)
        {
            groups.Add(regionTargets.Skip(index).Take(pageSize).ToArray());
        }

        return groups;
    }

    private static string ResolveCouncilSeatTableRegionKey(ScheduleRegionTarget target)
    {
        if (!string.IsNullOrWhiteSpace(target.RegionName))
        {
            return NormalizeRegionKey(target.RegionName);
        }

        if (!string.IsNullOrWhiteSpace(target.DisplayName))
        {
            return NormalizeRegionKey(target.DisplayName);
        }

        return target.DistrictCode ?? string.Empty;
    }

    private static string ResolveAggregateRegionGroupLabel(
        ChannelScheduleItem queueItem,
        IReadOnlyList<ScheduleRegionTarget> regionTargets)
    {
        if (queueItem.Channel == BroadcastChannel.Normal &&
            string.Equals(queueItem.FormatName, "판세_광역단체장", StringComparison.Ordinal))
        {
            return "전국";
        }

        var regionNames = regionTargets
            .Select(target => target.RegionName)
            .Where(regionName => !string.IsNullOrWhiteSpace(regionName))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (regionNames.Length == 1)
        {
            return regionNames[0];
        }

        return string.IsNullOrWhiteSpace(queueItem.SelectionRegionLabel)
            ? "선택권역"
            : queueItem.SelectionRegionLabel;
    }

    private static bool IsTopTurnoutDistrictBoardTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.TopLeft &&
               string.Equals(template.Name, "투표율_선거구별", StringComparison.Ordinal);
    }

    private static bool IsNormalPreElectionTurnoutDistrictBoardTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Normal &&
               string.Equals(template.Name, "투표율_선거구별 사전", StringComparison.Ordinal);
    }

    private static bool IsBottomTurnoutBoardTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               (string.Equals(template.Name, "사전투표율", StringComparison.Ordinal) ||
                string.Equals(template.Name, "사전투표율_시도", StringComparison.Ordinal) ||
                string.Equals(template.Name, "사전투표율_시군구", StringComparison.Ordinal) ||
                string.Equals(template.Name, "투표율", StringComparison.Ordinal) ||
                string.Equals(template.Name, "투표율_시도", StringComparison.Ordinal) ||
                string.Equals(template.Name, "투표율_시군구", StringComparison.Ordinal));
    }

    private static bool IsTopPanseTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.TopLeft &&
               template.Name.StartsWith("판세_", StringComparison.Ordinal);
    }

    private static bool IsNormalPanseMapTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Normal &&
               string.Equals(template.Name, "판세_광역단체장", StringComparison.Ordinal);
    }

    private static string NormalizeRegionKey(string value)
    {
        return string.Concat((value ?? string.Empty).Where(character => !char.IsWhiteSpace(character)));
    }

    private static IReadOnlyList<FormatCutDefinition> ResolvePlaybackCuts(
        FormatTemplateDefinition template,
        IReadOnlyList<FormatCutDefinition> baseCuts,
        ElectionDataSnapshot snapshot,
        bool useEndSceneOnLastCut)
    {
        if (!IsCandidatePagedTemplate(template) || baseCuts.Count == 0)
        {
            return ApplyEndSceneToLastCut(baseCuts, useEndSceneOnLastCut);
        }

        var candidateCount = Math.Max(snapshot.Candidates.Count, 1);
        var pageSize = ResolveCandidatePageSize(template);
        var pageStarts = Enumerable
            .Range(0, (int)Math.Ceiling(candidateCount / (double)pageSize))
            .Select(pageIndex => pageIndex * pageSize)
            .ToArray();
        var playbackCuts = new List<FormatCutDefinition>(baseCuts.Count * candidateCount);
        foreach (var baseCut in baseCuts)
        {
            foreach (var candidateStartIndex in pageStarts)
            {
                var isLastPage = candidateStartIndex == pageStarts[^1];
                var cutName = ResolveCandidatePagedCutName(template, baseCut.Name, candidateStartIndex, isLastPage);
                playbackCuts.Add(new FormatCutDefinition
                {
                    Name = cutName,
                    DurationSeconds = baseCut.DurationSeconds,
                    CandidateStartIndex = candidateStartIndex,
                    UseEndScene = baseCut.UseEndScene,
                    SceneIdOverride = ResolveCareerSceneId(template, cutName)
                });
            }
        }

        return IsAllCandidateTemplate(template)
            ? playbackCuts
            : ApplyEndSceneToLastCut(playbackCuts, useEndSceneOnLastCut);
    }

    private static string ResolveCandidatePagedCutName(
        FormatTemplateDefinition template,
        string cutName,
        int candidateStartIndex,
        bool isLastPage)
    {
        if (IsAllCandidateTemplate(template))
        {
            if (candidateStartIndex == 0)
            {
                return cutName;
            }

            if (template.RecommendedChannel == BroadcastChannel.Bottom)
            {
                return ResolveSuffixedCutName(cutName, "_loop");
            }

            if (isLastPage)
            {
                return ResolveSuffixedCutName(cutName, "_END");
            }

            return UsesAllCandidateLoopScene(template)
                ? ResolveSuffixedCutName(cutName, "_loop")
                : cutName;
        }

        return candidateStartIndex == 0
            ? cutName
            : ResolveSuffixedCutName(cutName, "_loop");
    }

    private static string ResolveSuffixedCutName(string cutName, string suffix)
    {
        if (cutName.EndsWith(suffix, StringComparison.Ordinal))
        {
            return cutName;
        }

        const string inSuffix = "_in";
        if (string.Equals(suffix, "_loop", StringComparison.Ordinal) &&
            cutName.EndsWith(inSuffix, StringComparison.Ordinal))
        {
            return cutName[..^inSuffix.Length] + suffix;
        }

        return cutName + suffix;
    }

    private static string ResolveCareerSceneId(FormatTemplateDefinition template, string cutName)
    {
        var folderName = Path.GetDirectoryName(template.Id);
        return string.IsNullOrWhiteSpace(folderName)
            ? cutName
            : Path.Combine(folderName, cutName);
    }

    private static IReadOnlyList<FormatCutDefinition> ApplyEndSceneToLastCut(
        IReadOnlyList<FormatCutDefinition> cuts,
        bool useEndSceneOnLastCut)
    {
        if (!useEndSceneOnLastCut || cuts.Count == 0)
        {
            return cuts;
        }

        var result = cuts.ToArray();
        result[^1] = ResolveScheduledCut(result[^1], hasEndScene: true, useEndScene: true);
        return result;
    }

    private static FormatCutDefinition ResolveScheduledCut(
        FormatCutDefinition cut,
        bool hasEndScene,
        bool useEndScene)
    {
        if (!hasEndScene || !useEndScene || cut.UseEndScene)
        {
            return cut;
        }

        return new FormatCutDefinition
        {
            Name = cut.Name,
            DurationSeconds = cut.DurationSeconds,
            CandidateStartIndex = cut.CandidateStartIndex,
            UseEndScene = true,
            SceneIdOverride = cut.SceneIdOverride
        };
    }

    private static bool IsCareerTemplate(FormatTemplateDefinition template)
    {
        return template.Name.StartsWith("경력_", StringComparison.Ordinal);
    }

    private static bool IsAllCandidateTemplate(FormatTemplateDefinition template)
    {
        return template.Name.StartsWith("모든후보_", StringComparison.Ordinal) ||
               template.Name.StartsWith("전후보_", StringComparison.Ordinal);
    }

    private static bool IsCurrentLeaderTemplate(FormatTemplateDefinition template)
    {
        return template.Name.StartsWith("이시각1위_", StringComparison.Ordinal) ||
               IsBottomCurrentLeaderTemplate(template);
    }

    private static bool IsBottomCurrentLeaderTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               template.Name.StartsWith("1위_", StringComparison.Ordinal);
    }

    private static bool IsCandidatePagedTemplate(FormatTemplateDefinition template)
    {
        return IsCareerTemplate(template) ||
               IsAllCandidateTemplate(template) ||
               IsBottomWinnerTemplate(template) ||
               IsBottomCurrentLeaderTemplate(template);
    }

    private static bool IsBottomWinnerTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               template.Name.StartsWith("당선_", StringComparison.Ordinal);
    }

    private static bool IsBasicCouncilWinnerTemplate(FormatTemplateDefinition template)
    {
        return template.Name.StartsWith("당선_", StringComparison.Ordinal) &&
               (template.Name.Contains("광역의원", StringComparison.Ordinal) ||
                template.Name.Contains("기초의원", StringComparison.Ordinal));
    }

    private static int ResolveCandidatePageSize(FormatTemplateDefinition template)
    {
        if (IsBottomWinnerTemplate(template) ||
            IsBottomCurrentLeaderTemplate(template) ||
            (template.RecommendedChannel == BroadcastChannel.Bottom && IsAllCandidateTemplate(template)))
        {
            return 3;
        }

        if (!IsAllCandidateTemplate(template))
        {
            return 1;
        }

        return template.SceneWidth >= 5000 ||
               template.Name.Contains("8316", StringComparison.Ordinal) ||
               template.Name.Contains("3840", StringComparison.Ordinal) ||
               template.Name.Contains("2880", StringComparison.Ordinal) ||
               template.Name.Contains("5760", StringComparison.Ordinal) ||
               template.Id.Contains("_L", StringComparison.Ordinal)
            ? 3
            : 1;
    }

    private static int ResolveCurrentLeaderPageSize(FormatTemplateDefinition template)
    {
        if (template.RecommendedChannel == BroadcastChannel.Bottom &&
            template.Name.StartsWith("1위_", StringComparison.Ordinal))
        {
            return 3;
        }

        if (template.Name.Contains("_L", StringComparison.Ordinal) ||
            template.Id.Contains("_L", StringComparison.Ordinal) ||
            template.SceneWidth >= 5000)
        {
            return 3;
        }

        if (template.Name.Contains("_HD", StringComparison.Ordinal) ||
            template.Id.Contains("_HD", StringComparison.Ordinal))
        {
            return 2;
        }

        return 1;
    }

    private static bool UsesAllCandidateLoopScene(FormatTemplateDefinition template)
    {
        return ResolveCandidatePageSize(template) == 1;
    }

    private IReadOnlyList<FormatCutDefinition> ResolveCuts(FormatTemplateDefinition template, BroadcastStationProfile station)
    {
        if (template.LoopMode != LoopMode.StationRegions)
        {
            return template.Cuts;
        }

        var loopCuts = new List<FormatCutDefinition>();
        foreach (var region in station.RegionFilters)
        {
            loopCuts.Add(new FormatCutDefinition
            {
                Name = $"{template.Cuts[0].Name} - {region}",
                DurationSeconds = template.Cuts[0].DurationSeconds
            });
        }

        return loopCuts;
    }

    private ChannelScheduleItem? GetNextPlayableItem()
    {
        if (_preparedCutFrame is { Item: var preparedItem } && Queue.Contains(preparedItem))
        {
            return preparedItem;
        }

        var anchorItem = ActivePlaybackItem ?? GetLastPlaybackItem();
        return GetSequentialNextItem(anchorItem, allowWrap: LoopEnabled);
    }

    public void RefreshQueueMarkers()
    {
        var activeItem = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending);
        var anchorItem = activeItem ?? GetLastPlaybackItem();
        var nextItem = GetSequentialNextItem(anchorItem, allowWrap: LoopEnabled);

        foreach (var item in Queue)
        {
            if (item == activeItem || item.State == ScheduleQueueItemState.Error)
            {
                continue;
            }

            item.State = item == nextItem ? ScheduleQueueItemState.Next : ScheduleQueueItemState.Queued;
        }
    }

    private ChannelScheduleItem? GetLastPlaybackItem()
    {
        if (_lastPlaybackItemId is not { } itemId)
        {
            return null;
        }

        return Queue.FirstOrDefault(item => item.Id == itemId);
    }

    private ChannelScheduleItem? GetSequentialNextItem(ChannelScheduleItem? anchorItem, bool allowWrap)
    {
        if (Queue.Count == 0)
        {
            return null;
        }

        if (anchorItem is null)
        {
            return Queue.FirstOrDefault(IsPlayableQueueItem);
        }

        var anchorIndex = Queue.IndexOf(anchorItem);
        if (anchorIndex < 0)
        {
            return Queue.FirstOrDefault(IsPlayableQueueItem);
        }

        var nextItem = Queue.Skip(anchorIndex + 1).FirstOrDefault(IsPlayableQueueItem);
        if (nextItem is not null)
        {
            return nextItem;
        }

        return allowWrap
            ? Queue.Take(anchorIndex + 1).FirstOrDefault(IsPlayableQueueItem)
            : null;
    }

    private static bool IsPlayableQueueItem(ChannelScheduleItem item)
    {
        return item.State is ScheduleQueueItemState.Queued or ScheduleQueueItemState.Next or ScheduleQueueItemState.Completed;
    }

    private sealed record PreparedCutFrame(
        ChannelScheduleItem Item,
        FormatTemplateDefinition Template,
        FormatCutDefinition Cut,
        ElectionDataSnapshot Snapshot,
        BroadcastStationProfile Station,
        string ImageRootPath,
        string RegionLabel);

    private sealed record CutPreviewFrame(FormatCutDefinition Cut, ElectionDataSnapshot Snapshot, string RegionLabel);
}
