using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Domain;
using Tornado3_2026Election.Persistence;
using Tornado3_2026Election.Services;

namespace Tornado3_2026Election.ViewModels;

public sealed class MainViewModel : ObservableObject
{
    private static readonly Brush ConnectedStatusBrush = new SolidColorBrush(Colors.LimeGreen);
    private static readonly Brush DisconnectedStatusBrush = new SolidColorBrush(Colors.OrangeRed);
    private static readonly Brush DataReceivingNavigationBrush = new SolidColorBrush(Colors.LimeGreen);
    private static readonly Brush DataWaitingNavigationBrush = new SolidColorBrush(Colors.White);
    private static readonly TimeSpan AutomaticSaveDelay = TimeSpan.FromMilliseconds(500);
    private FormatCatalogService _formatCatalogService;
    private readonly AppStateStore _stateStore;
    private readonly LogService _logService;
    private readonly KarismaThumbnailGeneratorService _thumbnailGeneratorService;
    private readonly CutDebugStateStore _cutDebugStateStore;
    private readonly ITornado3Adapter _sharedTornadoAdapter;
    private readonly SemaphoreSlim _stateSaveLock = new(1, 1);
    private AppPage _currentPage = AppPage.Normal;
    private ChannelOperationMode _operationMode = ChannelOperationMode.General;
    private bool _isSituationRoomExpanded;
    private bool _suppressAutomaticSave;
    private bool _isSyncingQueuedCutDurations;
    private int _automaticSaveRevision;
    private int? _windowX;
    private int? _windowY;
    private int? _windowWidth;
    private int? _windowHeight;
    private bool _isWindowMaximized;
    private SelectionOption<LogLevel?>? _selectedLogFilterOption;
    private readonly List<(BroadcastChannel Channel, CutListEntryViewModel Entry)> _allCutListEntries = [];
    private SelectionOption<BroadcastChannel?>? _selectedCutListFilterOption;
    private SelectionOption<CutCategory?>? _selectedCutListCategoryOption;
    private string _thumbnailGenerationStatus = string.Empty;

    public MainViewModel()
    {
        _stateStore = new AppStateStore();
        _logService = new LogService();
        Settings = new SettingsViewModel(new StationCatalogService().GetAll());
        _formatCatalogService = new FormatCatalogService(Settings.ImageRootPath);
        _thumbnailGeneratorService = new KarismaThumbnailGeneratorService(_logService);

        Data = new DataViewModel(_logService);
        var selectedStationProfile = Settings.BuildSelectedStationProfile();
        Data.SetConfiguredRegions(selectedStationProfile.RegionFilters);
        Data.SetSelectedStationContext(selectedStationProfile.Id, selectedStationProfile.Name);
        RestoreSelection = new RestoreSelectionViewModel();
        LogFilterOptions =
        [
            new SelectionOption<LogLevel?>(null, "전체"),
            new SelectionOption<LogLevel?>(LogLevel.Info, "정보"),
            new SelectionOption<LogLevel?>(LogLevel.Warning, "경고"),
            new SelectionOption<LogLevel?>(LogLevel.Error, "오류")
        ];
        CutListFilterOptions =
        [
            new SelectionOption<BroadcastChannel?>(null, "전체"),
            new SelectionOption<BroadcastChannel?>(BroadcastChannel.Normal, "노멀"),
            new SelectionOption<BroadcastChannel?>(BroadcastChannel.TopLeft, "좌상단"),
            new SelectionOption<BroadcastChannel?>(BroadcastChannel.Bottom, "하단"),
            new SelectionOption<BroadcastChannel?>(BroadcastChannel.VideoWall, "비디오월")
        ];
        CutListCategoryOptions = [];
        FilteredLogs = [];
        CutListItems = [];
        _selectedCutListFilterOption = CutListFilterOptions[0];
        RebuildCutListCategoryOptions();

        _cutDebugStateStore = new CutDebugStateStore();
        _cutDebugStateStore.SetDebugFeatureEnabled(Settings.IsDebugFeaturesEnabled);
        Settings.PropertyChanged += Settings_PropertyChanged;
        Data.PropertyChanged += Data_PropertyChanged;
        _sharedTornadoAdapter = KarismaTornado3Adapter.CreateOrFallback(
            _logService,
            () => Settings.ImageRootPath,
            _cutDebugStateStore,
            Settings.GetKarismaLayerNo);

        NormalChannel = CreateChannelViewModel(BroadcastChannel.Normal, "노멀", _sharedTornadoAdapter);
        TopLeftChannel = CreateChannelViewModel(BroadcastChannel.TopLeft, "좌상단", _sharedTornadoAdapter);
        BottomChannel = CreateChannelViewModel(BroadcastChannel.Bottom, "하단", _sharedTornadoAdapter);
        VideoWallChannel = CreateChannelViewModel(BroadcastChannel.VideoWall, "비디오월", _sharedTornadoAdapter);

        Channels = [NormalChannel, TopLeftChannel, BottomChannel, VideoWallChannel];
        UpdateChannelThumbnailLayouts();
        BuildCutListEntries();
        foreach (var channel in Channels)
        {
            channel.PropertyChanged += Channel_PropertyChanged;
            channel.FormatDurationChanged += Channel_FormatDurationChanged;
            channel.Queue.CollectionChanged += ChannelQueue_CollectionChanged;
        }

        SaveStateCommand = new AsyncRelayCommand(SaveStateAsync);
        RestoreStateCommand = new AsyncRelayCommand(RestoreAsync);
        GenerateCutThumbnailsCommand = new AsyncRelayCommand(GenerateCutThumbnailsAsync, CanGenerateCutThumbnails);
        ClearLogsCommand = new RelayCommand(_logService.Clear);
        ToggleSituationRoomCommand = new RelayCommand(ToggleSituationRoom);

        RestoreSelection.PropertyChanged += RestoreSelection_PropertyChanged;
        foreach (var station in Settings.Stations)
        {
            station.PropertyChanged += Station_PropertyChanged;
        }

        _logService.Entries.CollectionChanged += (_, _) => RebuildFilteredLogs();
        SelectedLogFilterOption = LogFilterOptions[0];
        ThumbnailGenerationStatus = BuildInitialThumbnailGenerationStatus();
        _logService.Info("SYSTEM_SPEC 기반 MVVM 구조를 초기화했습니다.");
        _ = WarmupSharedCgConnectionAsync();
    }

    public DataViewModel Data { get; }

    public SettingsViewModel Settings { get; }

    public RestoreSelectionViewModel RestoreSelection { get; }

    public ChannelScheduleViewModel NormalChannel { get; }

    public ChannelScheduleViewModel TopLeftChannel { get; }

    public ChannelScheduleViewModel BottomChannel { get; }

    public ChannelScheduleViewModel VideoWallChannel { get; }

    public IReadOnlyList<ChannelScheduleViewModel> Channels { get; }

    public AsyncRelayCommand SaveStateCommand { get; }

    public AsyncRelayCommand RestoreStateCommand { get; }

    public AsyncRelayCommand GenerateCutThumbnailsCommand { get; }

    public RelayCommand ClearLogsCommand { get; }

    public RelayCommand ToggleSituationRoomCommand { get; }

    public ObservableCollection<LogEntry> Logs => _logService.Entries;

    public ObservableCollection<LogEntry> FilteredLogs { get; }

    public ObservableCollection<CutListEntryViewModel> CutListItems { get; }

    public IReadOnlyList<SelectionOption<LogLevel?>> LogFilterOptions { get; }

    public IReadOnlyList<SelectionOption<BroadcastChannel?>> CutListFilterOptions { get; }

    public ObservableCollection<SelectionOption<CutCategory?>> CutListCategoryOptions { get; }

    public ChannelOperationMode OperationMode
    {
        get => _operationMode;
        set
        {
            if (SetProperty(ref _operationMode, value))
            {
                EnsureCurrentPageAvailableForMode();
                OnPropertyChanged(
                    nameof(IsGeneralOperationMode),
                    nameof(IsVideoWallOperationMode),
                    nameof(OperationModeLabel),
                    nameof(OperationModeBadgeText),
                    nameof(OperationModeDetailText),
                    nameof(NormalMenuVisibility),
                    nameof(TopLeftMenuVisibility),
                    nameof(BottomMenuVisibility),
                    nameof(VideoWallMenuVisibility),
                    nameof(GeneralIntegratedVisibility),
                    nameof(VideoWallIntegratedVisibility),
                    nameof(NormalVisibility),
                    nameof(TopLeftVisibility),
                    nameof(BottomVisibility),
                    nameof(VideoWallVisibility),
                    nameof(HeaderStatus),
                    nameof(CgIntegrationSummary),
                    nameof(CgIntegrationDetail),
                    nameof(TornadoConnectionSummary),
                    nameof(TornadoConnectionDetail));
                QueueAutomaticSave();
            }
        }
    }

    public AppPage CurrentPage
    {
        get => _currentPage;
        set
        {
            if (SetProperty(ref _currentPage, value))
            {
                OnPropertyChanged(
                    nameof(IntegratedScheduleVisibility),
                    nameof(NormalVisibility),
                    nameof(TopLeftVisibility),
                    nameof(BottomVisibility),
                    nameof(VideoWallVisibility),
                    nameof(PreElectionDataVisibility),
                    nameof(DataVisibility),
                    nameof(CareerPromiseDataVisibility),
                    nameof(CutListVisibility),
                    nameof(SettingsVisibility),
                    nameof(LogVisibility),
                    nameof(CurrentPageTitle),
                    nameof(HeaderStatus));
            }
        }
    }

    public string CurrentPageTitle => CurrentPage switch
    {
        AppPage.Normal => "노멀",
        AppPage.TopLeft => "좌상단",
        AppPage.Bottom => "하단",
        AppPage.VideoWall => "비디오월",
        AppPage.PreElectionData => "사전데이터",
        AppPage.TurnoutData => "투표데이터",
        AppPage.CountingData or AppPage.Data => "개표데이터",
        AppPage.CareerPromiseData => "공약데이터",
        AppPage.CutList => "컷리스트",
        AppPage.Settings => "설정",
        AppPage.Log => "로그",
        _ => GetDefaultPageTitle()
    };

    public bool IsGeneralOperationMode => OperationMode == ChannelOperationMode.General;

    public bool IsVideoWallOperationMode => OperationMode == ChannelOperationMode.VideoWall;

    public string OperationModeLabel => IsGeneralOperationMode ? "일반" : "비디오월";

    public string OperationModeBadgeText => IsGeneralOperationMode ? "일반 3채널" : "비디오월 단독";

    public string OperationModeDetailText => IsGeneralOperationMode
        ? "일반 모드에서는 노멀, 좌상단, 하단만 노출하고 운영합니다."
        : "비디오월 모드에서는 비디오월만 단독으로 노출하고 운영합니다.";

    public Visibility NormalMenuVisibility => IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility TopLeftMenuVisibility => IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility BottomMenuVisibility => IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility VideoWallMenuVisibility => IsVideoWallOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility GeneralIntegratedVisibility => IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility VideoWallIntegratedVisibility => IsVideoWallOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility IntegratedScheduleVisibility => CurrentPage == AppPage.IntegratedSchedule ? Visibility.Visible : Visibility.Collapsed;

    public Visibility NormalVisibility => CurrentPage == AppPage.Normal && IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility TopLeftVisibility => CurrentPage == AppPage.TopLeft && IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility BottomVisibility => CurrentPage == AppPage.Bottom && IsGeneralOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility VideoWallVisibility => CurrentPage == AppPage.VideoWall && IsVideoWallOperationMode ? Visibility.Visible : Visibility.Collapsed;

    public Visibility PreElectionDataVisibility => CurrentPage == AppPage.PreElectionData ? Visibility.Visible : Visibility.Collapsed;

    public Visibility DataVisibility => IsLiveDataPage(CurrentPage) ? Visibility.Visible : Visibility.Collapsed;

    public Visibility CareerPromiseDataVisibility => CurrentPage == AppPage.CareerPromiseData ? Visibility.Visible : Visibility.Collapsed;

    public Visibility CutListVisibility => CurrentPage == AppPage.CutList ? Visibility.Visible : Visibility.Collapsed;

    public Visibility SettingsVisibility => CurrentPage == AppPage.Settings ? Visibility.Visible : Visibility.Collapsed;

    public Visibility LogVisibility => CurrentPage == AppPage.Log ? Visibility.Visible : Visibility.Collapsed;

    public string CutListSummary
    {
        get
        {
            var totalCount = _allCutListEntries.Count;
            var filteredCount = CutListItems.Count;
            var selectedFilters = new List<string>();
            if (SelectedCutListFilterOption?.Value is not null)
            {
                selectedFilters.Add(SelectedCutListFilterOption.Label);
            }

            if (SelectedCutListCategoryOption?.Value is not null)
            {
                selectedFilters.Add(SelectedCutListCategoryOption.Label);
            }

            if (selectedFilters.Count == 0)
            {
                return $"등록 컷 {totalCount}개";
            }

            return $"{string.Join(" / ", selectedFilters)} 컷 {filteredCount}개 / 전체 {totalCount}개";
        }
    }

    public string CutThumbnailSummary => $"기본 썸네일 {_formatCatalogService.GetAll().Count(template => CutThumbnailAssetCatalog.HasThumbnail(template))}개 / 전체 {_formatCatalogService.GetAll().Count}개";

    public string ThumbnailGenerationStatus
    {
        get => _thumbnailGenerationStatus;
        private set => SetProperty(ref _thumbnailGenerationStatus, value);
    }

    public bool IsSituationRoomExpanded
    {
        get => _isSituationRoomExpanded;
        set
        {
            if (SetProperty(ref _isSituationRoomExpanded, value))
            {
                OnPropertyChanged(nameof(SituationRoomBodyVisibility), nameof(SituationRoomToggleText));
            }
        }
    }

    public Visibility SituationRoomBodyVisibility => IsSituationRoomExpanded ? Visibility.Visible : Visibility.Collapsed;

    public string SituationRoomToggleText => IsSituationRoomExpanded ? "상황실 접기" : "상황실 펼치기";

    public ImageSource? SelectedStationLogo
    {
        get
        {
            var logoPath = TryGetSelectedStationLogoPath();
            return logoPath is null ? null : new BitmapImage(new Uri(logoPath, UriKind.Absolute));
        }
    }

    public Visibility SelectedStationLogoVisibility => TryGetSelectedStationLogoPath() is null
        ? Visibility.Collapsed
        : Visibility.Visible;

    public SelectionOption<LogLevel?>? SelectedLogFilterOption
    {
        get => _selectedLogFilterOption;
        set
        {
            if (value is null)
            {
                return;
            }

            if (SetProperty(ref _selectedLogFilterOption, value))
            {
                RebuildFilteredLogs();
                OnPropertyChanged(nameof(LogFilterSummary));
            }
        }
    }

    public SelectionOption<BroadcastChannel?>? SelectedCutListFilterOption
    {
        get => _selectedCutListFilterOption;
        set
        {
            if (value is null)
            {
                return;
            }

            if (SetProperty(ref _selectedCutListFilterOption, value))
            {
                RebuildCutListCategoryOptions();
                ApplyCutListFilter();
            }
        }
    }

    public SelectionOption<CutCategory?>? SelectedCutListCategoryOption
    {
        get => _selectedCutListCategoryOption;
        set
        {
            if (value is null)
            {
                return;
            }

            if (SetProperty(ref _selectedCutListCategoryOption, value))
            {
                ApplyCutListFilter();
            }
        }
    }

    public string LogFilterSummary => $"표시 {FilteredLogs.Count}건 / 전체 {Logs.Count}건";

    public string CgIntegrationSummary => IsCgConnected ? "Connected" : "Disconnected";

    public Brush CgIntegrationBrush => IsCgConnected ? ConnectedStatusBrush : DisconnectedStatusBrush;

    public Brush DataNavigationIconBrush => Data.HasLiveDataSignal
        ? DataReceivingNavigationBrush
        : DataWaitingNavigationBrush;

    public string CgIntegrationDetail
    {
        get
        {
            var activeChannels = GetActiveChannels().ToArray();
            if (activeChannels.Length == 0)
            {
                return "운영 대상 없음";
            }

            var liveChannels = activeChannels
                .Where(channel => channel.IsLiveCg)
                .ToArray();

            if (liveChannels.Length == 0)
            {
                return "Mock Adapter";
            }

            var targets = liveChannels
                .Select(channel => channel.CgConnectionTarget)
                .Distinct(StringComparer.Ordinal)
                .ToArray();

            return $"{liveChannels[0].CgBackendName} / {string.Join(", ", targets)}";
        }
    }

    public bool IsCgConnected
    {
        get
        {
            var activeChannels = GetActiveChannels().ToArray();
            if (activeChannels.Length == 0)
            {
                return false;
            }

            var liveChannels = activeChannels
                .Where(channel => channel.IsLiveCg)
                .ToArray();

            return liveChannels.Length > 0 && liveChannels.All(channel => channel.IsCgConnected);
        }
    }

    public string TornadoConnectionSummary
    {
        get
        {
            var activeChannels = GetActiveChannels().ToArray();
            if (activeChannels.Length == 0)
            {
                return "채널 없음";
            }

            var healthyCount = activeChannels.Count(channel => channel.AdapterState != TornadoConnectionState.Error);
            return $"{healthyCount}/{activeChannels.Length} 채널 정상";
        }
    }

    public string TornadoConnectionDetail
    {
        get
        {
            var activeChannels = GetActiveChannels().ToArray();
            if (activeChannels.Length == 0)
            {
                return "운영 대상 없음";
            }

            if (activeChannels.Any(channel => channel.AdapterState == TornadoConnectionState.Error))
            {
                return "오류 채널 확인 필요";
            }

            if (activeChannels.Any(channel => channel.AdapterState == TornadoConnectionState.Sending))
            {
                return "전송 중 채널 포함";
            }

            if (activeChannels.Any(channel => channel.AdapterState == TornadoConnectionState.OnAir))
            {
                return "송출 중 채널 포함";
            }

            return "전체 준비 상태";
        }
    }

    public string HeaderStatus => $"{Settings.SelectedStation.Name} / {CurrentPageTitle} / {Data.BroadcastPhaseBadgeText} / {OperationModeLabel}";

    public void Navigate(string tag)
    {
        var targetPage = tag switch
        {
            "normal" when IsGeneralOperationMode => AppPage.Normal,
            "top-left" when IsGeneralOperationMode => AppPage.TopLeft,
            "bottom" when IsGeneralOperationMode => AppPage.Bottom,
            "videowall" when IsVideoWallOperationMode => AppPage.VideoWall,
            "pre-election-data" => AppPage.PreElectionData,
            "turnout-data" => AppPage.TurnoutData,
            "counting-data" => AppPage.CountingData,
            "data" => Data.IsPreElectionPhase ? AppPage.TurnoutData : AppPage.CountingData,
            "career-promises" => AppPage.CareerPromiseData,
            "cut-list" => AppPage.CutList,
            "settings" => AppPage.Settings,
            "log" => AppPage.Log,
            _ => GetDefaultPage()
        };

        CurrentPage = targetPage;
        if (targetPage == AppPage.CareerPromiseData)
        {
            Data.EnsureCareerPromiseElectionType();
        }

        SyncBroadcastPhaseForLiveDataPage(targetPage);
    }

    public bool IsPageAvailable(AppPage page)
    {
        return page switch
        {
            AppPage.Normal or AppPage.TopLeft or AppPage.Bottom => IsGeneralOperationMode,
            AppPage.VideoWall => IsVideoWallOperationMode,
            _ => true
        };
    }

    private static bool IsLiveDataPage(AppPage page)
    {
        return page is AppPage.TurnoutData or AppPage.CountingData or AppPage.Data;
    }

    private void SyncBroadcastPhaseForLiveDataPage(AppPage page)
    {
        var targetPhase = page switch
        {
            AppPage.TurnoutData => BroadcastPhase.PreElection,
            AppPage.CountingData or AppPage.Data or AppPage.CareerPromiseData => BroadcastPhase.Counting,
            _ => (BroadcastPhase?)null
        };

        if (targetPhase is { } phase)
        {
            Data.ApplyBroadcastPhase(phase);
        }
    }

    public async Task<bool> HasRestorableStateAsync()
    {
        return await _stateStore.LoadAsync() is not null;
    }

    public async Task<(int? X, int? Y, int Width, int Height, bool IsMaximized)?> GetSavedWindowPlacementAsync()
    {
        var state = await _stateStore.LoadAsync();
        if (state is null || state.WindowWidth <= 0 || state.WindowHeight <= 0)
        {
            return null;
        }

        _windowX = state.WindowX;
        _windowY = state.WindowY;
        _windowWidth = state.WindowWidth;
        _windowHeight = state.WindowHeight;
        _isWindowMaximized = state.IsWindowMaximized;
        return (state.WindowX, state.WindowY, state.WindowWidth, state.WindowHeight, state.IsWindowMaximized);
    }

    public async Task RestoreStartupStateAsync()
    {
        var state = await _stateStore.LoadAsync();
        if (state is null)
        {
            return;
        }

        _suppressAutomaticSave = true;
        try
        {
            RestoreSelection.RestoreSchedules = state.AutoRestoreSchedules;
            RestoreSelection.RestoreStations = state.AutoRestoreStations;
            RestoreSelection.RestoreStatusValues = state.AutoRestoreStatusValues;
            _windowX = state.WindowX;
            _windowY = state.WindowY;
            _windowWidth = state.WindowWidth > 0 ? state.WindowWidth : _windowWidth;
            _windowHeight = state.WindowHeight > 0 ? state.WindowHeight : _windowHeight;
            _isWindowMaximized = state.IsWindowMaximized;
            ApplyState(state);
        }
        finally
        {
            _suppressAutomaticSave = false;
        }

        _logService.Info("저장한 시작 옵션에 따라 상태를 자동 복원했습니다.");
        _ = WarmupSharedCgConnectionAsync();
    }

    public async Task RestoreAsync()
    {
        var state = await _stateStore.LoadAsync();
        if (state is null)
        {
            _logService.Warning("복원 가능한 저장 상태가 없습니다.");
            return;
        }

        _suppressAutomaticSave = true;
        try
        {
            ApplyState(state);
        }
        finally
        {
            _suppressAutomaticSave = false;
        }

        _logService.Info("저장 상태 복원을 완료했습니다.");
    }

    public async Task SaveStateAsync()
    {
        await SaveStateCoreAsync(writeLog: true);
    }

    public async Task ShutdownAsync()
    {
        CancelPendingAutomaticSave();
        await SaveStateCoreAsync(writeLog: false);
        if (_sharedTornadoAdapter is IDisposable disposableAdapter)
        {
            disposableAdapter.Dispose();
        }

        Data.Dispose();
    }

    public void UpdateWindowPlacement(int? x, int? y, int width, int height, bool isMaximized)
    {
        if (width <= 0 || height <= 0)
        {
            return;
        }

        _windowX = x;
        _windowY = y;
        _windowWidth = width;
        _windowHeight = height;
        _isWindowMaximized = isMaximized;
        QueueAutomaticSave();
    }

    public void ApplyBroadcastPhase(BroadcastPhase phase)
    {
        Data.ApplyBroadcastPhase(phase);
        if (IsLiveDataPage(CurrentPage))
        {
            CurrentPage = phase == BroadcastPhase.PreElection
                ? AppPage.TurnoutData
                : AppPage.CountingData;
        }

        OnPropertyChanged(nameof(HeaderStatus));
    }

    public void ApplyOperationMode(ChannelOperationMode mode)
    {
        if (OperationMode == mode)
        {
            return;
        }

        OperationMode = mode;
        _logService.Info($"운영 모드를 {OperationModeLabel}로 전환했습니다.");
    }

    private void ToggleSituationRoom()
    {
        IsSituationRoomExpanded = !IsSituationRoomExpanded;
    }

    private bool CanGenerateCutThumbnails()
    {
        return KarismaThumbnailGeneratorService.IsGenerationAvailable() &&
               !string.IsNullOrWhiteSpace(Settings.ImageRootPath);
    }

    private async Task GenerateCutThumbnailsAsync()
    {
        ThumbnailGenerationStatus = "썸네일 생성 중...";

        try
        {
            var templates = _formatCatalogService.GetAll();
            var result = _sharedTornadoAdapter is KarismaTornado3Adapter karismaAdapter
                ? await karismaAdapter.GenerateThumbnailsAsync(
                    templates,
                    Settings.SelectedStationVideoWallLayoutPreset,
                    CancellationToken.None)
                : await _thumbnailGeneratorService.GenerateAsync(
                    templates,
                    Settings.ImageRootPath,
                    Settings.SelectedStationVideoWallLayoutPreset,
                    CancellationToken.None);

            RefreshCutListThumbnails();
            foreach (var channel in Channels)
            {
                channel.RefreshSelectedFormatPreview();
                channel.RefreshQueueThumbnails();
            }
            ThumbnailGenerationStatus = result.FailedCount == 0
                ? $"썸네일 {result.GeneratedCount}개 생성 완료"
                : $"썸네일 {result.GeneratedCount}개 생성, {result.FailedCount}개 실패";

            _logService.Info($"[Thumbnail] 생성 완료 / success={result.GeneratedCount} failed={result.FailedCount}");
        }
        catch (OperationCanceledException)
        {
            ThumbnailGenerationStatus = "썸네일 생성이 취소되었습니다.";
            _logService.Warning("[Thumbnail] 생성이 취소되었습니다.");
        }
        catch (Exception ex)
        {
            ThumbnailGenerationStatus = $"썸네일 생성 실패: {ex.Message}";
            _logService.Error($"[Thumbnail] 생성 실패: {ex.Message}");
        }
    }

    private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName is nameof(SettingsViewModel.IsDebugFeaturesEnabled))
        {
            _cutDebugStateStore.SetDebugFeatureEnabled(Settings.IsDebugFeaturesEnabled);
        }

        if (args.PropertyName is nameof(SettingsViewModel.SelectedStation) or nameof(SettingsViewModel.SelectedStationId))
        {
            var selectedStationProfile = Settings.BuildSelectedStationProfile();
            Data.SetConfiguredRegions(selectedStationProfile.RegionFilters);
            Data.SetSelectedStationContext(selectedStationProfile.Id, selectedStationProfile.Name);
            OnPropertyChanged(nameof(HeaderStatus), nameof(SelectedStationLogo), nameof(SelectedStationLogoVisibility));
        }

        if (args.PropertyName is not nameof(SettingsViewModel.SelectedStationLogoAssetPath)
            and not nameof(SettingsViewModel.SelectedStationRegions)
            and not nameof(SettingsViewModel.SelectedStationRegionSummary))
        {
            QueueAutomaticSave();
        }

        if (args.PropertyName is nameof(SettingsViewModel.SelectedStationId)
            or nameof(SettingsViewModel.SelectedStationVideoWallLayoutPreset)
            or nameof(SettingsViewModel.ImageRootPath))
        {
            if (args.PropertyName == nameof(SettingsViewModel.ImageRootPath))
            {
                ReloadFormatCatalog();
            }
            else
            {
                UpdateChannelThumbnailLayouts();
                UpdateCutListThumbnailLayouts();
            }

            _ = WarmupSharedCgConnectionAsync();
            GenerateCutThumbnailsCommand.NotifyCanExecuteChanged();
        }
    }

    private void Data_PropertyChanged(object? sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName is nameof(DataViewModel.DistrictName) or nameof(DataViewModel.HeaderMetricSummary) or nameof(DataViewModel.BroadcastPhase))
        {
            OnPropertyChanged(nameof(HeaderStatus));
        }

        if (args.PropertyName is nameof(DataViewModel.HasLiveDataSignal))
        {
            OnPropertyChanged(nameof(DataNavigationIconBrush));
        }

        if (args.PropertyName is nameof(DataViewModel.IsPollingEnabled)
            or nameof(DataViewModel.BroadcastPhase)
            or nameof(DataViewModel.ElectionType)
            or nameof(DataViewModel.DistrictName)
            or nameof(DataViewModel.DistrictCode)
            or nameof(DataViewModel.ShowOnlyConfiguredRegions)
            or nameof(DataViewModel.CloseRaceThresholdPercent)
            or nameof(DataViewModel.SuperCloseRaceThresholdPercent)
            or nameof(DataViewModel.TotalExpectedVotes)
            or nameof(DataViewModel.TurnoutVotes))
        {
            QueueAutomaticSave();
        }
    }

    private void RestoreSelection_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        QueueAutomaticSave();
    }

    private void Channel_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(ChannelScheduleViewModel.LoopEnabled)
            or nameof(ChannelScheduleViewModel.EmptyScheduleBehavior))
        {
            QueueAutomaticSave();
        }

        if (e.PropertyName is nameof(ChannelScheduleViewModel.AdapterState)
            or nameof(ChannelScheduleViewModel.AdapterStateLabel)
            or nameof(ChannelScheduleViewModel.IsCgConnected))
        {
            OnPropertyChanged(
                nameof(IsCgConnected),
                nameof(CgIntegrationSummary),
                nameof(CgIntegrationBrush),
                nameof(CgIntegrationDetail),
                nameof(TornadoConnectionSummary),
                nameof(TornadoConnectionDetail));
        }
    }

    private void ChannelQueue_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs args)
    {
        if (args.OldItems is not null)
        {
            foreach (var item in args.OldItems.OfType<ChannelScheduleItem>())
            {
                item.PropertyChanged -= QueueItem_PropertyChanged;
            }
        }

        if (args.NewItems is not null)
        {
            foreach (var item in args.NewItems.OfType<ChannelScheduleItem>())
            {
                item.PropertyChanged -= QueueItem_PropertyChanged;
                item.PropertyChanged += QueueItem_PropertyChanged;
            }
        }

        if (args.Action is NotifyCollectionChangedAction.Add
            or NotifyCollectionChangedAction.Remove
            or NotifyCollectionChangedAction.Move
            or NotifyCollectionChangedAction.Replace
            or NotifyCollectionChangedAction.Reset)
        {
            QueueAutomaticSave();
        }
    }

    private void QueueItem_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (_isSyncingQueuedCutDurations ||
            e.PropertyName != nameof(ChannelScheduleItem.DefaultCutDurationSeconds) ||
            sender is not ChannelScheduleItem item)
        {
            return;
        }

        ApplyScheduleDurationToCutList(item);
    }

    private void Channel_FormatDurationChanged(object? sender, FormatTemplateDefinition template)
    {
        OnCutDurationChanged(template);
    }

    private void Station_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(StationFilterItemViewModel.RegionFiltersText)
            or nameof(StationFilterItemViewModel.RegionSelectionSummary)
            or nameof(StationFilterItemViewModel.SelectedRegionCount))
        {
            if (sender == Settings.SelectedStation)
            {
                var selectedStationProfile = Settings.BuildSelectedStationProfile();
                Data.SetConfiguredRegions(selectedStationProfile.RegionFilters);
                Data.SetSelectedStationContext(selectedStationProfile.Id, selectedStationProfile.Name);
            }

            QueueAutomaticSave();
        }
    }

    private void ApplyState(AppState state)
    {
        Settings.IsDebugFeaturesEnabled = state.IsDebugFeaturesEnabled;
        Settings.NormalLayerNo = state.NormalLayerNo;
        Settings.TopLeftLayerNo = state.TopLeftLayerNo;
        Settings.BottomLayerNo = state.BottomLayerNo;
        Settings.VideoWallLayerNo = state.VideoWallLayerNo;

        if (RestoreSelection.RestoreStations)
        {
            Settings.SelectedStationId = state.SelectedStationId;
            Settings.ImageRootPath = state.ImageRootPath;
            foreach (var station in Settings.Stations)
            {
                if (state.StationRegionFilters.TryGetValue(station.Id, out var filters))
                {
                    station.RegionFiltersText = filters;
                }

                if (state.StationVideoWallLayouts.TryGetValue(station.Id, out var videoWallLayoutValue) &&
                    Enum.TryParse<VideoWallLayoutPreset>(videoWallLayoutValue, ignoreCase: true, out var videoWallLayoutPreset))
                {
                    station.VideoWallLayoutPreset = videoWallLayoutPreset;
                }
            }

            UpdateChannelThumbnailLayouts();
            UpdateCutListThumbnailLayouts();
        }

        if (RestoreSelection.RestoreStatusValues)
        {
            if (!Enum.TryParse<ChannelOperationMode>(state.OperationMode, ignoreCase: true, out var operationMode))
            {
                operationMode = ChannelOperationMode.General;
            }

            OperationMode = operationMode;

            if (!Enum.TryParse<BroadcastPhase>(state.BroadcastPhase, ignoreCase: true, out var broadcastPhase))
            {
                broadcastPhase = BroadcastPhase.Counting;
            }

            Data.BroadcastPhase = broadcastPhase;
            Data.ElectionType = state.ElectionType;
            Data.DistrictName = string.IsNullOrWhiteSpace(state.DistrictName) ? Data.DistrictName : state.DistrictName;
            Data.DistrictCode = string.IsNullOrWhiteSpace(state.DistrictCode) ? Data.DistrictCode : state.DistrictCode;
            Data.ShowOnlyConfiguredRegions = state.ShowOnlyConfiguredRegions;
            Data.SetCloseRaceThresholds(state.CloseRaceThresholdPercent, state.SuperCloseRaceThresholdPercent);
            Data.TotalExpectedVotes = state.TotalExpectedVotes > 0 ? state.TotalExpectedVotes : Data.TotalExpectedVotes;
            Data.TurnoutVotes = state.TurnoutVotes;
            Data.IsPollingEnabled = state.IsPollingEnabled;
            Data.PollingIntervalSeconds = DataViewModel.FixedPollingIntervalSeconds;
            Data.ReplaceCandidates(state.Candidates.Select(candidate => new CandidateEntry
            {
                CandidateCode = candidate.CandidateCode,
                BallotNumber = string.IsNullOrWhiteSpace(candidate.BallotNumber) ? candidate.CandidateCode : candidate.BallotNumber,
                Name = candidate.Name,
                Party = candidate.Party,
                VoteCount = candidate.VoteCount,
                VoteRate = candidate.VoteRate,
                HasImage = candidate.HasImage,
                ManualJudgement = candidate.ManualJudgement
            }));
        }

        if (RestoreSelection.RestoreSchedules)
        {
            RestoreChannelState(NormalChannel, state, BroadcastChannel.Normal);
            RestoreChannelState(TopLeftChannel, state, BroadcastChannel.TopLeft);
            RestoreChannelState(BottomChannel, state, BroadcastChannel.Bottom);
            RestoreChannelState(VideoWallChannel, state, BroadcastChannel.VideoWall);
        }

        ApplyCutDurations(state.CutDurations);
        SyncAllQueuedCutDurations();

        OnPropertyChanged(nameof(HeaderStatus));
    }

    private void QueueAutomaticSave()
    {
        if (_suppressAutomaticSave)
        {
            return;
        }

        var revision = Interlocked.Increment(ref _automaticSaveRevision);
        _ = RunAutomaticSaveAsync(revision);
    }

    private async Task RunAutomaticSaveAsync(int revision)
    {
        try
        {
            await Task.Delay(AutomaticSaveDelay).ConfigureAwait(false);
            if (_suppressAutomaticSave || revision != Volatile.Read(ref _automaticSaveRevision))
            {
                return;
            }

            await SaveStateCoreAsync(writeLog: false).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logService.Warning($"자동 저장 실패: {ex.Message}");
        }
    }

    private void CancelPendingAutomaticSave()
    {
        Interlocked.Increment(ref _automaticSaveRevision);
    }

    private async Task SaveStateCoreAsync(bool writeLog)
    {
        await _stateSaveLock.WaitAsync();
        try
        {
            var state = new AppState
            {
                SelectedStationId = Settings.SelectedStationId,
                ImageRootPath = Settings.ImageRootPath,
                AutoRestoreSchedules = RestoreSelection.RestoreSchedules,
                AutoRestoreStations = RestoreSelection.RestoreStations,
                AutoRestoreStatusValues = RestoreSelection.RestoreStatusValues,
                WindowX = _windowX,
                WindowY = _windowY,
                WindowWidth = _windowWidth ?? 0,
                WindowHeight = _windowHeight ?? 0,
                IsWindowMaximized = _isWindowMaximized,
                IsDebugFeaturesEnabled = Settings.IsDebugFeaturesEnabled,
                NormalLayerNo = Settings.GetKarismaLayerNo(BroadcastChannel.Normal),
                TopLeftLayerNo = Settings.GetKarismaLayerNo(BroadcastChannel.TopLeft),
                BottomLayerNo = Settings.GetKarismaLayerNo(BroadcastChannel.Bottom),
                VideoWallLayerNo = Settings.GetKarismaLayerNo(BroadcastChannel.VideoWall),
                OperationMode = OperationMode.ToString(),
                BroadcastPhase = Data.BroadcastPhase.ToString(),
                IsPollingEnabled = Data.IsPollingEnabled,
                PollingIntervalSeconds = DataViewModel.FixedPollingIntervalSeconds,
                ElectionType = Data.ElectionType,
                DistrictName = Data.DistrictName,
                DistrictCode = Data.DistrictCode,
                ShowOnlyConfiguredRegions = Data.ShowOnlyConfiguredRegions,
                CloseRaceThresholdPercent = Data.CloseRaceThresholdPercent,
                SuperCloseRaceThresholdPercent = Data.SuperCloseRaceThresholdPercent,
                TotalExpectedVotes = Data.TotalExpectedVotes,
                TurnoutVotes = Data.TurnoutVotes,
                Candidates = Data.Candidates.Select(candidate => new CandidateState
                {
                    CandidateCode = candidate.CandidateCode,
                    BallotNumber = candidate.BallotNumber,
                    Name = candidate.Name,
                    Party = candidate.Party,
                    VoteCount = candidate.VoteCount,
                    VoteRate = candidate.VoteRate,
                    HasImage = candidate.HasImage,
                    ManualJudgement = candidate.ManualJudgement
                }).ToList(),
                Channels = BuildChannelStateMap(),
                CutDurations = BuildCutDurationMap(),
                StationRegionFilters = Settings.Stations.ToDictionary(station => station.Id, station => station.RegionFiltersText),
                StationVideoWallLayouts = Settings.Stations.ToDictionary(
                    station => station.Id,
                    station => station.VideoWallLayoutPreset.ToString())
            };

            await _stateStore.SaveAsync(state);
            if (writeLog)
            {
                _logService.Info("현재 통합 방송 상태값을 저장했습니다.");
            }
        }
        finally
        {
            _stateSaveLock.Release();
        }
    }

    private async Task WarmupSharedCgConnectionAsync()
    {
        if (!_sharedTornadoAdapter.IsLiveCg)
        {
            return;
        }

        try
        {
            await _sharedTornadoAdapter.EnsureConnectedAsync(CancellationToken.None).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logService.Warning($"CG startup connection attempt failed: {ex.Message}");
        }
    }

    private ChannelScheduleViewModel CreateChannelViewModel(BroadcastChannel channel, string title, ITornado3Adapter adapter)
    {
        var queue = new ObservableCollection<ChannelScheduleItem>();
        var engine = new ChannelScheduleEngine(
            channel,
            queue,
            adapter,
            Data,
            Settings.BuildSelectedStationProfile,
            () => Settings.ImageRootPath,
            () => Settings.SelectedStationVideoWallLayoutPreset,
            formatId => _formatCatalogService.FindById(formatId),
            _logService);

        var cutDebug = _cutDebugStateStore.Get(channel);
        return new ChannelScheduleViewModel(
            channel,
            title,
            _formatCatalogService.GetByChannel(channel),
            Data,
            adapter,
            cutDebug,
            _cutDebugStateStore,
            engine,
            _logService);
    }

    private void ReloadFormatCatalog()
    {
        var cutDurations = BuildCutDurationMap();
        _formatCatalogService = new FormatCatalogService(Settings.ImageRootPath);

        NormalChannel.UpdateFormats(_formatCatalogService.GetByChannel(BroadcastChannel.Normal));
        TopLeftChannel.UpdateFormats(_formatCatalogService.GetByChannel(BroadcastChannel.TopLeft));
        BottomChannel.UpdateFormats(_formatCatalogService.GetByChannel(BroadcastChannel.Bottom));
        VideoWallChannel.UpdateFormats(_formatCatalogService.GetByChannel(BroadcastChannel.VideoWall));

        UpdateChannelThumbnailLayouts();
        BuildCutListEntries();
        ApplyCutDurations(cutDurations);
        SyncAllQueuedCutDurations();
    }

    private Dictionary<string, ChannelState> BuildChannelStateMap()
    {
        return Channels.ToDictionary(
            channel => channel.Channel.ToString(),
            channel => new ChannelState
            {
                LoopEnabled = channel.LoopEnabled,
                EmptyScheduleBehavior = channel.EmptyScheduleBehavior,
                Items = channel.Queue.Select(item => new ScheduleItemState
                {
                    Id = item.Id,
                    FormatId = item.FormatId,
                    FormatName = item.FormatName,
                    Description = item.Description,
                    Channel = item.Channel,
                    RequiresImage = item.RequiresImage,
                    DefaultCutDurationSeconds = item.DefaultCutDurationSeconds,
                    TotalCuts = item.TotalCuts,
                    RegionScope = item.RegionScope,
                    ScheduleElectionType = item.ScheduleElectionType,
                    RegionLabel = item.RegionLabel,
                    RegionCode = item.RegionCode,
                    State = item.State
                }).ToList()
            });
    }

    private Dictionary<string, double> BuildCutDurationMap()
    {
        var durations = new Dictionary<string, double>(StringComparer.Ordinal);
        foreach (var template in _formatCatalogService.GetAll())
        {
            foreach (var cut in template.Cuts)
            {
                durations[BuildCutDurationKey(template.Id, cut.Name)] = cut.DurationSeconds;
            }
        }

        return durations;
    }

    private void EnsureCurrentPageAvailableForMode()
    {
        if (!IsPageAvailable(CurrentPage))
        {
            CurrentPage = GetDefaultPage();
        }
    }

    private AppPage GetDefaultPage()
    {
        return IsVideoWallOperationMode ? AppPage.VideoWall : AppPage.Normal;
    }

    private string GetDefaultPageTitle()
    {
        return GetDefaultPage() switch
        {
            AppPage.VideoWall => "비디오월",
            _ => "노멀"
        };
    }

    private string? TryGetSelectedStationLogoPath()
    {
        var relativePath = Settings.SelectedStationLogoAssetPath;
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            return null;
        }

        var normalizedPath = relativePath
            .Replace('\\', Path.DirectorySeparatorChar)
            .Replace('/', Path.DirectorySeparatorChar);
        var absolutePath = Path.Combine(AppContext.BaseDirectory, normalizedPath);
        return File.Exists(absolutePath) ? absolutePath : null;
    }

    private IEnumerable<ChannelScheduleViewModel> GetActiveChannels()
    {
        return IsGeneralOperationMode
            ? [NormalChannel, TopLeftChannel, BottomChannel]
            : [VideoWallChannel];
    }

    private void RebuildFilteredLogs()
    {
        var selectedLevel = SelectedLogFilterOption?.Value;
        var filteredEntries = Logs
            .Where(entry => selectedLevel is null || entry.Level == selectedLevel.Value)
            .ToArray();

        FilteredLogs.Clear();
        foreach (var entry in filteredEntries)
        {
            FilteredLogs.Add(entry);
        }

        OnPropertyChanged(nameof(LogFilterSummary));
    }

    private void BuildCutListEntries()
    {
        var entries = _formatCatalogService
            .GetAll()
            .OrderBy(template => CutListElectionCategoryResolver.Resolve(template.Name))
            .ThenBy(template => template.RecommendedChannel)
            .ThenBy(template => template.Name, StringComparer.Ordinal)
            .SelectMany(template => template.Cuts.Select(cut => (
                template.RecommendedChannel,
                Entry: new CutListEntryViewModel(
                    template,
                    cut,
                    OnCutDurationChanged,
                    Settings.SelectedStationVideoWallLayoutPreset))))
            .ToArray();

        _allCutListEntries.Clear();
        _allCutListEntries.AddRange(entries);
        RebuildCutListCategoryOptions();
        ApplyCutListFilter();
        OnPropertyChanged(nameof(CutThumbnailSummary));
    }

    private void OnCutDurationChanged(FormatTemplateDefinition template)
    {
        SyncQueuedCutDurations(template);
        QueueAutomaticSave();
    }

    private void ApplyScheduleDurationToCutList(ChannelScheduleItem item)
    {
        var template = _formatCatalogService.FindById(item.FormatId);
        if (template is null)
        {
            QueueAutomaticSave();
            return;
        }

        var normalized = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(item.DefaultCutDurationSeconds, template);
        if (Math.Abs(item.DefaultCutDurationSeconds - normalized) >= 0.001d)
        {
            _isSyncingQueuedCutDurations = true;
            try
            {
                item.DefaultCutDurationSeconds = normalized;
            }
            finally
            {
                _isSyncingQueuedCutDurations = false;
            }
        }

        var changed = false;
        foreach (var cut in template.Cuts)
        {
            if (Math.Abs(cut.DurationSeconds - normalized) < 0.001d)
            {
                continue;
            }

            cut.DurationSeconds = normalized;
            changed = true;
        }

        if (changed)
        {
            RefreshCutListEntries(template);
            SyncQueuedCutDurations(template);
            QueueAutomaticSave();
        }
    }

    private void RefreshCutListEntries(FormatTemplateDefinition template)
    {
        foreach (var item in _allCutListEntries.Where(item =>
                     string.Equals(item.Entry.FormatId, template.Id, StringComparison.Ordinal)))
        {
            item.Entry.RefreshFromSource();
        }
    }

    private void ApplyCutDurations(IReadOnlyDictionary<string, double>? durations)
    {
        if (durations is null || durations.Count == 0)
        {
            foreach (var item in _allCutListEntries)
            {
                item.Entry.RefreshFromSource();
            }

            return;
        }

        foreach (var template in _formatCatalogService.GetAll())
        {
            foreach (var cut in template.Cuts)
            {
                if (!durations.TryGetValue(BuildCutDurationKey(template.Id, cut.Name), out var duration) ||
                    double.IsNaN(duration) ||
                    double.IsInfinity(duration))
                {
                    continue;
                }

                cut.DurationSeconds = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(duration, template);
            }
        }

        foreach (var item in _allCutListEntries)
        {
            item.Entry.RefreshFromSource();
        }
    }

    private void ApplyCutListFilter()
    {
        var selectedChannel = SelectedCutListFilterOption?.Value;
        var selectedCategory = SelectedCutListCategoryOption?.Value;
        var filteredEntries = _allCutListEntries
            .Where(item =>
                (selectedChannel is null || item.Channel == selectedChannel.Value) &&
                (selectedCategory is null || item.Entry.IsInCategory(selectedCategory.Value)))
            .Select(item => item.Entry)
            .ToArray();

        CutListItems.Clear();
        foreach (var entry in filteredEntries)
        {
            CutListItems.Add(entry);
        }

        OnPropertyChanged(nameof(CutListSummary));
    }

    private void RebuildCutListCategoryOptions()
    {
        var selectedChannel = SelectedCutListFilterOption?.Value;
        var selectedCategory = SelectedCutListCategoryOption?.Value;
        var entriesInSelectedChannel = _allCutListEntries
            .Where(item => selectedChannel is null || item.Channel == selectedChannel.Value)
            .Select(item => item.Entry)
            .ToArray();
        var options = CreateCutListCategoryOptions(entriesInSelectedChannel);

        CutListCategoryOptions.Clear();
        foreach (var option in options)
        {
            CutListCategoryOptions.Add(option);
        }

        var nextSelectedOption = selectedCategory.HasValue
            ? CutListCategoryOptions.FirstOrDefault(option => option.Value == selectedCategory.Value)
            : null;
        nextSelectedOption ??= CutListCategoryOptions.FirstOrDefault();

        if (!ReferenceEquals(_selectedCutListCategoryOption, nextSelectedOption))
        {
            _selectedCutListCategoryOption = nextSelectedOption;
            OnPropertyChanged(nameof(SelectedCutListCategoryOption));
        }
    }

    private static IReadOnlyList<SelectionOption<CutCategory?>> CreateCutListCategoryOptions(
        IReadOnlyList<CutListEntryViewModel> entries)
    {
        List<SelectionOption<CutCategory?>> options = [new(null, "전체보기")];
        var seenResultKeys = new HashSet<string>(StringComparer.Ordinal)
        {
            BuildCutListCategoryResultKey(entries.Select(entry => entry.FormatId))
        };

        foreach (var category in CutCategoryResolver.GetOrderedCategories())
        {
            var matchingEntries = entries
                .Where(entry => entry.IsInCategory(category))
                .ToArray();
            if (matchingEntries.Length == 0)
            {
                continue;
            }

            if (!seenResultKeys.Add(BuildCutListCategoryResultKey(matchingEntries.Select(entry => entry.FormatId))))
            {
                continue;
            }

            options.Add(new SelectionOption<CutCategory?>(category, CutCategoryResolver.GetLabel(category)));
        }

        return options;
    }

    private static string BuildCutListCategoryResultKey(IEnumerable<string> formatIds)
    {
        return string.Join(
            "\u001F",
            formatIds.OrderBy(formatId => formatId, StringComparer.Ordinal));
    }

    private void RefreshCutListThumbnails()
    {
        foreach (var item in _allCutListEntries)
        {
            item.Entry.RefreshThumbnail();
        }

        OnPropertyChanged(nameof(CutThumbnailSummary));
    }

    private void UpdateChannelThumbnailLayouts()
    {
        var videoWallLayoutPreset = Settings.SelectedStationVideoWallLayoutPreset;
        foreach (var channel in Channels)
        {
            channel.UpdateVideoWallLayoutPreset(videoWallLayoutPreset);
        }
    }

    private void UpdateCutListThumbnailLayouts()
    {
        var videoWallLayoutPreset = Settings.SelectedStationVideoWallLayoutPreset;
        foreach (var item in _allCutListEntries)
        {
            item.Entry.UpdateVideoWallLayoutPreset(videoWallLayoutPreset);
        }
    }

    private string BuildInitialThumbnailGenerationStatus()
    {
        return KarismaThumbnailGeneratorService.IsGenerationAvailable()
            ? "프로젝트 Assets\\Thumbnail 이미지를 우선 사용합니다."
            : "프로젝트 Assets\\Thumbnail 경로를 찾지 못했습니다.";
    }

    private void SyncAllQueuedCutDurations()
    {
        foreach (var template in _formatCatalogService.GetAll())
        {
            SyncQueuedCutDurations(template);
        }
    }

    private void SyncQueuedCutDurations(FormatTemplateDefinition template)
    {
        var defaultDuration = template.Cuts.FirstOrDefault()?.DurationSeconds ?? 0;
        _isSyncingQueuedCutDurations = true;
        try
        {
            foreach (var channel in Channels)
            {
                foreach (var item in channel.Queue.Where(item => string.Equals(item.FormatId, template.Id, StringComparison.Ordinal)))
                {
                    item.DefaultCutDurationSeconds = defaultDuration;
                }
            }
        }
        finally
        {
            _isSyncingQueuedCutDurations = false;
        }
    }

    private static string BuildCutDurationKey(string formatId, string cutName)
    {
        return $"{formatId}::{cutName}";
    }

    private void RestoreChannelState(ChannelScheduleViewModel channelViewModel, AppState state, BroadcastChannel channel)
    {
        if (!state.Channels.TryGetValue(channel.ToString(), out var channelState))
        {
            return;
        }

        channelViewModel.Queue.Clear();
        foreach (var item in channelState.Items)
        {
            var template = _formatCatalogService.FindById(item.FormatId);
            channelViewModel.Queue.Add(new ChannelScheduleItem
            {
                Id = item.Id,
                FormatId = template?.Id ?? item.FormatId,
                FormatName = template?.Name ?? item.FormatName,
                Description = template?.Description ?? item.Description,
                Channel = item.Channel,
                RequiresImage = template?.RequiresImage ?? item.RequiresImage,
                DefaultCutDurationSeconds = template is null
                    ? item.DefaultCutDurationSeconds
                    : ScheduleTemplatePolicy.NormalizeCutDurationSeconds(
                        item.DefaultCutDurationSeconds > 0
                            ? item.DefaultCutDurationSeconds
                            : template.Cuts.FirstOrDefault()?.DurationSeconds ?? item.DefaultCutDurationSeconds,
                        template),
                TotalCuts = template?.Cuts.Count ?? item.TotalCuts,
                RegionScope = item.RegionScope,
                ScheduleElectionType = item.ScheduleElectionType,
                RegionLabel = item.RegionLabel,
                RegionCode = item.RegionCode,
                State = item.State
            });
        }

        channelViewModel.LoopEnabled = channelState.LoopEnabled;
        channelViewModel.EmptyScheduleBehavior = channelState.EmptyScheduleBehavior;
        channelViewModel.RefreshSummary();
    }
}


