using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Domain;
using Tornado3_2026Election.Services;

namespace Tornado3_2026Election.ViewModels;

public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposable
{
    public const string TurnoutPhotoSidoMode = "sido";
    public const string TurnoutPhotoDistrictMode = "district";

    private const string DistrictOverviewOptionValue = "__ALL_DISTRICTS__";
    private const string DistrictOverviewOptionLabel = "전체보기";
    private const string StationRegionOverviewOptionValue = "__SELECTED_REGIONS__";
    private const string StationRegionOverviewOptionLabel = "선택권역보기";
    private const string CouncilSeatCandidateCodePrefix = "SEAT:";
    private const string CouncilSeatDistrictCandidateCodePrefix = "SEAT:D:";
    private const string CouncilSeatProportionalCandidateCodePrefix = "SEAT:P:";
    private const string PanseSummaryCandidateCodePrefix = "PANSE:";
    private const string PanseDemocraticPartyLabel = "더불어민주당";
    private const string PansePeoplePowerPartyLabel = "국민의힘";
    private const string PanseOtherPartyLabel = "무·기타";
    public const int FixedPollingIntervalSeconds = 60;
    private static readonly Regex TopRankSlotCountPattern = new(@"1-(\d+)위", RegexOptions.Compiled);
    private static readonly Regex PeopleSlotCountPattern = new(@"(\d+)인", RegexOptions.Compiled);
    private static readonly Regex CareerPromiseCandidateCodeNumberPattern = new(@"\d+", RegexOptions.Compiled);

    private static readonly IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> DefaultDistrictOptions =
    [
        CreateDistrictOption("서울특별시", "11"),
        CreateDistrictOption("부산광역시", "26"),
        CreateDistrictOption("대구광역시", "27"),
        CreateDistrictOption("인천광역시", "28"),
        CreateDistrictOption("광주광역시", "29"),
        CreateDistrictOption("대전광역시", "30"),
        CreateDistrictOption("울산광역시", "31"),
        CreateDistrictOption("세종특별자치시", "51"),
        CreateDistrictOption("경기도", "41"),
        CreateDistrictOption("강원특별자치도", "42"),
        CreateDistrictOption("충청북도", "43"),
        CreateDistrictOption("충청남도", "44"),
        CreateDistrictOption("전북특별자치도", "45"),
        CreateDistrictOption("전라남도", "46"),
        CreateDistrictOption("경상북도", "47"),
        CreateDistrictOption("경상남도", "48"),
        CreateDistrictOption("제주특별자치도", "49")
    ];
    private static readonly IReadOnlyDictionary<string, string> RegionAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["서울"] = "서울",
        ["서울특별시"] = "서울",
        ["부산"] = "부산",
        ["부산광역시"] = "부산",
        ["대구"] = "대구",
        ["대구광역시"] = "대구",
        ["인천"] = "인천",
        ["인천광역시"] = "인천",
        ["광주"] = "광주",
        ["광주광역시"] = "광주",
        ["대전"] = "대전",
        ["대전광역시"] = "대전",
        ["울산"] = "울산",
        ["울산광역시"] = "울산",
        ["세종"] = "세종",
        ["세종특별자치시"] = "세종",
        ["경기"] = "경기",
        ["경기도"] = "경기",
        ["강원"] = "강원",
        ["강원도"] = "강원",
        ["강원특별자치도"] = "강원",
        ["충북"] = "충북",
        ["충청북도"] = "충북",
        ["충남"] = "충남",
        ["충청남도"] = "충남",
        ["전북"] = "전북",
        ["전라북도"] = "전북",
        ["전북특별자치도"] = "전북",
        ["전남"] = "전남",
        ["전라남도"] = "전남",
        ["경북"] = "경북",
        ["경상북도"] = "경북",
        ["경남"] = "경남",
        ["경상남도"] = "경남",
        ["제주"] = "제주",
        ["제주도"] = "제주",
        ["제주특별자치도"] = "제주"
    };
    private static readonly IReadOnlyList<SelectionOption<string>> PreElectionColorPartyOptions =
    [
        new SelectionOption<string>(string.Empty, "표기 그대로"),
        new SelectionOption<string>("국민의힘", "국민의힘"),
        new SelectionOption<string>("더불어민주당", "더불어민주당"),
        new SelectionOption<string>("개혁신당", "개혁신당"),
        new SelectionOption<string>("조국혁신당", "조국혁신당"),
        new SelectionOption<string>("진보당", "진보당"),
        new SelectionOption<string>("정의당", "정의당"),
        new SelectionOption<string>("녹색당", "녹색당"),
        new SelectionOption<string>("무기타", "무소속/기타")
    ];
    private static readonly IReadOnlyList<(int ElectionOrder, int Year)> HistoricalWinnerSlotsAll =
    [
        (1, 1995),
        (2, 1998),
        (3, 2002),
        (4, 2006),
        (5, 2010),
        (6, 2014),
        (7, 2018),
        (8, 2022)
    ];
    private static readonly IReadOnlyList<(int ElectionOrder, int Year)> HistoricalTurnoutSlotsLocal =
    [
        (3, 2002),
        (4, 2006),
        (5, 2010),
        (6, 2014),
        (7, 2018),
        (8, 2022)
    ];
    private static readonly IReadOnlyList<(int ElectionOrder, int Year)> HistoricalTurnoutSlotsEducation =
    [
        (5, 2010),
        (6, 2014),
        (7, 2018),
        (8, 2022)
    ];

    private readonly LogService _logService;
    private readonly SbsElectionApiClient _apiClient;
    private readonly PreElectionHistoryService _preElectionHistoryService;
    private readonly CareerPromiseService _careerPromiseService;
    private readonly Dictionary<string, string> _districtCodeMap = new(StringComparer.Ordinal);
    private readonly Dictionary<string, SbsElectionApiClient.DistrictSelectionOption> _districtOptionMap = new(StringComparer.Ordinal);
    private CancellationTokenSource? _pollingCts;
    private TaskCompletionSource<bool>? _refreshSignal;
    private BroadcastPhase _broadcastPhase = BroadcastPhase.Counting;
    private bool _isRefreshing;
    private bool _isPollingEnabled = true;
    private bool _isUpdatingDistrictOptions;
    private bool _isUpdatingPreElectionHistoryDistrictOptions;
    private bool _isApplyingRefreshResult;
    private DateTimeOffset _lastRefreshAt = DateTimeOffset.MinValue;
    private DateTimeOffset _lastManualRefreshAt = DateTimeOffset.MinValue;
    private string _electionType = "광역단체장";
    private string _districtName = "부산광역시";
    private string _districtCode = "26";
    private string _regionName = "부산광역시";
    private string _electionDistrictName = "부산광역시";
    private int _totalExpectedVotes = 1_240_000;
    private int _turnoutVotes = 528_400;
    private int _pollingIntervalSeconds = FixedPollingIntervalSeconds;
    private int _pollingCountdownSeconds;
    private int _districtOptionsRevision;
    private int _selectionRefreshRevision;
    private int _districtOverviewRefreshRevision;
    private string _lastRefreshWarningMessage = string.Empty;
    private IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> _districtSelectionSource = DefaultDistrictOptions;
    private HashSet<string> _configuredRegions = new(StringComparer.OrdinalIgnoreCase);
    private bool _showOnlyConfiguredRegions;
    private double _closeRaceThresholdPercent = 5.0;
    private double _superCloseRaceThresholdPercent = 3.0;
    private double _draftCloseRaceThresholdPercent = 5.0;
    private double _draftSuperCloseRaceThresholdPercent = 3.0;
    private bool _isRefreshingCloseRaceTargets;
    private string _closeRaceThresholdStatusText = "접전 기준을 조정한 뒤 대상 확인을 누르면 현재 조건에 맞는 선거구를 확인할 수 있습니다.";
    private string _closeRaceTargetSummaryText = "아직 대상 확인을 실행하지 않았습니다.";
    private string _selectedDistrictViewName = StationRegionOverviewOptionValue;
    private bool _isRefreshingDistrictOverview;
    private string _districtOverviewStatusText = "전체보기를 선택하면 지역별 개표율을 확인할 수 있습니다.";
    private string _selectedStationId = "KNN";
    private string _selectedStationName = "KNN";
    private double? _countedRateFromApi;
    private int? _countedVotesFromApi;
    private int? _remainingVotesFromApi;
    private string _referenceTimeLabel = string.Empty;
    private bool _isUpdatingCareerPromiseRows;
    private CancellationTokenSource? _careerPromiseAutoSaveCts;
    private string _careerPromiseEditorContextKey = string.Empty;
    private string _careerPromiseEditorStatusText = "현재 후보자 기준으로 공약을 편집할 수 있습니다.";
    private bool _careerPromiseHasPendingChanges;
    private string _preElectionHistoryElectionType = "광역단체장";
    private string _preElectionHistoryDistrictName = "부산광역시";
    private string _preElectionHistoryStatusText = "사전 데이터를 준비 중입니다.";
    private string _preElectionHistoryEditorStatusText = "사전 데이터를 편집할 수 있습니다.";
    private string _preElectionHistoryRegionLabel = string.Empty;
    private bool _preElectionHistoryHasPendingChanges;
    private IReadOnlyList<PreElectionHistoricalTurnoutEntry> _resolvedHistoricalTurnout = Array.Empty<PreElectionHistoricalTurnoutEntry>();
    private IReadOnlyList<PreElectionHistoricalWinnerEntry> _resolvedHistoricalWinners = Array.Empty<PreElectionHistoricalWinnerEntry>();

    public DataViewModel(LogService logService)
    {
        _logService = logService;
        _apiClient = new SbsElectionApiClient();
        _preElectionHistoryService = new PreElectionHistoryService(logService);
        _careerPromiseService = new CareerPromiseService(logService);

        ElectionTypeOptions =
        [
            new SelectionOption<string>("국회의원", "국회의원"),
            new SelectionOption<string>("광역단체장", "광역단체장"),
            new SelectionOption<string>("교육감", "교육감"),
            new SelectionOption<string>("광역의원", "광역의원"),
            new SelectionOption<string>("기초단체장", "기초단체장"),
            new SelectionOption<string>("기초의원", "기초의원"),
            new SelectionOption<string>("비례대표광역의원", "비례대표광역의원"),
            new SelectionOption<string>("비례대표기초의원", "비례대표기초의원")
        ];
        CareerPromiseElectionTypeOptions =
        [
            new SelectionOption<string>("광역단체장", "광역단체장"),
            new SelectionOption<string>("기초단체장", "기초단체장")
        ];

        DistrictOptions = [];
        DistrictViewOptions = [];
        DistrictOverviewCards = [];
        CloseRaceTargets = [];
        PreElectionTurnoutHistory = [];
        PreElectionWinnerHistory = [];
        CareerPromiseRows = [];
        PreElectionHistoryEditableTurnoutRows = [];
        PreElectionHistoryEditableWinnerRows = [];
        PreElectionHistoryDistrictOptions = [];
        PreElectionHistoryElectionTypeOptions = _preElectionHistoryService.GetSupportedElectionTypes()
            .Select(option => new SelectionOption<string>(option, option))
            .ToArray();

        EnsureOptionExists(ElectionTypeOptions, _electionType);
        _districtSelectionSource = DefaultDistrictOptions;
        ApplyDistrictSelectionOptions(DefaultDistrictOptions, _districtName, _districtCode, _districtName);
        _preElectionHistoryDistrictName = _districtName;
        RefreshPreElectionHistorySelectionOptions();

        Candidates =
        [
            new CandidateEntry { CandidateCode = "A01", BallotNumber = "1", Name = "김후보", Party = "더불어민주당", VoteCount = 312000, VoteRate = 34.8, HasImage = true },
            new CandidateEntry { CandidateCode = "A02", BallotNumber = "2", Name = "이후보", Party = "국민의힘", VoteCount = 287000, VoteRate = 32.0, HasImage = true },
            new CandidateEntry { CandidateCode = "A03", BallotNumber = "3", Name = "이서윤", Party = "개혁신당", VoteCount = 168000, VoteRate = 18.7, HasImage = false },
            new CandidateEntry { CandidateCode = "A04", BallotNumber = "4", Name = "정민석", Party = "무소속", VoteCount = 129000, VoteRate = 14.5, HasImage = true }
        ];

        ManualRefreshCommand = new AsyncRelayCommand(() => RefreshAsync(isManualRequest: true));
        ApplyCloseRaceThresholdsCommand = new RelayCommand(ApplyCloseRaceThresholds, () => HasPendingCloseRaceThresholdChange);
        ResetCloseRaceThresholdsCommand = new RelayCommand(ResetCloseRaceThresholds);
        RefreshCloseRaceTargetsCommand = new AsyncRelayCommand(RefreshCloseRaceTargetsAsync, () => !IsRefreshingCloseRaceTargets);
        AddCareerPromiseRowCommand = new RelayCommand(AddCareerPromiseRow);
        SaveCareerPromisesCommand = new AsyncRelayCommand(
            SaveCareerPromisesAsync,
            () => CareerPromiseHasPendingChanges &&
                  !string.IsNullOrWhiteSpace(ResolveCareerPromiseDistrictName()));
        SavePreElectionHistoryCommand = new AsyncRelayCommand(SavePreElectionHistoryAsync, () => PreElectionHistoryHasPendingChanges);

        RecalculateJudgements();
        RefreshCareerPromiseEditor();
        RefreshPreElectionHistoryPresentation();
        _ = RefreshDistrictOptionsForElectionTypeAsync();
        StartPolling();
    }

    public ObservableCollection<CandidateEntry> Candidates { get; }

    public ObservableCollection<SelectionOption<string>> ElectionTypeOptions { get; }

    public IReadOnlyList<SelectionOption<string>> CareerPromiseElectionTypeOptions { get; }

    public ObservableCollection<SelectionOption<string>> DistrictOptions { get; }

    public ObservableCollection<SelectionOption<string>> DistrictViewOptions { get; }

    public ObservableCollection<DistrictOverviewCardViewModel> DistrictOverviewCards { get; }

    public ObservableCollection<CloseRaceTargetViewModel> CloseRaceTargets { get; }

    public ObservableCollection<PreElectionTurnoutHistoryCardViewModel> PreElectionTurnoutHistory { get; }

    public ObservableCollection<PreElectionWinnerHistoryCardViewModel> PreElectionWinnerHistory { get; }

    public ObservableCollection<CareerPromiseEditRowViewModel> CareerPromiseRows { get; }

    public ObservableCollection<PreElectionHistoryTurnoutEditRowViewModel> PreElectionHistoryEditableTurnoutRows { get; }

    public ObservableCollection<PreElectionHistoryWinnerEditRowViewModel> PreElectionHistoryEditableWinnerRows { get; }

    public ObservableCollection<SelectionOption<string>> PreElectionHistoryDistrictOptions { get; }

    public IReadOnlyList<SelectionOption<string>> PreElectionHistoryElectionTypeOptions { get; }

    public AsyncRelayCommand ManualRefreshCommand { get; }

    public RelayCommand ApplyCloseRaceThresholdsCommand { get; }

    public RelayCommand ResetCloseRaceThresholdsCommand { get; }

    public AsyncRelayCommand RefreshCloseRaceTargetsCommand { get; }

    public RelayCommand AddCareerPromiseRowCommand { get; }

    public AsyncRelayCommand SaveCareerPromisesCommand { get; }

    public AsyncRelayCommand SavePreElectionHistoryCommand { get; }

    public void SelectDistrictOverviewCard(DistrictOverviewCardViewModel? card)
    {
        if (!IsDistrictOverviewMode || card is null)
        {
            return;
        }

        var districtViewName = ResolveDistrictViewSelectionName(card.DistrictViewName, card.DistrictCode);
        if (string.IsNullOrWhiteSpace(districtViewName))
        {
            return;
        }

        SelectedDistrictViewName = districtViewName;
    }

    public BroadcastPhase BroadcastPhase
    {
        get => _broadcastPhase;
        set
        {
            if (SetProperty(ref _broadcastPhase, value))
            {
                OnPropertyChanged(nameof(StatusText), nameof(HasLiveDataSignal));
                NotifyModePresentationChanged();
            }
        }
    }

    public bool IsPreElectionPhase => BroadcastPhase == BroadcastPhase.PreElection;

    public bool IsCountingPhase => BroadcastPhase == BroadcastPhase.Counting;

    public string BroadcastPhaseLabel => IsPreElectionPhase ? "투표" : "개표";

    public string BroadcastPhaseBadgeText => IsPreElectionPhase ? "투표율 수신" : "개표 득표수 수신";

    public string BroadcastPhaseDetailText => IsPreElectionPhase
        ? "투표 단계에서는 투표율과 투표자 수를 수신합니다."
        : "개표 단계에서는 후보 득표수와 판정 데이터를 수신합니다.";

    public string HeaderMetricSummary => IsPreElectionPhase
        ? $"투표율 {TurnoutRateDisplay} / 투표자 {TurnoutVotes:N0}"
        : $"개표율 {CountedRateDisplay} / 개표 {CountedVotes:N0} / 남은표 {RemainingVotes:N0}";

    public string SituationMetricPrimaryLabel => IsPreElectionPhase ? "투표율" : "개표수";

    public string SituationMetricPrimaryValue => IsPreElectionPhase ? TurnoutRateDisplay : CountedVotes.ToString("N0");

    public string SituationMetricSecondaryLabel => IsPreElectionPhase ? "투표자" : "남은표";

    public string SituationMetricSecondaryValue => IsPreElectionPhase ? TurnoutVotes.ToString("N0") : RemainingVotes.ToString("N0");

    public string TotalExpectedVotesLabel => IsPreElectionPhase ? "총 선거인수" : "총 예상표";

    public bool IsCurrentApiSelectionSupported => SupportsApiRefresh(BroadcastPhase, ElectionType);

    public bool HasConfiguredRegionFilter => _configuredRegions.Count > 0;

    public bool ShowOnlyConfiguredRegions
    {
        get => _showOnlyConfiguredRegions;
        set
        {
            if (SetProperty(ref _showOnlyConfiguredRegions, value))
            {
                OnPropertyChanged(nameof(ConfiguredRegionFilterHintText));
                ReapplyDistrictSelectionOptions();
            }
        }
    }

    public string ConfiguredRegionFilterHintText
    {
        get
        {
            if (_configuredRegions.Count == 0)
            {
                return "선택한 방송사에 권역 설정이 없어 전체 선거구를 표시합니다.";
            }

            var configuredRegions = string.Join(", ", _configuredRegions.OrderBy(region => region, StringComparer.Ordinal));
            return ShowOnlyConfiguredRegions
                ? $"설정 권역만 표시 중: {configuredRegions}"
                : $"설정 권역: {configuredRegions}";
        }
    }

    public double CloseRaceThresholdPercent
    {
        get => _closeRaceThresholdPercent;
        private set
        {
            var normalized = NormalizeRaceThreshold(value);
            if (SetProperty(ref _closeRaceThresholdPercent, normalized))
            {
                OnPropertyChanged(nameof(CloseRaceThresholdSummaryText));
            }
        }
    }

    public double SuperCloseRaceThresholdPercent
    {
        get => _superCloseRaceThresholdPercent;
        private set
        {
            var normalized = NormalizeRaceThreshold(value);
            if (SetProperty(ref _superCloseRaceThresholdPercent, normalized))
            {
                OnPropertyChanged(nameof(CloseRaceThresholdSummaryText));
            }
        }
    }

    public double DraftCloseRaceThresholdPercent
    {
        get => _draftCloseRaceThresholdPercent;
        set
        {
            var normalized = NormalizeRaceThreshold(value);
            if (SetProperty(ref _draftCloseRaceThresholdPercent, normalized))
            {
                NotifyCloseRaceDraftChanged();
            }
        }
    }

    public double DraftSuperCloseRaceThresholdPercent
    {
        get => _draftSuperCloseRaceThresholdPercent;
        set
        {
            var normalized = NormalizeRaceThreshold(value);
            if (SetProperty(ref _draftSuperCloseRaceThresholdPercent, normalized))
            {
                NotifyCloseRaceDraftChanged();
            }
        }
    }

    public bool HasPendingCloseRaceThresholdChange =>
        Math.Abs(DraftCloseRaceThresholdPercent - CloseRaceThresholdPercent) >= 0.05 ||
        Math.Abs(DraftSuperCloseRaceThresholdPercent - SuperCloseRaceThresholdPercent) >= 0.05;

    public bool IsRefreshingCloseRaceTargets
    {
        get => _isRefreshingCloseRaceTargets;
        private set
        {
            if (SetProperty(ref _isRefreshingCloseRaceTargets, value))
            {
                RefreshCloseRaceTargetsCommand.NotifyCanExecuteChanged();
            }
        }
    }

    public string CloseRaceThresholdStatusText
    {
        get => _closeRaceThresholdStatusText;
        private set => SetProperty(ref _closeRaceThresholdStatusText, value ?? string.Empty);
    }

    public string CloseRaceThresholdSummaryText =>
        $"접전 {SuperCloseRaceThresholdPercent:0.0}% 초과 ~ {CloseRaceThresholdPercent:0.0}% 이하 / 초접전 {SuperCloseRaceThresholdPercent:0.0}% 이하";

    public string CloseRaceTargetSummaryText
    {
        get => _closeRaceTargetSummaryText;
        private set => SetProperty(ref _closeRaceTargetSummaryText, value ?? string.Empty);
    }

    public Visibility CloseRaceTargetsVisibility => CloseRaceTargets.Count > 0
        ? Visibility.Visible
        : Visibility.Collapsed;

    public bool IsDistrictOverviewMode =>
        IsDistrictOverviewSelection(SelectedDistrictViewName);

    public string DistrictOverviewTitle => IsCountingPhase
        ? IsStationRegionOverviewMode
            ? "선택 권역 개표율"
            : "전체 개표율"
        : IsStationRegionOverviewMode
            ? "선택 권역 투표율"
            : "전체 투표율";

    public string DistrictOverviewStatusText
    {
        get => _districtOverviewStatusText;
        private set => SetProperty(ref _districtOverviewStatusText, value ?? string.Empty);
    }

    public Visibility TurnoutBoardVisibility => IsPreElectionPhase && IsCurrentApiSelectionSupported && !IsDistrictOverviewMode
        ? Visibility.Visible
        : Visibility.Collapsed;

    public Visibility PreElectionHistoryVisibility => Visibility.Visible;

    public Visibility PreElectionTurnoutHistoryVisibility => PreElectionTurnoutHistory.Count > 0
        ? Visibility.Visible
        : Visibility.Collapsed;

    public Visibility PreElectionWinnerHistoryVisibility => PreElectionWinnerHistory.Count > 0
        ? Visibility.Visible
        : Visibility.Collapsed;

    public string PreElectionHistoryTitle => string.IsNullOrWhiteSpace(_preElectionHistoryRegionLabel)
        ? "사전 데이터"
        : $"{_preElectionHistoryRegionLabel} 사전 데이터";

    public string PreElectionHistoryElectionType
    {
        get => _preElectionHistoryElectionType;
        set
        {
            var normalizedValue = string.IsNullOrWhiteSpace(value)
                ? _preElectionHistoryElectionType
                : value;
            if (SetProperty(ref _preElectionHistoryElectionType, normalizedValue))
            {
                RefreshPreElectionHistorySelectionOptions();
                RefreshPreElectionHistoryPresentation();
                OnPropertyChanged(nameof(PreElectionHistorySummaryText), nameof(PreElectionHistoryCoverageText));
            }
        }
    }

    public string PreElectionHistoryDistrictName
    {
        get => _preElectionHistoryDistrictName;
        set
        {
            if (string.IsNullOrWhiteSpace(value) && _isUpdatingPreElectionHistoryDistrictOptions)
            {
                return;
            }

            var normalizedValue = string.IsNullOrWhiteSpace(value)
                ? _preElectionHistoryDistrictName
                : value;
            if (!_isUpdatingPreElectionHistoryDistrictOptions)
            {
                EnsureOptionExists(PreElectionHistoryDistrictOptions, normalizedValue);
            }

            if (SetProperty(ref _preElectionHistoryDistrictName, normalizedValue))
            {
                RefreshPreElectionHistoryPresentation();
            }
        }
    }

    public string PreElectionHistorySummaryText => _preElectionHistoryService.GetCoverageSummary(PreElectionHistoryElectionType);

    public string PreElectionHistoryCoverageText => _preElectionHistoryService.GetCoverageDetail(PreElectionHistoryElectionType);

    public string PreElectionHistoryStatusText
    {
        get => _preElectionHistoryStatusText;
        private set => SetProperty(ref _preElectionHistoryStatusText, value ?? string.Empty);
    }

    public string PreElectionHistoryEditorStatusText
    {
        get => _preElectionHistoryEditorStatusText;
        private set => SetProperty(ref _preElectionHistoryEditorStatusText, value ?? string.Empty);
    }

    public bool PreElectionHistoryHasPendingChanges
    {
        get => _preElectionHistoryHasPendingChanges;
        private set
        {
            if (SetProperty(ref _preElectionHistoryHasPendingChanges, value))
            {
                OnPropertyChanged(nameof(PreElectionHistorySaveStateText));
                SavePreElectionHistoryCommand.NotifyCanExecuteChanged();
            }
        }
    }

    public string PreElectionHistorySaveStateText => PreElectionHistoryHasPendingChanges
        ? "수정된 값이 있습니다. 저장 후 송출에 반영됩니다."
        : "현재 저장본과 동일합니다.";

    public Visibility DistrictOverviewVisibility => IsDistrictOverviewMode && IsCurrentApiSelectionSupported
        ? Visibility.Visible
        : Visibility.Collapsed;

    public Visibility CandidateBoardVisibility => IsCountingPhase && IsCurrentApiSelectionSupported && !IsDistrictOverviewMode
        ? Visibility.Visible
        : Visibility.Collapsed;

    public Visibility CareerPromiseVisibility => CandidateBoardVisibility == Visibility.Visible && Candidates.Count > 0
        ? Visibility.Visible
        : Visibility.Collapsed;

    public string CareerPromiseContextText =>
        $"{ElectionType} / {ResolveCareerPromiseDistrictName()}";

    public string CareerPromiseStatusText =>
        "현재 후보자의 선거구명, 이름, 정당이 저장본과 일치할 때 경력 컷 공약으로 송출합니다.";

    public string CareerPromiseEditorStatusText
    {
        get => _careerPromiseEditorStatusText;
        private set => SetProperty(ref _careerPromiseEditorStatusText, value ?? string.Empty);
    }

    public bool CareerPromiseHasPendingChanges
    {
        get => _careerPromiseHasPendingChanges;
        private set
        {
            if (SetProperty(ref _careerPromiseHasPendingChanges, value))
            {
                OnPropertyChanged(nameof(CareerPromiseSaveStateText));
                SaveCareerPromisesCommand.NotifyCanExecuteChanged();
            }
        }
    }

    public string CareerPromiseSaveStateText => CareerPromiseHasPendingChanges
        ? "수정된 공약이 있습니다. 곧 자동 저장됩니다."
        : "자동 저장 완료";

    public string CareerPromiseFilePath => _careerPromiseService.FilePath;

    public bool IsRefreshing
    {
        get => _isRefreshing;
        private set
        {
            if (SetProperty(ref _isRefreshing, value))
            {
                OnPropertyChanged(nameof(StatusText), nameof(PollingCountdownText), nameof(PollingModeLabel), nameof(PollingStateDetail));
                OnPropertyChanged(nameof(HasLiveDataSignal));
            }
        }
    }

    public bool IsPollingEnabled
    {
        get => _isPollingEnabled;
        set
        {
            if (SetProperty(ref _isPollingEnabled, value))
            {
                OnPropertyChanged(nameof(StatusText), nameof(PollingCountdownText), nameof(PollingModeLabel), nameof(PollingStateDetail));
                if (value)
                {
                    StartPolling();
                }
                else
                {
                    StopPolling();
                }
            }
        }
    }

    public DateTimeOffset LastRefreshAt
    {
        get => _lastRefreshAt;
        private set
        {
            if (SetProperty(ref _lastRefreshAt, value))
            {
                OnPropertyChanged(nameof(StatusText), nameof(LastRefreshDisplay));
                OnPropertyChanged(nameof(HasLiveDataSignal));
            }
        }
    }

    public string ElectionType
    {
        get => _electionType;
        set
        {
            var normalizedValue = string.IsNullOrWhiteSpace(value) ? _electionType : value;
            EnsureOptionExists(ElectionTypeOptions, normalizedValue);
            if (SetProperty(ref _electionType, normalizedValue))
            {
                OnPropertyChanged(nameof(StatusText), nameof(CareerPromiseContextText), nameof(HasLiveDataSignal));
                NotifyModePresentationChanged();
                RefreshPreElectionHistoryPresentation();
                _ = RefreshDistrictOptionsForElectionTypeAsync();
            }
        }
    }

    public string DistrictName
    {
        get => _districtName;
        set
        {
            if (value is null && _isUpdatingDistrictOptions)
            {
                return;
            }

            var normalizedValue = value ?? string.Empty;
            EnsureOptionExists(DistrictOptions, normalizedValue);
            var changed = SetProperty(ref _districtName, normalizedValue);
            ApplyResolvedLocation(ResolveLocationFields(normalizedValue));
            if (changed &&
                !string.IsNullOrWhiteSpace(normalizedValue) &&
                _districtCodeMap.TryGetValue(normalizedValue, out var districtCode))
            {
                DistrictCode = districtCode;
            }

            if (changed)
            {
                SyncSelectedDistrictViewName(normalizedValue);
                OnPropertyChanged(nameof(CareerPromiseContextText));
                RefreshPreElectionHistoryPresentation();
                QueueSelectionRefresh("선거구 변경");
            }
        }
    }

    public string SelectedDistrictViewName
    {
        get => _selectedDistrictViewName;
        set
        {
            if (value is null)
            {
                return;
            }

            var normalizedValue = string.IsNullOrWhiteSpace(value)
                ? StationRegionOverviewOptionValue
                : value;
            if (!SetProperty(ref _selectedDistrictViewName, normalizedValue))
            {
                return;
            }

            UpdateDistrictOverviewStatus();
            OnPropertyChanged(nameof(IsDistrictOverviewMode));
            OnPropertyChanged(nameof(DistrictOverviewTitle));
            OnPropertyChanged(nameof(TurnoutBoardVisibility));
            OnPropertyChanged(nameof(PreElectionHistoryVisibility));
            OnPropertyChanged(nameof(DistrictOverviewVisibility));
            OnPropertyChanged(nameof(CandidateBoardVisibility));
            OnPropertyChanged(nameof(CareerPromiseVisibility));

            if (IsDistrictOverviewMode)
            {
                _ = RefreshDistrictOverviewAsync(IsStationRegionOverviewMode ? "선택권역보기 선택" : "전체보기 선택");
                return;
            }

            if (!string.Equals(DistrictName, normalizedValue, StringComparison.Ordinal))
            {
                DistrictName = normalizedValue;
            }
        }
    }

    public string DistrictCode
    {
        get => _districtCode;
        set
        {
            var normalizedValue = value ?? string.Empty;
            if (SetProperty(ref _districtCode, normalizedValue))
            {
                OnPropertyChanged(nameof(CareerPromiseContextText));
                SaveCareerPromisesCommand.NotifyCanExecuteChanged();
                QueueSelectionRefresh("지역 코드 변경");
            }
        }
    }

    public string RegionName
    {
        get => _regionName;
        private set => SetProperty(ref _regionName, value ?? string.Empty);
    }

    public string ElectionDistrictName
    {
        get => _electionDistrictName;
        private set => SetProperty(ref _electionDistrictName, value ?? string.Empty);
    }

    public int TotalExpectedVotes
    {
        get => _totalExpectedVotes;
        set
        {
            var normalized = Math.Max(1, value);
            if (SetProperty(ref _totalExpectedVotes, normalized))
            {
                if (_turnoutVotes > normalized)
                {
                    _turnoutVotes = normalized;
                    OnPropertyChanged(nameof(TurnoutVotes));
                }

                RecalculateJudgements();
                NotifyMetricPresentationChanged();
            }
        }
    }

    public int TurnoutVotes
    {
        get => _turnoutVotes;
        set
        {
            var normalized = Math.Clamp(value, 0, TotalExpectedVotes);
            if (SetProperty(ref _turnoutVotes, normalized))
            {
                OnPropertyChanged(
                    nameof(TurnoutRemainingVotes),
                    nameof(TurnoutRate),
                    nameof(TurnoutRateDisplay),
                    nameof(CountedRateBaseVotes),
                    nameof(CountedRateBaseLabel),
                    nameof(CountedRate),
                    nameof(CountedRateDisplay));
                OnPropertyChanged(nameof(SituationMetricPrimaryValue), nameof(SituationMetricSecondaryValue), nameof(HeaderMetricSummary));
            }
        }
    }

    public int PollingIntervalSeconds
    {
        get => _pollingIntervalSeconds;
        set
        {
            if (SetProperty(ref _pollingIntervalSeconds, FixedPollingIntervalSeconds))
            {
                OnPropertyChanged(nameof(PollingCountdownText));
                StartPolling();
            }
        }
    }

    public int PollingCountdownSeconds
    {
        get => _pollingCountdownSeconds;
        private set
        {
            var normalized = Math.Max(0, value);
            if (SetProperty(ref _pollingCountdownSeconds, normalized))
            {
                OnPropertyChanged(nameof(PollingCountdownText));
            }
        }
    }

    public int CountedVotes => _countedVotesFromApi ?? Candidates.Sum(candidate => candidate.VoteCount);

    public int RemainingVotes => _remainingVotesFromApi ?? Math.Max(0, TotalExpectedVotes - CountedVotes);

    public int TurnoutRemainingVotes => Math.Max(0, TotalExpectedVotes - TurnoutVotes);

    public int CountedRateBaseVotes => TurnoutVotes > 0
        ? TurnoutVotes
        : TotalExpectedVotes;

    public string CountedRateBaseLabel => TurnoutVotes > 0
        ? "총 투표수"
        : TotalExpectedVotesLabel;

    public double CountedRate
    {
        get
        {
            if (_countedRateFromApi.HasValue)
            {
                return _countedRateFromApi.Value;
            }

            return CountedRateBaseVotes <= 0
                ? 0
                : Math.Round(CountedVotes * 100d / CountedRateBaseVotes, 1, MidpointRounding.AwayFromZero);
        }
    }

    public double TurnoutRate => TotalExpectedVotes <= 0
        ? 0
        : Math.Round(TurnoutVotes * 100d / TotalExpectedVotes, 1, MidpointRounding.AwayFromZero);

    public string CountedRateDisplay => $"{CountedRate:0.0}%";

    public string TurnoutRateDisplay => $"{TurnoutRate:0.0}%";

    public string LastRefreshDisplay => LastRefreshAt == DateTimeOffset.MinValue
        ? "미수신"
        : LastRefreshAt.ToString("HH:mm:ss");

    public bool HasLiveDataSignal =>
        IsRefreshing ||
        (IsCurrentApiSelectionSupported &&
         LastRefreshAt != DateTimeOffset.MinValue &&
         string.IsNullOrWhiteSpace(_lastRefreshWarningMessage));

    public string PollingModeLabel
    {
        get
        {
            if (IsRefreshing)
            {
                return "수신 중";
            }

            return IsPollingEnabled ? "자동 수신" : "수동 수신";
        }
    }

    public string PollingStateDetail => IsRefreshing
        ? "갱신 완료 후 송출"
        : PollingCountdownText;

    public string PollingCountdownText
    {
        get
        {
            if (!IsPollingEnabled)
            {
                return "API 자동 수신 OFF";
            }

            if (IsRefreshing)
            {
                return "API 수신 진행 중";
            }

            return $"다음 API 수신까지 {PollingCountdownSeconds}초";
        }
    }

    public string StatusText
    {
        get
        {
            if (IsRefreshing)
            {
                return $"{BroadcastPhaseLabel} 데이터 갱신 중. 송출 요청은 갱신 완료 후 진행됩니다.";
            }

            if (!IsCurrentApiSelectionSupported)
            {
                return $"{ElectionType} {BroadcastPhaseLabel} 단계는 SBS API 미지원입니다.";
            }

            if (!string.IsNullOrWhiteSpace(_lastRefreshWarningMessage))
            {
                return _lastRefreshWarningMessage;
            }

            if (LastRefreshAt == DateTimeOffset.MinValue)
            {
                return $"{BroadcastPhaseLabel} 단계 초기 데이터 유지 중";
            }

            return $"마지막 갱신 {LastRefreshAt:HH:mm:ss} / 단계 {BroadcastPhaseLabel} / 폴링 {(IsPollingEnabled ? "ON" : "OFF")}";
        }
    }

    public void StartPolling()
    {
        _pollingCts?.Cancel();
        if (!IsPollingEnabled)
        {
            PollingCountdownSeconds = 0;
            return;
        }

        _pollingCts = new CancellationTokenSource();
        _ = RunPollingLoopAsync(_pollingCts.Token);
    }

    public void ApplyBroadcastPhase(BroadcastPhase phase)
    {
        if (BroadcastPhase == phase)
        {
            return;
        }

        BroadcastPhase = phase;
        _logService.Info($"방송 단계를 {BroadcastPhaseLabel} 모드로 전환했습니다.");
        QueueSelectionRefresh("방송 단계 변경");
    }

    public async Task WaitForRefreshAsync(CancellationToken cancellationToken)
    {
        while (IsRefreshing)
        {
            var waiter = _refreshSignal;
            if (waiter is null)
            {
                break;
            }

            await waiter.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
        }
    }

    public void SetConfiguredRegions(IEnumerable<string> regionFilters)
    {
        var normalizedRegions = NormalizeConfiguredRegions(regionFilters);
        if (_configuredRegions.SetEquals(normalizedRegions))
        {
            return;
        }

        _configuredRegions = normalizedRegions;
        OnPropertyChanged(nameof(HasConfiguredRegionFilter), nameof(ConfiguredRegionFilterHintText));
        if (IsBasicCouncilElectionType(ElectionType))
        {
            _ = RefreshDistrictOptionsForElectionTypeAsync();
        }
        else
        {
            ReapplyDistrictSelectionOptions();
        }
    }

    public void SetCloseRaceThresholds(double closeRaceThresholdPercent, double superCloseRaceThresholdPercent)
    {
        var (closeThreshold, superThreshold) = NormalizeRaceThresholdPair(
            closeRaceThresholdPercent,
            superCloseRaceThresholdPercent);
        CloseRaceThresholdPercent = closeThreshold;
        SuperCloseRaceThresholdPercent = superThreshold;
        _draftCloseRaceThresholdPercent = closeThreshold;
        _draftSuperCloseRaceThresholdPercent = superThreshold;
        OnPropertyChanged(
            nameof(DraftCloseRaceThresholdPercent),
            nameof(DraftSuperCloseRaceThresholdPercent),
            nameof(HasPendingCloseRaceThresholdChange),
            nameof(CloseRaceThresholdSummaryText));
        ApplyCloseRaceThresholdsCommand.NotifyCanExecuteChanged();
        CloseRaceThresholdStatusText = $"접전 기준을 불러왔습니다. {CloseRaceThresholdSummaryText}";
    }

    public void EnsureCareerPromiseElectionType()
    {
        if (IsCareerPromiseElectionType(ElectionType))
        {
            return;
        }

        ElectionType = "광역단체장";
    }

    public void SetSelectedStationContext(string stationId, string stationName)
    {
        var normalizedStationId = string.IsNullOrWhiteSpace(stationId) ? "KNN" : stationId.Trim();
        var normalizedStationName = string.IsNullOrWhiteSpace(stationName) ? normalizedStationId : stationName.Trim();
        if (string.Equals(_selectedStationId, normalizedStationId, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(_selectedStationName, normalizedStationName, StringComparison.Ordinal))
        {
            return;
        }

        _selectedStationId = normalizedStationId;
        _selectedStationName = normalizedStationName;
        ApplyCareerPromisesToCurrentCandidates();
        RefreshCareerPromiseEditor();
        SaveCareerPromisesCommand.NotifyCanExecuteChanged();
        OnPropertyChanged(nameof(CareerPromiseContextText));
    }

    public ElectionDataSnapshot GetCurrentSnapshot()
    {
        var history = ResolvePreElectionHistoryRecords(ElectionType, RegionName, DistrictName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase,
            ElectionType = ElectionType,
            DistrictName = DistrictName,
            DistrictCode = DistrictCode,
            RegionName = RegionName,
            ElectionDistrictName = ElectionDistrictName,
            Candidates = BuildSnapshotCandidatesWithCareerPromises(Candidates, ElectionType, DistrictCode, DistrictName),
            TotalExpectedVotes = TotalExpectedVotes,
            TurnoutVotes = TurnoutVotes,
            CountedVotesFromApi = _countedVotesFromApi,
            RemainingVotesFromApi = _remainingVotesFromApi,
            CountedRateFromApi = _countedRateFromApi,
            ReceivedAt = LastRefreshAt == DateTimeOffset.MinValue ? DateTimeOffset.Now : LastRefreshAt,
            ReferenceTimeLabel = _referenceTimeLabel,
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    public async Task<IReadOnlyList<ScheduleRegionOption>> GetScheduleRegionOptionsAsync(
        FormatTemplateDefinition? template,
        string? turnoutPhotoMode = null,
        CancellationToken cancellationToken = default)
    {
        var electionType = ResolveScheduleElectionType(template);
        if (ScheduleTemplatePolicy.UsesSingleRegionOption(template))
        {
            return
            [
                new ScheduleRegionOption
                {
                    Scope = ScheduleRegionScope.Single,
                    Label = ScheduleTemplatePolicy.SingleRegionLabel,
                    ElectionType = electionType
                }
            ];
        }

        if (IsNormalPanseMapTemplate(template))
        {
            return
            [
                new ScheduleRegionOption
                {
                    Scope = ScheduleRegionScope.All,
                    Label = "전국",
                    ElectionType = electionType
                }
            ];
        }

        if (ShouldUseTurnoutPhotoRegionLevelOptions(template))
        {
            return await GetTurnoutPhotoRegionLevelOptionsAsync(turnoutPhotoMode, cancellationToken).ConfigureAwait(false);
        }

        var options = await GetScheduleDistrictOptionsAsync(electionType, template, cancellationToken).ConfigureAwait(false);

        var regionOptions = new List<ScheduleRegionOption>
        {
            new()
            {
                Scope = ScheduleRegionScope.All,
                Label = "전체",
                ElectionType = electionType
            },
            new()
            {
                Scope = ScheduleRegionScope.StationRegions,
                Label = "선택권역",
                ElectionType = electionType
            }
        };

        if (IsByElectionTemplate(template) ||
            IsNormalPreElectionTurnoutDistrictBoardTemplate(template) ||
            ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template?.Name))
        {
            regionOptions.AddRange(CreateScheduleRegionGroupOptions(options, electionType));
        }
        else
        {
            regionOptions.AddRange(options.Select(option => new ScheduleRegionOption
            {
                Scope = ScheduleRegionScope.Single,
                Label = option.DisplayName,
                ElectionType = electionType,
                RegionName = option.RegionName,
                DistrictName = option.DistrictName,
                DistrictCode = option.DistrictCode
            }));
        }

        return regionOptions;
    }

    private async Task<IReadOnlyList<ScheduleRegionOption>> GetTurnoutPhotoRegionLevelOptionsAsync(
        string? turnoutPhotoMode,
        CancellationToken cancellationToken)
    {
        return string.Equals(turnoutPhotoMode, TurnoutPhotoDistrictMode, StringComparison.Ordinal)
            ? await GetTurnoutPhotoDistrictRegionOptionsAsync(cancellationToken).ConfigureAwait(false)
            : await GetTurnoutPhotoSidoRegionOptionsAsync(cancellationToken).ConfigureAwait(false);
    }

    private async Task<IReadOnlyList<ScheduleRegionOption>> GetTurnoutPhotoSidoRegionOptionsAsync(
        CancellationToken cancellationToken)
    {
        const string electionType = "광역단체장";
        var options = await GetScheduleDistrictOptionsAsync(electionType, cancellationToken).ConfigureAwait(false);
        var regionOptions = new List<ScheduleRegionOption>
        {
            new()
            {
                Scope = ScheduleRegionScope.All,
                Label = "전체",
                ElectionType = electionType
            },
            new()
            {
                Scope = ScheduleRegionScope.StationRegions,
                Label = "선택권역",
                ElectionType = electionType
            }
        };

        regionOptions.AddRange(options.Select(option => new ScheduleRegionOption
        {
            Scope = ScheduleRegionScope.Single,
            Label = option.DisplayName,
            ElectionType = electionType,
            RegionName = option.RegionName,
            DistrictName = option.DistrictName,
            DistrictCode = option.DistrictCode
        }));

        return regionOptions;
    }

    private async Task<IReadOnlyList<ScheduleRegionOption>> GetTurnoutPhotoDistrictRegionOptionsAsync(
        CancellationToken cancellationToken)
    {
        const string electionType = "기초단체장";
        var options = await GetScheduleDistrictOptionsAsync(electionType, cancellationToken).ConfigureAwait(false);
        var regionOptions = new List<ScheduleRegionOption>
        {
            new()
            {
                Scope = ScheduleRegionScope.StationRegions,
                Label = "선택권역",
                ElectionType = electionType
            }
        };

        regionOptions.AddRange(CreateScheduleRegionGroupOptions(options, electionType));

        return regionOptions;
    }

    private static IEnumerable<ScheduleRegionOption> CreateScheduleRegionGroupOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string electionType)
    {
        return options
            .Where(option => !string.IsNullOrWhiteSpace(option.RegionName))
            .GroupBy(option => ResolveScheduleRegionGroupKey(option), StringComparer.OrdinalIgnoreCase)
            .Select(group =>
            {
                var first = group.First();
                var label = NormalizeConfiguredRegion(first.RegionName);
                return new ScheduleRegionOption
                {
                    Scope = ScheduleRegionScope.RegionGroup,
                    Label = string.IsNullOrWhiteSpace(label) ? first.RegionName : label,
                    ElectionType = electionType,
                    RegionName = first.RegionName,
                    DistrictName = string.Empty,
                    DistrictCode = ResolveScheduleRegionGroupCode(first)
                };
            })
            .OrderBy(option => ResolveDefaultRegionOrder(option.RegionName))
            .ThenBy(option => option.Label, StringComparer.Ordinal);
    }

    private static bool IsByElectionTemplate(FormatTemplateDefinition? template)
    {
        var source = $"{template?.Name} {template?.Id}";
        return source.Contains("보궐선거", StringComparison.Ordinal);
    }

    private static string ResolveScheduleRegionGroupKey(SbsElectionApiClient.DistrictSelectionOption option)
    {
        var regionCode = ResolveScheduleRegionGroupCode(option);
        return string.IsNullOrWhiteSpace(regionCode)
            ? NormalizeConfiguredRegion(option.RegionName)
            : regionCode;
    }

    private static string ResolveScheduleRegionGroupCode(SbsElectionApiClient.DistrictSelectionOption option)
    {
        if (!string.IsNullOrWhiteSpace(option.ParentRegionCode))
        {
            return option.ParentRegionCode;
        }

        return SbsElectionApiClient.ResolveBasicApiSidoCode(option.RegionName);
    }

    private static int ResolveDefaultRegionOrder(string regionName)
    {
        var normalizedRegion = NormalizeConfiguredRegion(regionName);
        for (var index = 0; index < DefaultDistrictOptions.Count; index++)
        {
            if (string.Equals(
                    NormalizeConfiguredRegion(DefaultDistrictOptions[index].RegionName),
                    normalizedRegion,
                    StringComparison.OrdinalIgnoreCase))
            {
                return index;
            }
        }

        return int.MaxValue;
    }

    public bool ValidateForFormat(FormatTemplateDefinition template, out string errorMessage)
    {
        return ValidateSnapshotForFormat(template, GetCurrentSnapshot(), out errorMessage);
    }

    public bool ValidateSnapshotForFormat(FormatTemplateDefinition template, ElectionDataSnapshot snapshot, out string errorMessage)
    {
        if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name))
        {
            if (snapshot.HistoricalTurnoutHistory.Count == 0)
            {
                errorMessage = "역대 투표율 데이터가 없습니다.";
                return false;
            }

            errorMessage = string.Empty;
            return true;
        }

        if (ScheduleTemplatePolicy.IsHistoricalWinnerFormat(template.Name))
        {
            if (snapshot.HistoricalWinnerHistory.Count == 0)
            {
                errorMessage = "역대 당선자 데이터가 없습니다.";
                return false;
            }

            errorMessage = string.Empty;
            return true;
        }

        if (IsTurnoutTemplate(template) &&
            (snapshot.TurnoutVotes <= 0 || snapshot.TurnoutRate <= 0))
        {
            errorMessage = "투표율 데이터가 0이라 송출할 수 없습니다.";
            return false;
        }

        if (!template.RequiresCandidateData)
        {
            errorMessage = string.Empty;
            return true;
        }

        var validCandidates = snapshot.Candidates
            .Where(IsCandidateReadyForBroadcast)
            .ToArray();
        var isWinnerTemplate = IsWinnerTemplate(template);

        if (validCandidates.Length == 0)
        {
            errorMessage = isWinnerTemplate
                ? "당선 판정을 받은 후보가 없어 송출할 수 없습니다."
                : "후보 데이터가 없습니다.";
            return false;
        }

        if (isWinnerTemplate &&
            !validCandidates.Any(candidate => IsElectedJudgement(candidate.EffectiveJudgement)))
        {
            errorMessage = "당선 판정을 받은 후보가 없어 송출할 수 없습니다.";
            return false;
        }

        if (!isWinnerTemplate && validCandidates.Length != snapshot.Candidates.Count)
        {
            errorMessage = "필수 후보 필드가 비어 있어 송출할 수 없습니다.";
            return false;
        }

        if (!isWinnerTemplate && !IsCareerTemplate(template) && snapshot.BroadcastPhase == BroadcastPhase.Counting)
        {
            if (snapshot.CountedVotes <= 0 && snapshot.CountedRate <= 0)
            {
                errorMessage = "개표수와 개표율이 모두 0이라 송출할 수 없습니다.";
                return false;
            }

            if (!validCandidates.Any(candidate => candidate.VoteCount > 0 || candidate.VoteRate > 0))
            {
                errorMessage = "후보 득표 데이터가 없어 송출할 수 없습니다.";
                return false;
            }
        }

        if (IsCloseRaceTemplate(template))
        {
            if (!IsCloseRaceElectionType(snapshot.ElectionType))
            {
                errorMessage = "접전 컷은 광역단체장 또는 기초단체장 선거에서만 송출할 수 있습니다.";
                return false;
            }

            if (!TryResolveVoteRateGap(validCandidates, out _, out _, out var gap))
            {
                errorMessage = "접전 판단에 필요한 1위와 2위 후보 득표율 데이터가 없습니다.";
                return false;
            }

            var isSuperCloseRace = IsSuperCloseRaceTemplate(template);
            var matchesThreshold = isSuperCloseRace
                ? gap <= SuperCloseRaceThresholdPercent
                : gap > SuperCloseRaceThresholdPercent && gap <= CloseRaceThresholdPercent;
            if (!matchesThreshold)
            {
                errorMessage = isSuperCloseRace
                    ? $"득표율차 {gap:0.0}%p로 초접전 기준({SuperCloseRaceThresholdPercent:0.0}%p 이하)에 맞지 않습니다."
                    : $"득표율차 {gap:0.0}%p로 접전 기준({SuperCloseRaceThresholdPercent:0.0}%p 초과 ~ {CloseRaceThresholdPercent:0.0}%p 이하)에 맞지 않습니다.";
                return false;
            }
        }

        if (template.RequiresImage && snapshot.Candidates.Any(candidate => !candidate.HasImage))
        {
            errorMessage = "이미지 필수 포맷인데 후보 이미지가 없는 항목이 있습니다.";
            return false;
        }

        errorMessage = string.Empty;
        return true;
    }

    private static bool IsCandidateReadyForBroadcast(CandidateEntry candidate)
    {
        return !string.IsNullOrWhiteSpace(candidate.Name)
               && !string.IsNullOrWhiteSpace(candidate.Party)
               && !string.IsNullOrWhiteSpace(candidate.CandidateCode);
    }

    private static bool IsWinnerTemplate(FormatTemplateDefinition template)
    {
        return IsWinnerTemplate(template.Name) ||
               IsWinnerTemplate(template.Id) ||
               template.Cuts.Any(cut => IsWinnerTemplate(cut.Name));
    }

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

    private static bool IsWinnerTemplate(string? source)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            return false;
        }

        var sourceName = System.IO.Path.GetFileNameWithoutExtension(
            source
                .Replace('/', System.IO.Path.DirectorySeparatorChar)
                .Replace('\\', System.IO.Path.DirectorySeparatorChar));

        return sourceName.StartsWith("당선_", StringComparison.Ordinal);
    }

    private static bool IsCloseRaceTemplate(FormatTemplateDefinition template)
    {
        return IsCloseRaceTemplate(template.Name) ||
               IsCloseRaceTemplate(template.Id) ||
               template.Cuts.Any(cut => IsCloseRaceTemplate(cut.Name));
    }

    private static bool IsCloseRaceTemplate(string? source)
    {
        var sourceName = GetTemplateBaseName(source);
        return sourceName.StartsWith("접전_", StringComparison.Ordinal) ||
               sourceName.StartsWith("초접전_", StringComparison.Ordinal);
    }

    private static bool IsSuperCloseRaceTemplate(FormatTemplateDefinition template)
    {
        return IsSuperCloseRaceTemplate(template.Name) ||
               IsSuperCloseRaceTemplate(template.Id) ||
               template.Cuts.Any(cut => IsSuperCloseRaceTemplate(cut.Name));
    }

    private static bool IsSuperCloseRaceTemplate(string? source)
    {
        return GetTemplateBaseName(source).StartsWith("초접전_", StringComparison.Ordinal);
    }

    private static string GetTemplateBaseName(string? source)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            return string.Empty;
        }

        return System.IO.Path.GetFileNameWithoutExtension(
            source
                .Replace('/', System.IO.Path.DirectorySeparatorChar)
                .Replace('\\', System.IO.Path.DirectorySeparatorChar));
    }

    private static bool IsElectedJudgement(CandidateJudgement judgement)
    {
        return judgement is CandidateJudgement.Elected
            or CandidateJudgement.ElectedInProgress
            or CandidateJudgement.UnopposedElected
            or CandidateJudgement.ElectedAfterCountComplete;
    }

    private static bool IsTurnoutTemplate(FormatTemplateDefinition template)
    {
        return template.Name.Contains("투표율", StringComparison.Ordinal);
    }

    private BroadcastPhase ResolveScheduleRefreshPhase(FormatTemplateDefinition template)
    {
        if (IsTurnoutTemplate(template))
        {
            return BroadcastPhase.PreElection;
        }

        return template.RequiresCandidateData || IsCareerTemplate(template)
            ? BroadcastPhase.Counting
            : BroadcastPhase;
    }

    private static bool IsCareerTemplate(FormatTemplateDefinition template)
    {
        return IsCareerTemplate(template.Name) ||
               IsCareerTemplate(template.Id) ||
               template.Cuts.Any(cut => IsCareerTemplate(cut.Name));
    }

    private static bool IsCareerTemplate(string? source)
    {
        return GetTemplateBaseName(source).StartsWith("경력_", StringComparison.Ordinal);
    }

    private bool ShouldUseTurnoutPhotoRegionLevelOptions(FormatTemplateDefinition? template)
    {
        return template is not null &&
               (string.Equals(template.Name, "투표율", StringComparison.Ordinal) ||
                string.Equals(template.Name, "투표율_사진", StringComparison.Ordinal));
    }

    private static int ResolveRequiredCandidateCount(FormatTemplateDefinition template)
    {
        foreach (var source in new[] { template.Cuts.FirstOrDefault()?.Name, template.Name, template.Id })
        {
            var count = ResolveRequiredCandidateCount(source);
            if (count > 0)
            {
                return count;
            }
        }

        return 0;
    }

    private static int ResolveRequiredCandidateCount(string? source)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            return 0;
        }

        var sourceName = System.IO.Path.GetFileNameWithoutExtension(
            source
                .Replace('/', System.IO.Path.DirectorySeparatorChar)
                .Replace('\\', System.IO.Path.DirectorySeparatorChar));

        var topRankMatch = TopRankSlotCountPattern.Match(sourceName);
        if (topRankMatch.Success &&
            int.TryParse(topRankMatch.Groups[1].Value, out var topRankSlotCount) &&
            topRankSlotCount > 0)
        {
            return topRankSlotCount;
        }

        var peopleMatch = PeopleSlotCountPattern.Match(sourceName);
        if (peopleMatch.Success &&
            int.TryParse(peopleMatch.Groups[1].Value, out var peopleSlotCount) &&
            peopleSlotCount > 0)
        {
            return peopleSlotCount;
        }

        if (sourceName.StartsWith("1위_", StringComparison.Ordinal) ||
            sourceName.Contains("이시각1위", StringComparison.Ordinal) ||
            sourceName.StartsWith("당선_", StringComparison.Ordinal) ||
            sourceName.StartsWith("경력_", StringComparison.Ordinal))
        {
            return 1;
        }

        if (sourceName.Contains("접전", StringComparison.Ordinal))
        {
            return 2;
        }

        return 0;
    }

    public async Task<IReadOnlyList<ScheduleRegionTarget>> ResolveScheduleRegionTargetsAsync(
        ChannelScheduleItem item,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        CancellationToken cancellationToken)
    {
        var electionType = ResolveScheduleElectionType(template, item.ScheduleElectionType);
        if (ScheduleTemplatePolicy.UsesSingleRegionOption(template))
        {
            return [CreateSingleScheduleRegionTarget(electionType)];
        }

        var options = await GetScheduleDistrictOptionsAsync(electionType, template, cancellationToken).ConfigureAwait(false);
        if (options.Count == 0)
        {
            return Array.Empty<ScheduleRegionTarget>();
        }

        if (IsNormalPanseMapTemplate(template))
        {
            return options
                .Select(option => CreateScheduleRegionTarget(option, electionType))
                .ToArray();
        }

        if (IsNormalPreElectionTurnoutDistrictBoardTemplate(template) &&
            item.RegionScope == ScheduleRegionScope.Single)
        {
            var regionGroupTargets = ResolveRegionGroupTargets(item, options, electionType);
            if (regionGroupTargets.Count > 1)
            {
                return regionGroupTargets;
            }
        }

        if (UsesHistoricalScheduleOptions(template) &&
            item.RegionScope is ScheduleRegionScope.Single or ScheduleRegionScope.RegionGroup)
        {
            var splitHistoricalTargets = ResolveCombinedHistoricalRegionTargets(item, options, electionType);
            if (splitHistoricalTargets.Count > 0)
            {
                return splitHistoricalTargets;
            }
        }

        return item.RegionScope switch
        {
            ScheduleRegionScope.StationRegions => ResolveStationRegionTargets(options, electionType, station),
            ScheduleRegionScope.RegionGroup => ResolveRegionGroupTargets(item, options, electionType),
            ScheduleRegionScope.Single => ResolveSingleRegionTarget(item, options, electionType),
            _ => options.Select(option => CreateScheduleRegionTarget(option, electionType)).ToArray()
        };
    }

    public async Task<ElectionDataSnapshot> GetScheduleSnapshotAsync(
        ChannelScheduleItem item,
        FormatTemplateDefinition template,
        ScheduleRegionTarget target,
        CancellationToken cancellationToken)
    {
        var electionType = string.IsNullOrWhiteSpace(target.ElectionType)
            ? ResolveScheduleElectionType(item.FormatName, item.ScheduleElectionType)
            : target.ElectionType;
        if (ScheduleTemplatePolicy.UsesSingleRegionOption(template))
        {
            return CreateSingleScheduleSnapshot(electionType, target);
        }

        if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name))
        {
            return CreateHistoricalTurnoutScheduleSnapshot(electionType, target);
        }

        if (ScheduleTemplatePolicy.IsHistoricalWinnerFormat(template.Name))
        {
            return CreateHistoricalWinnerScheduleSnapshot(electionType, target);
        }

        if (ShouldUseTurnoutOverviewSnapshot(template, electionType))
        {
            return await CreateTurnoutScheduleSnapshotAsync(
                    electionType,
                    [target],
                    includeNationalSlot: false,
                    includeRegionalBoardSlots: false,
                    includeEmptyRegionalBoardSlots: false,
                    maxRegionalSlots: 0,
                    cancellationToken)
                .ConfigureAwait(false);
        }

        var refreshPhase = ResolveScheduleRefreshPhase(template);
        var refreshResult = await _apiClient
            .RefreshAsync(refreshPhase, electionType, target.DisplayName, target.DistrictCode, cancellationToken)
            .ConfigureAwait(false);

        return CreateSnapshotFromRefreshResult(electionType, refreshResult, refreshPhase);
    }

    public Task<ElectionDataSnapshot> GetAggregateScheduleSnapshotAsync(
        ChannelScheduleItem item,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        var electionType = ResolveScheduleElectionType(template, item.ScheduleElectionType);
        if (IsWinnerTemplate(template))
        {
            return CreateCouncilWinnerScheduleSnapshotAsync(
                electionType,
                station,
                regionTargets,
                cancellationToken);
        }

        if (ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template.Name))
        {
            return CreateCouncilSeatScheduleSnapshotAsync(
                electionType,
                station,
                regionTargets,
                cancellationToken);
        }

        if (IsBottomTurnoutBoardTemplate(template) && ShouldUseTurnoutOverviewSnapshot(template, electionType))
        {
            return CreateTurnoutScheduleSnapshotAsync(
                electionType,
                regionTargets,
                includeNationalSlot: true,
                includeRegionalBoardSlots: true,
                includeEmptyRegionalBoardSlots: false,
                maxRegionalSlots: 4,
                cancellationToken);
        }

        if (IsRegionalTurnoutBoardTemplate(template) && ShouldUseTurnoutOverviewSnapshot(template, electionType))
        {
            return CreateTurnoutScheduleSnapshotAsync(
                electionType,
                regionTargets,
                includeNationalSlot: false,
                includeRegionalBoardSlots: true,
                includeEmptyRegionalBoardSlots: IsNormalPreElectionTurnoutDistrictBoardTemplate(template),
                maxRegionalSlots: ResolveRegionalTurnoutBoardMaxSlots(template),
                cancellationToken);
        }

        if (IsCurrentLeaderTemplate(template))
        {
            return CreateCurrentLeaderScheduleSnapshotAsync(
                electionType,
                station,
                regionTargets,
                cancellationToken);
        }

        if (IsNormalPanseMapTemplate(template))
        {
            return CreateNormalPanseMapScheduleSnapshotAsync(
                electionType,
                regionTargets,
                cancellationToken);
        }

        if (IsTopPanseTemplate(template))
        {
            return CreateTopPanseScheduleSnapshotAsync(
                electionType,
                template,
                station,
                regionTargets,
                cancellationToken);
        }

        throw new InvalidOperationException($"집계형 스냅샷을 지원하지 않는 포맷입니다. {template.Id}");
    }

    private async Task RefreshDistrictOptionsForElectionTypeAsync()
    {
        var revision = Interlocked.Increment(ref _districtOptionsRevision);

        try
        {
            if (SupportsApiDistrictOptions(ElectionType))
            {
                _logService.Info($"{ElectionType} 선거구 목록을 SBS API 기준으로 불러옵니다.");
                var options = await _apiClient.GetDistrictOptionsAsync(
                    ElectionType,
                    ResolveApiDistrictRegionScope(ElectionType),
                    CancellationToken.None);
                if (revision != _districtOptionsRevision)
                {
                    return;
                }

                var (preferredName, preferredCode, preferredRegionName) = CaptureCurrentDistrictPreferences();
                if (options.Count > 0)
                {
                    ApplyDistrictSelectionSource(options, preferredName, preferredCode, preferredRegionName);
                    _logService.Info($"{ElectionType} 선거구 목록 {options.Count}건을 적용했습니다.");
                    QueueSelectionRefresh("선거 종류 변경");
                    return;
                }

                if (IsBasicCouncilElectionType(ElectionType))
                {
                    ApplyDistrictSelectionSource(
                        Array.Empty<SbsElectionApiClient.DistrictSelectionOption>(),
                        preferredName,
                        preferredCode,
                        preferredRegionName);
                    _logService.Warning($"{ElectionType} 선거구 목록을 불러오지 못했습니다. 선택권역 설정을 확인하세요.");
                    return;
                }
            }

            var fallbackPreferences = CaptureCurrentDistrictPreferences();
            ApplyDistrictSelectionSource(
                DefaultDistrictOptions,
                fallbackPreferences.PreferredName,
                fallbackPreferences.PreferredCode,
                fallbackPreferences.PreferredRegionName);
            QueueSelectionRefresh("선거 종류 변경");
        }
        catch (Exception ex)
        {
            if (revision != _districtOptionsRevision)
            {
                return;
            }

            var fallbackPreferences = CaptureCurrentDistrictPreferences();
            if (IsBasicCouncilElectionType(ElectionType))
            {
                ApplyDistrictSelectionSource(
                    Array.Empty<SbsElectionApiClient.DistrictSelectionOption>(),
                    fallbackPreferences.PreferredName,
                    fallbackPreferences.PreferredCode,
                    fallbackPreferences.PreferredRegionName);
                _logService.Warning($"기초의원 선거구 목록 갱신 실패: {ex.Message}");
                return;
            }

            ApplyDistrictSelectionSource(
                DefaultDistrictOptions,
                fallbackPreferences.PreferredName,
                fallbackPreferences.PreferredCode,
                fallbackPreferences.PreferredRegionName);
            _logService.Warning($"선거구 목록 갱신 실패: {ex.Message}");
        }
    }

    private void QueueSelectionRefresh(string reason)
    {
        if (_isUpdatingDistrictOptions || _isApplyingRefreshResult || string.IsNullOrWhiteSpace(DistrictName))
        {
            return;
        }

        var revision = Interlocked.Increment(ref _selectionRefreshRevision);
        _ = RunSelectionRefreshAsync(reason, revision);
    }

    private async Task RunSelectionRefreshAsync(string reason, int revision)
    {
        await Task.Delay(TimeSpan.FromMilliseconds(200));
        if (revision != _selectionRefreshRevision || _isUpdatingDistrictOptions || _isApplyingRefreshResult)
        {
            return;
        }

        if (IsRefreshing)
        {
            await WaitForRefreshAsync(CancellationToken.None);
        }

        if (revision != _selectionRefreshRevision || _isUpdatingDistrictOptions || _isApplyingRefreshResult)
        {
            return;
        }

        await RefreshAsync(isManualRequest: false, triggerReason: reason);
    }

    public async Task RefreshAsync(bool isManualRequest, string? triggerReason = null)
    {
        if (isManualRequest && DateTimeOffset.Now - _lastManualRefreshAt < TimeSpan.FromSeconds(3))
        {
            _logService.Warning("수동 수신은 3초 이내 재요청할 수 없습니다.");
            return;
        }

        _refreshSignal = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
        IsRefreshing = true;

        try
        {
            if (isManualRequest)
            {
                _lastManualRefreshAt = DateTimeOffset.Now;
                _logService.Info($"수동 수신 요청을 처리합니다. 현재 단계는 {BroadcastPhaseLabel}입니다.");
            }
            else if (!string.IsNullOrWhiteSpace(triggerReason))
            {
                _logService.Info($"{triggerReason}으로 {BroadcastPhaseLabel} 데이터 수신을 처리합니다.");
            }
            else
            {
                _logService.Info($"Polling 기반 {BroadcastPhaseLabel} 데이터 수신을 처리합니다.");
            }

            var refreshResult = await _apiClient
                .RefreshAsync(BroadcastPhase, ElectionType, DistrictName, DistrictCode, CancellationToken.None);

            ApplyRefreshResult(refreshResult);

            if (IsDistrictOverviewMode)
            {
                await RefreshDistrictOverviewAsync(triggerReason ?? (isManualRequest ? "수동 갱신" : "자동 갱신"));
            }

            LastRefreshAt = refreshResult.ReceivedAt;
            SetRefreshWarningMessage(string.Empty);

            if (IsPreElectionPhase)
            {
                _logService.Info($"SBS API 연동 갱신 완료. 투표율={TurnoutRateDisplay}, 투표자={TurnoutVotes:N0} ({refreshResult.SourcePath})");
            }
            else
            {
                _logService.Info($"SBS API 연동 갱신 완료. 개표수={CountedVotes:N0}, 남은표={RemainingVotes:N0} ({refreshResult.SourcePath})");
            }
        }
        catch (Exception ex)
        {
            SetRefreshWarningMessage(ex.Message);
            _logService.Warning($"SBS API 수신 실패: {ex.Message}");
        }
        finally
        {
            IsRefreshing = false;
            _refreshSignal?.TrySetResult(true);
            _refreshSignal = null;

            if (isManualRequest && IsPollingEnabled)
            {
                StartPolling();
            }
        }
    }

    public void ReplaceCandidates(IEnumerable<CandidateEntry> candidates)
    {
        Candidates.Clear();
        foreach (var candidate in candidates)
        {
            Candidates.Add(candidate);
        }

        RecalculateJudgements();
        ApplyCareerPromisesToCurrentCandidates();
        RefreshCareerPromiseEditor();
        OnPropertyChanged(
            nameof(CountedVotes),
            nameof(RemainingVotes),
            nameof(CountedRateBaseVotes),
            nameof(CountedRateBaseLabel),
            nameof(CountedRate),
            nameof(CountedRateDisplay));
        OnPropertyChanged(
            nameof(SituationMetricPrimaryValue),
            nameof(SituationMetricSecondaryValue),
            nameof(HeaderMetricSummary),
            nameof(CareerPromiseVisibility),
            nameof(CareerPromiseContextText));
    }

    private Task SaveCareerPromisesAsync()
    {
        CancelCareerPromiseAutoSave();
        SaveCareerPromisesCore(refreshEditorAfterSave: true, isAutoSave: false);
        return Task.CompletedTask;
    }

    private void SaveCareerPromisesCore(bool refreshEditorAfterSave, bool isAutoSave)
    {
        try
        {
            var districtName = ResolveCareerPromiseDistrictName();
            var entries = CareerPromiseRows
                .Select(row => row.ToEntry(_selectedStationId, ElectionType, DistrictCode, districtName))
                .Where(entry => entry is not null)
                .Select(entry => entry!)
                .ToArray();

            _careerPromiseService.SaveEntries(_selectedStationId, ElectionType, DistrictCode, districtName, entries);
            ApplyCareerPromisesToCurrentCandidates();
            CareerPromiseHasPendingChanges = false;
            if (refreshEditorAfterSave)
            {
                RefreshCareerPromiseEditor();
            }

            CareerPromiseEditorStatusText = isAutoSave
                ? $"{DateTimeOffset.Now:HH:mm:ss} 자동 저장 완료"
                : $"{ElectionType} {districtName} 공약을 저장했습니다.";
        }
        catch (Exception ex)
        {
            _logService.Warning($"경력 컷 공약 저장에 실패했습니다: {ex.Message}");
            CareerPromiseEditorStatusText = $"경력 컷 공약 저장 실패: {ex.Message}";
        }
    }

    private void AddCareerPromiseRow()
    {
        var row = CreateCareerPromiseRow(
            candidateCode: string.Empty,
            candidateName: string.Empty,
            party: string.Empty,
            promises: null,
            districtName: ResolveCareerPromiseDistrictName());
        CareerPromiseRows.Add(row);
        CareerPromiseEditorStatusText = "새 공약 행을 추가했습니다. 내용을 입력하면 자동 저장됩니다.";
    }

    private void RemoveCareerPromiseRow(CareerPromiseEditRowViewModel row)
    {
        row.PropertyChanged -= CareerPromiseRow_PropertyChanged;
        if (!CareerPromiseRows.Remove(row))
        {
            return;
        }

        QueueCareerPromiseAutoSave("공약 행을 삭제했습니다. 자동 저장 대기 중입니다.");
    }

    private CareerPromiseEditRowViewModel CreateCareerPromiseRow(
        string candidateCode,
        string candidateName,
        string party,
        IReadOnlyList<string>? promises,
        string districtName)
    {
        var row = new CareerPromiseEditRowViewModel(
            candidateCode,
            candidateName,
            party,
            promises,
            districtName,
            RemoveCareerPromiseRow);
        row.PropertyChanged += CareerPromiseRow_PropertyChanged;
        return row;
    }

    private void QueueCareerPromiseAutoSave(string statusText)
    {
        if (_isUpdatingCareerPromiseRows)
        {
            return;
        }

        CareerPromiseHasPendingChanges = true;
        CareerPromiseEditorStatusText = statusText;
        CancelCareerPromiseAutoSave();

        var autoSaveCts = new CancellationTokenSource();
        _careerPromiseAutoSaveCts = autoSaveCts;
        _ = RunCareerPromiseAutoSaveAsync(autoSaveCts);
    }

    private async Task RunCareerPromiseAutoSaveAsync(CancellationTokenSource autoSaveCts)
    {
        try
        {
            await Task.Delay(TimeSpan.FromMilliseconds(800), autoSaveCts.Token).ConfigureAwait(false);
            await UiDispatcher.EnqueueAsync(() =>
            {
                if (!ReferenceEquals(_careerPromiseAutoSaveCts, autoSaveCts) ||
                    autoSaveCts.IsCancellationRequested)
                {
                    return;
                }

                SaveCareerPromisesCore(refreshEditorAfterSave: false, isAutoSave: true);
            }).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
        }
        finally
        {
            if (ReferenceEquals(_careerPromiseAutoSaveCts, autoSaveCts))
            {
                _careerPromiseAutoSaveCts = null;
            }

            autoSaveCts.Dispose();
        }
    }

    private void CancelCareerPromiseAutoSave()
    {
        var autoSaveCts = _careerPromiseAutoSaveCts;
        if (autoSaveCts is null)
        {
            return;
        }

        _careerPromiseAutoSaveCts = null;
        autoSaveCts.Cancel();
    }

    private void CareerPromiseRow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (_isUpdatingCareerPromiseRows)
        {
            return;
        }

        if (e.PropertyName is not nameof(CareerPromiseEditRowViewModel.DistrictName)
            and not nameof(CareerPromiseEditRowViewModel.CandidateCode)
            and not nameof(CareerPromiseEditRowViewModel.CandidateName)
            and not nameof(CareerPromiseEditRowViewModel.Party)
            and not nameof(CareerPromiseEditRowViewModel.Promise1)
            and not nameof(CareerPromiseEditRowViewModel.Promise2)
            and not nameof(CareerPromiseEditRowViewModel.Promise3))
        {
            return;
        }

        QueueCareerPromiseAutoSave("수정된 공약이 있습니다. 자동 저장 대기 중입니다.");
    }

    private void RefreshCareerPromiseEditor()
    {
        var districtName = ResolveCareerPromiseDistrictName();
        var contextKey = BuildCareerPromiseContextKey(ElectionType, districtName);
        IReadOnlyList<CareerPromiseEditRowViewModel>? preservedRows = null;
        if (CareerPromiseHasPendingChanges &&
            string.Equals(_careerPromiseEditorContextKey, contextKey, StringComparison.Ordinal))
        {
            preservedRows = CareerPromiseRows
                .ToArray();
        }

        var savedEntries = GetCareerPromiseEntries(ElectionType, DistrictCode, districtName);

        _isUpdatingCareerPromiseRows = true;
        try
        {
            foreach (var row in CareerPromiseRows)
            {
                row.PropertyChanged -= CareerPromiseRow_PropertyChanged;
            }

            CareerPromiseRows.Clear();
            var visibleRowKeys = new HashSet<string>(StringComparer.Ordinal);
            foreach (var candidate in SortCareerPromiseCandidates(Candidates))
            {
                IReadOnlyList<string>? promises = null;
                var preservedRow = preservedRows is null
                    ? null
                    : ResolveCareerPromiseRow(candidate, preservedRows);
                if (preservedRow is not null)
                {
                    promises = preservedRow.Promises;
                }
                else if (ResolveCareerPromiseEntry(candidate, savedEntries) is { } savedEntry)
                {
                    promises = savedEntry.Promises;
                }

                var row = CreateCareerPromiseRow(
                    candidate.CandidateCode,
                    candidate.Name,
                    candidate.Party,
                    promises,
                    districtName);
                CareerPromiseRows.Add(row);
                visibleRowKeys.Add(BuildCareerPromiseRowKey(row.CandidateName, row.Party));
            }

            if (preservedRows is not null)
            {
                foreach (var preservedRow in SortCareerPromiseRows(preservedRows))
                {
                    var rowKey = BuildCareerPromiseRowKey(preservedRow.CandidateName, preservedRow.Party);
                    if (!string.IsNullOrWhiteSpace(rowKey) && !visibleRowKeys.Add(rowKey))
                    {
                        continue;
                    }

                    CareerPromiseRows.Add(CreateCareerPromiseRow(
                        preservedRow.CandidateCode,
                        preservedRow.CandidateName,
                        preservedRow.Party,
                        preservedRow.Promises,
                        string.IsNullOrWhiteSpace(preservedRow.DistrictName) ? districtName : preservedRow.DistrictName));
                }
            }
            else
            {
                foreach (var savedEntry in SortCareerPromiseEntries(savedEntries))
                {
                    var rowKey = BuildCareerPromiseRowKey(savedEntry.CandidateName, savedEntry.Party);
                    if (!string.IsNullOrWhiteSpace(rowKey) && !visibleRowKeys.Add(rowKey))
                    {
                        continue;
                    }

                    CareerPromiseRows.Add(CreateCareerPromiseRow(
                        savedEntry.CandidateCode,
                        savedEntry.CandidateName,
                        savedEntry.Party,
                        savedEntry.Promises,
                        string.IsNullOrWhiteSpace(savedEntry.DistrictName) ? districtName : savedEntry.DistrictName));
                }
            }
        }
        finally
        {
            _isUpdatingCareerPromiseRows = false;
        }

        _careerPromiseEditorContextKey = contextKey;
        CareerPromiseHasPendingChanges = preservedRows is not null;
        if (CareerPromiseRows.Count == 0)
        {
            CareerPromiseEditorStatusText = "후보 데이터가 없어 경력 컷 공약을 편집할 수 없습니다.";
        }
        else if (!CareerPromiseHasPendingChanges)
        {
            CareerPromiseEditorStatusText = "현재 후보자 기준으로 공약을 편집할 수 있습니다.";
        }

        OnPropertyChanged(nameof(CareerPromiseVisibility), nameof(CareerPromiseContextText));
    }

    private void ApplyCareerPromisesToCurrentCandidates()
    {
        ApplyCareerPromisesToCandidates(
            Candidates,
            GetCareerPromiseEntries(ElectionType, DistrictCode, ResolveCareerPromiseDistrictName()));
    }

    private CandidateEntry[] BuildSnapshotCandidatesWithCareerPromises(
        IEnumerable<CandidateEntry> sourceCandidates,
        string electionType,
        string districtCode,
        string districtName)
    {
        var clonedCandidates = sourceCandidates.Select(candidate => candidate.Clone()).ToArray();
        ApplyCareerPromisesToCandidates(
            clonedCandidates,
            GetCareerPromiseEntries(electionType, districtCode, districtName));
        return clonedCandidates;
    }

    private IReadOnlyList<CareerPromiseEntry> GetCareerPromiseEntries(
        string electionType,
        string districtCode,
        string districtName)
    {
        if (string.IsNullOrWhiteSpace(districtName))
        {
            return Array.Empty<CareerPromiseEntry>();
        }

        return _careerPromiseService.GetEntries(_selectedStationId, electionType, districtCode, districtName);
    }

    private static void ApplyCareerPromisesToCandidates(
        IEnumerable<CandidateEntry> candidates,
        IReadOnlyList<CareerPromiseEntry> entries)
    {
        foreach (var candidate in candidates)
        {
            if (ResolveCareerPromiseEntry(candidate, entries) is { } entry)
            {
                candidate.ApplyCareerPromises(entry.Promises);
            }
            else
            {
                candidate.ApplyCareerPromises(Array.Empty<string>());
            }
        }
    }

    private static CareerPromiseEntry? ResolveCareerPromiseEntry(
        CandidateEntry candidate,
        IReadOnlyList<CareerPromiseEntry> entries)
    {
        if (entries.Count == 0)
        {
            return null;
        }

        var candidateName = NormalizeCareerPromiseLookupKey(candidate.Name);
        var party = NormalizeCareerPromiseLookupKey(candidate.Party);
        if (!string.IsNullOrWhiteSpace(candidateName) && !string.IsNullOrWhiteSpace(party))
        {
            var nameAndPartyMatch = entries.FirstOrDefault(entry =>
                string.Equals(NormalizeCareerPromiseLookupKey(entry.CandidateName), candidateName, StringComparison.Ordinal) &&
                string.Equals(NormalizeCareerPromiseLookupKey(entry.Party), party, StringComparison.Ordinal));
            if (nameAndPartyMatch is not null)
            {
                return nameAndPartyMatch;
            }
        }

        if (string.IsNullOrWhiteSpace(candidateName))
        {
            return null;
        }

        var nameMatches = entries
            .Where(entry => string.Equals(NormalizeCareerPromiseLookupKey(entry.CandidateName), candidateName, StringComparison.Ordinal))
            .Take(2)
            .ToArray();
        return nameMatches.Length == 1 ? nameMatches[0] : null;
    }

    private static CareerPromiseEditRowViewModel? ResolveCareerPromiseRow(
        CandidateEntry candidate,
        IReadOnlyList<CareerPromiseEditRowViewModel> rows)
    {
        if (rows.Count == 0)
        {
            return null;
        }

        var candidateName = NormalizeCareerPromiseLookupKey(candidate.Name);
        var party = NormalizeCareerPromiseLookupKey(candidate.Party);
        if (!string.IsNullOrWhiteSpace(candidateName) && !string.IsNullOrWhiteSpace(party))
        {
            var nameAndPartyMatch = rows.FirstOrDefault(row =>
                string.Equals(NormalizeCareerPromiseLookupKey(row.CandidateName), candidateName, StringComparison.Ordinal) &&
                string.Equals(NormalizeCareerPromiseLookupKey(row.Party), party, StringComparison.Ordinal));
            if (nameAndPartyMatch is not null)
            {
                return nameAndPartyMatch;
            }
        }

        if (string.IsNullOrWhiteSpace(candidateName))
        {
            return null;
        }

        var nameMatches = rows
            .Where(row => string.Equals(NormalizeCareerPromiseLookupKey(row.CandidateName), candidateName, StringComparison.Ordinal))
            .Take(2)
            .ToArray();
        return nameMatches.Length == 1 ? nameMatches[0] : null;
    }

    private static string NormalizeCareerPromiseLookupKey(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        return string.Concat(value.Where(character => !char.IsWhiteSpace(character)))
            .ToUpperInvariant();
    }

    private static string BuildCareerPromiseRowKey(
        string? candidateName,
        string? party)
    {
        var normalizedCandidateName = NormalizeCareerPromiseLookupKey(candidateName);
        var normalizedParty = NormalizeCareerPromiseLookupKey(party);
        return string.IsNullOrWhiteSpace(normalizedCandidateName) && string.IsNullOrWhiteSpace(normalizedParty)
            ? string.Empty
            : $"{normalizedCandidateName}|{normalizedParty}";
    }

    private static IEnumerable<CandidateEntry> SortCareerPromiseCandidates(IEnumerable<CandidateEntry> candidates)
    {
        return candidates
            .Select(candidate => new
            {
                Candidate = candidate,
                SortKey = BuildCareerPromiseCandidateCodeSortKey(candidate.CandidateCode)
            })
            .OrderBy(entry => entry.SortKey.Group)
            .ThenBy(entry => entry.SortKey.Prefix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.SortKey.Number)
            .ThenBy(entry => entry.SortKey.Suffix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.Candidate.Name, StringComparer.Ordinal)
            .Select(entry => entry.Candidate);
    }

    private static IEnumerable<CareerPromiseEditRowViewModel> SortCareerPromiseRows(IEnumerable<CareerPromiseEditRowViewModel> rows)
    {
        return rows
            .Select(row => new
            {
                Row = row,
                SortKey = BuildCareerPromiseCandidateCodeSortKey(row.CandidateCode)
            })
            .OrderBy(entry => entry.SortKey.Group)
            .ThenBy(entry => entry.SortKey.Prefix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.SortKey.Number)
            .ThenBy(entry => entry.SortKey.Suffix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.Row.CandidateName, StringComparer.Ordinal)
            .Select(entry => entry.Row);
    }

    private static IEnumerable<CareerPromiseEntry> SortCareerPromiseEntries(IEnumerable<CareerPromiseEntry> entries)
    {
        return entries
            .Select(entry => new
            {
                Entry = entry,
                SortKey = BuildCareerPromiseCandidateCodeSortKey(entry.CandidateCode)
            })
            .OrderBy(entry => entry.SortKey.Group)
            .ThenBy(entry => entry.SortKey.Prefix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.SortKey.Number)
            .ThenBy(entry => entry.SortKey.Suffix, StringComparer.OrdinalIgnoreCase)
            .ThenBy(entry => entry.Entry.CandidateName, StringComparer.Ordinal)
            .Select(entry => entry.Entry);
    }

    private static (int Group, string Prefix, int Number, string Suffix) BuildCareerPromiseCandidateCodeSortKey(string? candidateCode)
    {
        var normalized = candidateCode?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(normalized))
        {
            return (1, string.Empty, int.MaxValue, string.Empty);
        }

        var match = CareerPromiseCandidateCodeNumberPattern.Match(normalized);
        if (!match.Success || !int.TryParse(match.Value, out var number))
        {
            return (0, normalized, int.MaxValue, string.Empty);
        }

        var prefix = normalized[..match.Index];
        var suffix = normalized[(match.Index + match.Length)..];
        return (0, prefix, number, suffix);
    }

    private string ResolveCareerPromiseDistrictName()
    {
        return FirstNonWhiteSpace(DistrictName, ElectionDistrictName, RegionName);
    }

    private static string BuildCareerPromiseContextKey(
        string electionType,
        string districtName)
    {
        return $"{electionType.Trim()}|{NormalizeCareerPromiseLookupKey(districtName)}";
    }

    private void SetRefreshWarningMessage(string? message)
    {
        var normalized = message ?? string.Empty;
        if (string.Equals(_lastRefreshWarningMessage, normalized, StringComparison.Ordinal))
        {
            return;
        }

        _lastRefreshWarningMessage = normalized;
        OnPropertyChanged(nameof(StatusText), nameof(HasLiveDataSignal));
    }

    public void Dispose()
    {
        StopPolling();
        CancelCareerPromiseAutoSave();
        _apiClient.Dispose();
    }

    private async Task RunPollingLoopAsync(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                var remainingSeconds = PollingIntervalSeconds;
                PollingCountdownSeconds = remainingSeconds;

                while (remainingSeconds > 0)
                {
                    await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
                    remainingSeconds--;
                    PollingCountdownSeconds = remainingSeconds;
                }

                await RefreshAsync(isManualRequest: false);
            }
            catch (OperationCanceledException)
            {
                return;
            }
        }
    }

    private void StopPolling()
    {
        _pollingCts?.Cancel();
        _pollingCts = null;
        PollingCountdownSeconds = 0;
    }

    private void ApplyRefreshResult(SbsElectionApiClient.SbsElectionRefreshResult refreshResult)
    {
        _isApplyingRefreshResult = true;
        try
        {
            var refreshDistrictName = ResolveRefreshResultDistrictName(refreshResult);
            if (!string.IsNullOrWhiteSpace(refreshDistrictName))
            {
                DistrictName = refreshDistrictName;
            }

            if (!string.IsNullOrWhiteSpace(refreshResult.DistrictCode))
            {
                DistrictCode = refreshResult.DistrictCode;
            }

            ApplyResolvedLocation(refreshResult.RegionName, refreshResult.ElectionDistrictName, refreshResult.DistrictName);

            _countedRateFromApi = refreshResult.CountedRate;
            _countedVotesFromApi = refreshResult.CountedVotes;
            _remainingVotesFromApi = refreshResult.RemainingVotes;
            _referenceTimeLabel = refreshResult.ReferenceTimeLabel;

            TotalExpectedVotes = refreshResult.TotalExpectedVotes;
            TurnoutVotes = refreshResult.TurnoutVotes;

            if (refreshResult.Candidates is not null)
            {
                ReplaceCandidates(refreshResult.Candidates);
            }

            OnPropertyChanged(
                nameof(CountedVotes),
                nameof(RemainingVotes),
                nameof(CountedRateBaseVotes),
                nameof(CountedRateBaseLabel),
                nameof(CountedRate),
                nameof(CountedRateDisplay));
            OnPropertyChanged(nameof(SituationMetricPrimaryValue), nameof(SituationMetricSecondaryValue), nameof(HeaderMetricSummary));
            RefreshPreElectionHistoryPresentation();
        }
        finally
        {
            _isApplyingRefreshResult = false;
        }
    }

    private void RecalculateJudgements()
    {
        foreach (var candidate in Candidates)
        {
            candidate.AutomaticJudgement = CandidateJudgement.None;
        }
    }

    private void NotifyModePresentationChanged()
    {
        OnPropertyChanged(
            nameof(IsPreElectionPhase),
            nameof(IsCountingPhase),
            nameof(BroadcastPhaseLabel),
            nameof(BroadcastPhaseBadgeText),
            nameof(BroadcastPhaseDetailText),
            nameof(IsCurrentApiSelectionSupported),
            nameof(HeaderMetricSummary),
            nameof(SituationMetricPrimaryLabel),
            nameof(SituationMetricPrimaryValue),
            nameof(SituationMetricSecondaryLabel),
            nameof(SituationMetricSecondaryValue),
            nameof(TotalExpectedVotesLabel),
            nameof(CountedRateBaseLabel),
            nameof(TurnoutBoardVisibility),
            nameof(PreElectionHistoryVisibility),
            nameof(DistrictOverviewTitle),
            nameof(DistrictOverviewVisibility),
            nameof(CandidateBoardVisibility),
            nameof(CareerPromiseVisibility),
            nameof(PreElectionHistoryTitle));

        UpdateDistrictOverviewStatus();
        RefreshPreElectionHistoryPresentation();
    }

    private void NotifyMetricPresentationChanged()
    {
        OnPropertyChanged(
            nameof(CountedVotes),
            nameof(RemainingVotes),
            nameof(CountedRateBaseVotes),
            nameof(CountedRateBaseLabel),
            nameof(CountedRate),
            nameof(CountedRateDisplay),
            nameof(TurnoutRemainingVotes),
            nameof(TurnoutRate),
            nameof(TurnoutRateDisplay),
            nameof(SituationMetricPrimaryValue),
            nameof(SituationMetricSecondaryValue),
            nameof(HeaderMetricSummary));
    }

    private void ApplyResolvedLocation((string RegionName, string ElectionDistrictName) location)
    {
        RegionName = location.RegionName;
        ElectionDistrictName = location.ElectionDistrictName;
        RefreshPreElectionHistoryPresentation();
        OnPropertyChanged(nameof(CareerPromiseContextText));
    }

    private void ApplyResolvedLocation(string regionName, string electionDistrictName, string fallbackDistrictName)
    {
        var normalizedRegionName = string.IsNullOrWhiteSpace(regionName) ? fallbackDistrictName : regionName;
        var normalizedElectionDistrictName = string.IsNullOrWhiteSpace(electionDistrictName) ? fallbackDistrictName : electionDistrictName;
        ApplyResolvedLocation((normalizedRegionName ?? string.Empty, normalizedElectionDistrictName ?? string.Empty));
    }

    private void RefreshPreElectionHistorySelectionOptions()
    {
        var selectionRecords = _preElectionHistoryService.GetSelectionRecords(PreElectionHistoryElectionType);
        _isUpdatingPreElectionHistoryDistrictOptions = true;
        try
        {
            ReplaceOptions(
                PreElectionHistoryDistrictOptions,
                selectionRecords.Select(record => new SelectionOption<string>(record.DisplayName, record.DisplayName)));

            var preferredValue = string.IsNullOrWhiteSpace(PreElectionHistoryDistrictName)
                ? _districtName
                : PreElectionHistoryDistrictName;
            var resolvedValue = PreElectionHistoryDistrictOptions.Any(option => string.Equals(option.Value, preferredValue, StringComparison.Ordinal))
                ? preferredValue
                : PreElectionHistoryDistrictOptions.FirstOrDefault()?.Value ?? string.Empty;

            if (!string.Equals(_preElectionHistoryDistrictName, resolvedValue, StringComparison.Ordinal))
            {
                _preElectionHistoryDistrictName = resolvedValue;
                OnPropertyChanged(nameof(PreElectionHistoryDistrictName));
            }
        }
        finally
        {
            _isUpdatingPreElectionHistoryDistrictOptions = false;
        }
    }

    private void RefreshPreElectionHistoryPresentation()
    {
        var requestedDistrictName = string.IsNullOrWhiteSpace(PreElectionHistoryDistrictName)
            ? (string.IsNullOrWhiteSpace(DistrictName) ? RegionName : DistrictName)
            : PreElectionHistoryDistrictName;
        var history = _preElectionHistoryService.ResolveHistory(
            PreElectionHistoryElectionType,
            requestedDistrictName,
            requestedDistrictName);
        if (history is null)
        {
            _preElectionHistoryRegionLabel = requestedDistrictName;
            _resolvedHistoricalTurnout = Array.Empty<PreElectionHistoricalTurnoutEntry>();
            _resolvedHistoricalWinners = Array.Empty<PreElectionHistoricalWinnerEntry>();
            ReplacePreElectionTurnoutHistory([]);
            ReplacePreElectionWinnerHistory([]);
            PopulatePreElectionHistoryEditor(null);
            PreElectionHistoryStatusText = $"{PreElectionHistoryTitle} 저장형 데이터가 아직 없습니다.";
            NotifyPreElectionHistoryChanged();
            return;
        }

        _preElectionHistoryRegionLabel = history.DisplayName;
        _resolvedHistoricalTurnout = history.TurnoutHistory
            .OrderBy(entry => entry.Year)
            .ToArray();
        _resolvedHistoricalWinners = history.WinnerHistory
            .OrderBy(entry => entry.ElectionOrder)
            .ToArray();

        ReplacePreElectionTurnoutHistory(
            _resolvedHistoricalTurnout.Select(entry => new PreElectionTurnoutHistoryCardViewModel
            {
                YearLabel = entry.Year.ToString(),
                TurnoutRateDisplay = $"{entry.TurnoutRate:0.0}%",
                DetailText = $"선거인 {entry.Electors:N0} / 투표 {entry.Votes:N0}"
            }));
        ReplacePreElectionWinnerHistory(
            _resolvedHistoricalWinners.Select(entry => new PreElectionWinnerHistoryCardViewModel
            {
                ElectionLabel = $"{entry.ElectionOrder}회 · {entry.Year}",
                WinnerName = entry.Name,
                PartyName = entry.Party,
                Note = entry.Note
            }));
        PopulatePreElectionHistoryEditor(history);

        var turnoutSummary = _resolvedHistoricalTurnout.Count > 0
            ? $"투표율 {_resolvedHistoricalTurnout.First().Year}~{_resolvedHistoricalTurnout.Last().Year}"
            : "투표율 없음";
        var winnerSummary = _resolvedHistoricalWinners.Count > 0
            ? $"당선자 {_resolvedHistoricalWinners.First().ElectionOrder}~{_resolvedHistoricalWinners.Last().ElectionOrder}회"
            : "당선자 없음";
        PreElectionHistoryStatusText = $"{_preElectionHistoryRegionLabel} {PreElectionHistoryElectionType} 기준 저장형 데이터입니다. {turnoutSummary} / {winnerSummary}";
        NotifyPreElectionHistoryChanged();
    }

    private void NotifyPreElectionHistoryChanged()
    {
        OnPropertyChanged(
            nameof(PreElectionHistoryTitle),
            nameof(PreElectionHistorySummaryText),
            nameof(PreElectionHistoryCoverageText),
            nameof(PreElectionHistoryVisibility),
            nameof(PreElectionTurnoutHistoryVisibility),
            nameof(PreElectionWinnerHistoryVisibility));
    }

    private void PopulatePreElectionHistoryEditor(PreElectionHistoryRecord? history)
    {
        var turnoutByOrder = (history?.TurnoutHistory ?? Array.Empty<PreElectionHistoricalTurnoutEntry>())
            .GroupBy(entry => entry.ElectionOrder)
            .ToDictionary(group => group.Key, group => group.First());
        var winnerByOrder = (history?.WinnerHistory ?? Array.Empty<PreElectionHistoricalWinnerEntry>())
            .GroupBy(entry => entry.ElectionOrder)
            .ToDictionary(group => group.Key, group => group.First());

        PreElectionHistoryEditableTurnoutRows.Clear();
        foreach (var slot in GetPreElectionHistoryTurnoutSlots(PreElectionHistoryElectionType))
        {
            turnoutByOrder.TryGetValue(slot.ElectionOrder, out var entry);
            var row = new PreElectionHistoryTurnoutEditRowViewModel(slot.ElectionOrder, slot.Year, entry);
            row.PropertyChanged += PreElectionHistoryEditRow_PropertyChanged;
            PreElectionHistoryEditableTurnoutRows.Add(row);
        }

        PreElectionHistoryEditableWinnerRows.Clear();
        foreach (var slot in GetPreElectionHistoryWinnerSlots(PreElectionHistoryElectionType))
        {
            winnerByOrder.TryGetValue(slot.ElectionOrder, out var entry);
            var row = new PreElectionHistoryWinnerEditRowViewModel(slot.ElectionOrder, slot.Year, PreElectionColorPartyOptions, entry);
            row.PropertyChanged += PreElectionHistoryEditRow_PropertyChanged;
            PreElectionHistoryEditableWinnerRows.Add(row);
        }

        PreElectionHistoryHasPendingChanges = false;
        PreElectionHistoryEditorStatusText = history is null
            ? "선택한 저장형 데이터가 없습니다."
            : "셀을 수정한 뒤 저장하면 사전 데이터 JSON에 반영됩니다.";
    }

    private void PreElectionHistoryEditRow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (string.Equals(e.PropertyName, nameof(PreElectionHistoryTurnoutEditRowViewModel.TurnoutRateDisplay), StringComparison.Ordinal))
        {
            return;
        }

        if (!PreElectionHistoryHasPendingChanges)
        {
            PreElectionHistoryEditorStatusText = "수정된 값이 있습니다. 저장 버튼을 눌러 반영하세요.";
        }

        PreElectionHistoryHasPendingChanges = true;
    }

    private async Task SavePreElectionHistoryAsync()
    {
        try
        {
            var history = _preElectionHistoryService.ResolveHistory(
                PreElectionHistoryElectionType,
                PreElectionHistoryDistrictName,
                PreElectionHistoryDistrictName);
            if (history is null)
            {
                throw new InvalidOperationException("저장할 사전 데이터 레코드를 찾지 못했습니다.");
            }

            var updatedRecord = new PreElectionHistoryRecord
            {
                ElectionType = history.ElectionType,
                Key = history.Key,
                RegionKey = history.RegionKey,
                RegionName = history.RegionName,
                DistrictName = history.DistrictName,
                DisplayName = history.DisplayName,
                TurnoutHistory = PreElectionHistoryEditableTurnoutRows
                    .Select(row => row.ToEntry())
                    .Where(entry => entry is not null)
                    .Select(entry => entry!)
                    .OrderBy(entry => entry.ElectionOrder)
                    .ToArray(),
                WinnerHistory = PreElectionHistoryEditableWinnerRows
                    .Select(row => row.ToEntry())
                    .Where(entry => entry is not null)
                    .Select(entry => entry!)
                    .OrderBy(entry => entry.ElectionOrder)
                    .ToArray()
            };

            _preElectionHistoryService.SaveRecord(updatedRecord);
            RefreshPreElectionHistorySelectionOptions();
            RefreshPreElectionHistoryPresentation();
            PreElectionHistoryEditorStatusText = $"{updatedRecord.DisplayName} 저장을 완료했습니다.";
        }
        catch (Exception ex)
        {
            _logService.Warning($"사전 데이터 저장에 실패했습니다: {ex.Message}");
            PreElectionHistoryEditorStatusText = $"저장 실패: {ex.Message}";
        }

        await Task.CompletedTask;
    }

    private void ReplacePreElectionTurnoutHistory(IEnumerable<PreElectionTurnoutHistoryCardViewModel> cards)
    {
        PreElectionTurnoutHistory.Clear();
        foreach (var card in cards)
        {
            PreElectionTurnoutHistory.Add(card);
        }
    }

    private void ReplacePreElectionWinnerHistory(IEnumerable<PreElectionWinnerHistoryCardViewModel> cards)
    {
        PreElectionWinnerHistory.Clear();
        foreach (var card in cards)
        {
            PreElectionWinnerHistory.Add(card);
        }
    }

    private (IReadOnlyList<PreElectionHistoricalTurnoutEntry> TurnoutHistory, IReadOnlyList<PreElectionHistoricalWinnerEntry> WinnerHistory) ResolvePreElectionHistoryRecords(
        string electionType,
        string regionName,
        string districtName,
        bool includeOutsidePreElection = false)
    {
        if (!includeOutsidePreElection && BroadcastPhase != BroadcastPhase.PreElection)
        {
            return (Array.Empty<PreElectionHistoricalTurnoutEntry>(), Array.Empty<PreElectionHistoricalWinnerEntry>());
        }

        var history = _preElectionHistoryService.ResolveHistory(electionType, regionName, districtName);
        if (history is null)
        {
            return (Array.Empty<PreElectionHistoricalTurnoutEntry>(), Array.Empty<PreElectionHistoricalWinnerEntry>());
        }

        return (
            history.TurnoutHistory.OrderBy(entry => entry.Year).ToArray(),
            history.WinnerHistory.OrderBy(entry => entry.ElectionOrder).ToArray());
    }

    private static IReadOnlyList<(int ElectionOrder, int Year)> GetPreElectionHistoryTurnoutSlots(string electionType)
    {
        return string.Equals(PreElectionHistoryService.NormalizeElectionType(electionType), "교육감", StringComparison.Ordinal)
            ? HistoricalTurnoutSlotsEducation
            : HistoricalTurnoutSlotsLocal;
    }

    private static IReadOnlyList<(int ElectionOrder, int Year)> GetPreElectionHistoryWinnerSlots(string electionType)
    {
        return string.Equals(PreElectionHistoryService.NormalizeElectionType(electionType), "교육감", StringComparison.Ordinal)
            ? HistoricalTurnoutSlotsEducation
            : HistoricalWinnerSlotsAll;
    }

    private (string RegionName, string ElectionDistrictName) ResolveLocationFields(string districtName)
    {
        if (!string.IsNullOrWhiteSpace(districtName) &&
            _districtOptionMap.TryGetValue(districtName, out var selectedOption))
        {
            return (
                selectedOption.RegionName ?? string.Empty,
                selectedOption.DistrictName ?? selectedOption.DisplayName ?? string.Empty);
        }

        if (!string.Equals(ElectionType, "기초단체장", StringComparison.Ordinal) || string.IsNullOrWhiteSpace(districtName))
        {
            return (districtName ?? string.Empty, districtName ?? string.Empty);
        }

        var separatorIndex = districtName.IndexOf(' ');
        if (separatorIndex <= 0 || separatorIndex >= districtName.Length - 1)
        {
            return (districtName, districtName);
        }

        return (
            districtName[..separatorIndex].Trim(),
            districtName[(separatorIndex + 1)..].Trim());
    }

    private string ResolveRefreshResultDistrictName(SbsElectionApiClient.SbsElectionRefreshResult refreshResult)
    {
        var districtName = refreshResult.DistrictName ?? string.Empty;
        var canonicalSelectionName = ResolveDistrictViewSelectionName(districtName, refreshResult.DistrictCode);
        return string.IsNullOrWhiteSpace(canonicalSelectionName)
            ? districtName
            : canonicalSelectionName;
    }

    private string ResolveDistrictViewSelectionName(string? districtViewName, string? districtCode)
    {
        var normalizedDistrictCode = districtCode?.Trim() ?? string.Empty;
        if (!string.IsNullOrWhiteSpace(normalizedDistrictCode))
        {
            var codeMatch = _districtOptionMap.Values.FirstOrDefault(option =>
                string.Equals(option.DistrictCode, normalizedDistrictCode, StringComparison.OrdinalIgnoreCase));
            if (codeMatch is not null)
            {
                return codeMatch.DisplayName;
            }
        }

        var normalizedName = districtViewName?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(normalizedName))
        {
            return string.Empty;
        }

        if (_districtOptionMap.ContainsKey(normalizedName))
        {
            return normalizedName;
        }

        var normalizedDistrictKey = NormalizeDistrictKey(normalizedName);
        if (!string.IsNullOrWhiteSpace(normalizedDistrictKey))
        {
            var nameMatches = _districtOptionMap.Values
                .Where(option =>
                    string.Equals(NormalizeDistrictKey(option.DisplayName), normalizedDistrictKey, StringComparison.Ordinal) ||
                    string.Equals(NormalizeDistrictKey(option.DistrictName), normalizedDistrictKey, StringComparison.Ordinal))
                .ToArray();
            if (nameMatches.Length == 1)
            {
                return nameMatches[0].DisplayName;
            }
        }

        return normalizedName;
    }

    private void ReapplyDistrictSelectionOptions()
    {
        var preferences = CaptureCurrentDistrictPreferences();
        ApplyDistrictSelectionSource(
            _districtSelectionSource,
            preferences.PreferredName,
            preferences.PreferredCode,
            preferences.PreferredRegionName);
    }

    private void ApplyDistrictSelectionSource(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> sourceOptions,
        string preferredName,
        string preferredCode,
        string preferredRegionName)
    {
        _districtSelectionSource = sourceOptions;
        ApplyDistrictSelectionOptions(
            GetVisibleDistrictSelectionOptions(sourceOptions),
            preferredName,
            preferredCode,
            preferredRegionName);
    }

    private IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> GetVisibleDistrictSelectionOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> sourceOptions)
    {
        if (!ShowOnlyConfiguredRegions || _configuredRegions.Count == 0 || sourceOptions.Count == 0)
        {
            return sourceOptions;
        }

        var filteredOptions = GetConfiguredDistrictSelectionOptions(sourceOptions);

        return filteredOptions.Length > 0 ? filteredOptions : sourceOptions;
    }

    private IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> GetDistrictOverviewSelectionOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> sourceOptions)
    {
        if (IsStationRegionOverviewMode)
        {
            return GetConfiguredDistrictSelectionOptions(sourceOptions);
        }

        return GetVisibleDistrictSelectionOptions(sourceOptions);
    }

    private SbsElectionApiClient.DistrictSelectionOption[] GetConfiguredDistrictSelectionOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> sourceOptions)
    {
        if (_configuredRegions.Count == 0 || sourceOptions.Count == 0)
        {
            return [];
        }

        return sourceOptions
            .Where(option => MatchesConfiguredRegion(option.RegionName, _configuredRegions))
            .ToArray();
    }

    private void ApplyDistrictSelectionOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string preferredName,
        string preferredCode,
        string preferredRegionName)
    {
        var capturedPreferredName = preferredName ?? string.Empty;
        var capturedPreferredCode = preferredCode ?? string.Empty;
        var capturedPreferredRegionName = preferredRegionName ?? string.Empty;
        _isUpdatingDistrictOptions = true;
        try
        {
            ReplaceOptions(
                DistrictOptions,
                options.Select(option => new SelectionOption<string>(option.DisplayName, option.DisplayName)));
            ReplaceOptions(
                DistrictViewOptions,
                CreateDistrictViewSelectionOptions(options));

            _districtCodeMap.Clear();
            _districtOptionMap.Clear();
            foreach (var option in options)
            {
                _districtCodeMap[option.DisplayName] = option.DistrictCode;
                _districtOptionMap[option.DisplayName] = option;
            }

            if (options.Count == 0)
            {
                EnsureOptionExists(DistrictOptions, capturedPreferredName);
                if (!string.IsNullOrWhiteSpace(capturedPreferredName))
                {
                    DistrictName = capturedPreferredName;
                }

                if (!string.IsNullOrWhiteSpace(capturedPreferredCode))
                {
                    DistrictCode = capturedPreferredCode;
                }

                return;
            }

            var selectedName = ResolvePreferredDistrictSelection(
                options,
                capturedPreferredName,
                capturedPreferredCode,
                capturedPreferredRegionName);

            DistrictName = selectedName;

            if (!string.IsNullOrWhiteSpace(selectedName) &&
                _districtCodeMap.TryGetValue(selectedName, out var selectedCode))
            {
                DistrictCode = selectedCode;
            }
            else if (!string.IsNullOrWhiteSpace(capturedPreferredCode))
            {
                DistrictCode = capturedPreferredCode;
            }

            if (IsDistrictOverviewMode)
            {
                OnPropertyChanged(nameof(SelectedDistrictViewName));
            }
            else
            {
                SyncSelectedDistrictViewName(DistrictName);
            }
        }
        finally
        {
            _isUpdatingDistrictOptions = false;
        }

        UpdateDistrictOverviewStatus();
        if (IsDistrictOverviewMode)
        {
            _ = RefreshDistrictOverviewAsync("선거구 목록 변경");
        }
    }

    private string ResolvePreferredRegionName(string preferredName, string preferredCode)
    {
        if (!string.IsNullOrWhiteSpace(preferredName) &&
            _districtOptionMap.TryGetValue(preferredName, out var selectedOption))
        {
            return selectedOption.RegionName;
        }

        if (!string.IsNullOrWhiteSpace(preferredName))
        {
            var separatorIndex = preferredName.IndexOf(' ');
            if (separatorIndex > 0)
            {
                return preferredName[..separatorIndex];
            }

            return preferredName;
        }

        var matchedByCode = DefaultDistrictOptions.FirstOrDefault(option =>
            string.Equals(option.DistrictCode, preferredCode, StringComparison.OrdinalIgnoreCase));
        return matchedByCode?.RegionName ?? string.Empty;
    }

    private (string PreferredName, string PreferredCode, string PreferredRegionName) CaptureCurrentDistrictPreferences()
    {
        var preferredName = DistrictName;
        var preferredCode = DistrictCode;
        var preferredRegionName = ResolvePreferredRegionName(preferredName, preferredCode);
        return (preferredName, preferredCode, preferredRegionName);
    }

    private static string ResolvePreferredDistrictSelection(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string preferredName,
        string preferredCode,
        string preferredRegionName)
    {
        if (!string.IsNullOrWhiteSpace(preferredName))
        {
            var directMatch = options.FirstOrDefault(option =>
                string.Equals(option.DisplayName, preferredName, StringComparison.Ordinal));
            if (directMatch is not null)
            {
                return directMatch.DisplayName;
            }
        }

        if (!string.IsNullOrWhiteSpace(preferredCode))
        {
            var directCodeMatch = options.FirstOrDefault(option =>
                string.Equals(option.DistrictCode, preferredCode, StringComparison.OrdinalIgnoreCase));
            if (directCodeMatch is not null)
            {
                return directCodeMatch.DisplayName;
            }
        }

        var normalizedPreferredName = NormalizeDistrictKey(preferredName);
        if (!string.IsNullOrWhiteSpace(normalizedPreferredName))
        {
            var byName = options
                .Where(option =>
                    string.Equals(NormalizeDistrictKey(option.DisplayName), normalizedPreferredName, StringComparison.Ordinal) ||
                    string.Equals(NormalizeDistrictKey(option.DistrictName), normalizedPreferredName, StringComparison.Ordinal))
                .ToArray();

            if (!string.IsNullOrWhiteSpace(preferredCode))
            {
                var byParentCode = byName
                    .Where(option => string.Equals(option.ParentRegionCode, preferredCode, StringComparison.OrdinalIgnoreCase))
                    .ToArray();

                if (byParentCode.Length == 1)
                {
                    return byParentCode[0].DisplayName;
                }
            }

            if (byName.Length == 1)
            {
                return byName[0].DisplayName;
            }
        }

        if (!string.IsNullOrWhiteSpace(preferredRegionName))
        {
            var byRegion = options.FirstOrDefault(option =>
                string.Equals(option.RegionName, preferredRegionName, StringComparison.Ordinal));
            if (byRegion is not null)
            {
                return byRegion.DisplayName;
            }
        }

        if (!string.IsNullOrWhiteSpace(preferredCode))
        {
            var byParent = options.FirstOrDefault(option =>
                string.Equals(option.ParentRegionCode, preferredCode, StringComparison.OrdinalIgnoreCase));
            if (byParent is not null)
            {
                return byParent.DisplayName;
            }
        }

        return options[0].DisplayName;
    }

    private static void ReplaceOptions(
        ObservableCollection<SelectionOption<string>> target,
        IEnumerable<SelectionOption<string>> options)
    {
        target.Clear();
        foreach (var option in options)
        {
            target.Add(option);
        }
    }

    private async Task RefreshDistrictOverviewAsync(string reason)
    {
        if (!IsDistrictOverviewMode)
        {
            return;
        }

        if (!IsCurrentApiSelectionSupported)
        {
            ReplaceDistrictOverviewCards([]);
            DistrictOverviewStatusText = $"{ElectionType} {BroadcastPhaseLabel} 단계는 {GetDistrictOverviewModeLabel()}를 지원하지 않습니다.";
            return;
        }

        var visibleOptions = GetDistrictOverviewSelectionOptions(_districtSelectionSource);
        if (visibleOptions.Count == 0)
        {
            ReplaceDistrictOverviewCards([]);
            DistrictOverviewStatusText = GetDistrictOverviewEmptyStateText();
            return;
        }

        var revision = Interlocked.Increment(ref _districtOverviewRefreshRevision);
        _isRefreshingDistrictOverview = true;
        DistrictOverviewStatusText = IsPreElectionPhase
            ? $"{reason}으로 지역별 투표율을 불러오는 중입니다."
            : $"{reason}으로 지역별 개표율을 불러오는 중입니다.";

        try
        {
            if (IsPreElectionPhase)
            {
                var turnoutOverview = await _apiClient.GetTurnoutOverviewAsync(ElectionType, visibleOptions, CancellationToken.None);
                if (revision != _districtOverviewRefreshRevision || !IsDistrictOverviewMode)
                {
                    return;
                }

                ReplaceDistrictOverviewCards(
                    turnoutOverview.Items.Select(item => new DistrictOverviewCardViewModel
                    {
                        DistrictViewName = item.DisplayName,
                        DistrictCode = item.DistrictCode,
                        RegionName = item.DisplayName,
                        CountedRateDisplay = $"{item.TurnoutRate:0.0}%",
                        DetailText = BuildTurnoutOverviewDetailText(
                            item,
                            FirstNonWhiteSpace(item.ReferenceTimeLabel, turnoutOverview.ReferenceTimeLabel))
                    }));

                var referenceTimeStatus = string.IsNullOrWhiteSpace(turnoutOverview.ReferenceTimeLabel)
                    ? string.Empty
                    : $" / {turnoutOverview.ReferenceTimeLabel}";
                DistrictOverviewStatusText = DistrictOverviewCards.Count == 0
                    ? "수신된 지역별 투표율이 없습니다."
                    : $"총 {DistrictOverviewCards.Count}개 지역 / 전국 {turnoutOverview.NationalTurnoutRate:0.0}%{referenceTimeStatus} / 마지막 갱신 {DateTimeOffset.Now:HH:mm:ss}";
                return;
            }

            var overviewItems = await _apiClient.GetCountingOverviewAsync(ElectionType, visibleOptions, CancellationToken.None);
            if (revision != _districtOverviewRefreshRevision || !IsDistrictOverviewMode)
            {
                return;
            }

            ReplaceDistrictOverviewCards(
                overviewItems.Select(item => new DistrictOverviewCardViewModel
                {
                    DistrictViewName = item.DisplayName,
                    DistrictCode = item.DistrictCode,
                    RegionName = item.DisplayName,
                    CountedRateDisplay = $"{item.CountedRate:0.0}%",
                    DetailText = $"개표 {item.CountedVotes:N0} / 남은표 {item.UncountedVotes:N0}",
                    JudgementBadgeText = item.JudgementBadgeText,
                    JudgementDetailText = item.JudgementDetailText
                }));

            DistrictOverviewStatusText = DistrictOverviewCards.Count == 0
                ? "수신된 지역별 개표율이 없습니다."
                : $"총 {DistrictOverviewCards.Count}개 선거구 / 마지막 갱신 {DateTimeOffset.Now:HH:mm:ss}";
        }
        catch (Exception ex)
        {
            if (revision != _districtOverviewRefreshRevision)
            {
                return;
            }

            ReplaceDistrictOverviewCards([]);
            DistrictOverviewStatusText = $"{DistrictOverviewTitle} 수신 실패: {ex.Message}";
            _logService.Warning($"{DistrictOverviewTitle} 수신 실패: {ex.Message}");
        }
        finally
        {
            if (revision == _districtOverviewRefreshRevision)
            {
                _isRefreshingDistrictOverview = false;
            }
        }
    }

    private static string BuildTurnoutOverviewDetailText(
        SbsElectionApiClient.TurnoutOverviewItem item,
        string referenceTimeLabel)
    {
        var voteSummary = $"투표 {item.TurnoutVotes:N0} / 미투표 {Math.Max(0, item.TotalExpectedVotes - item.TurnoutVotes):N0}";
        return string.IsNullOrWhiteSpace(referenceTimeLabel)
            ? voteSummary
            : $"{referenceTimeLabel} / {voteSummary}";
    }

    private void ReplaceDistrictOverviewCards(IEnumerable<DistrictOverviewCardViewModel> cards)
    {
        DistrictOverviewCards.Clear();
        foreach (var card in cards)
        {
            DistrictOverviewCards.Add(card);
        }
    }

    private void ApplyCloseRaceThresholds()
    {
        var (closeThreshold, superThreshold) = NormalizeRaceThresholdPair(
            DraftCloseRaceThresholdPercent,
            DraftSuperCloseRaceThresholdPercent);

        CloseRaceThresholdPercent = closeThreshold;
        SuperCloseRaceThresholdPercent = superThreshold;
        _draftCloseRaceThresholdPercent = closeThreshold;
        _draftSuperCloseRaceThresholdPercent = superThreshold;
        OnPropertyChanged(
            nameof(DraftCloseRaceThresholdPercent),
            nameof(DraftSuperCloseRaceThresholdPercent),
            nameof(HasPendingCloseRaceThresholdChange),
            nameof(CloseRaceThresholdSummaryText));
        ApplyCloseRaceThresholdsCommand.NotifyCanExecuteChanged();
        CloseRaceThresholdStatusText = $"접전 기준을 적용했습니다. {CloseRaceThresholdSummaryText}";

        if (CloseRaceTargets.Count > 0)
        {
            _ = RefreshCloseRaceTargetsAsync();
        }
    }

    private void ResetCloseRaceThresholds()
    {
        DraftCloseRaceThresholdPercent = 5.0;
        DraftSuperCloseRaceThresholdPercent = 3.0;
        ApplyCloseRaceThresholds();
    }

    private async Task RefreshCloseRaceTargetsAsync()
    {
        if (IsRefreshingCloseRaceTargets)
        {
            return;
        }

        if (!IsCountingPhase)
        {
            ReplaceCloseRaceTargets([]);
            CloseRaceTargetSummaryText = "접전 대상 확인은 개표 단계에서만 사용할 수 있습니다.";
            return;
        }

        var electionType = ElectionType;
        if (!IsCloseRaceElectionType(electionType))
        {
            ReplaceCloseRaceTargets([]);
            CloseRaceTargetSummaryText = "접전 대상 확인은 광역단체장과 기초단체장 선거에서 사용할 수 있습니다.";
            return;
        }

        var targetOptions = ResolveCloseRaceTargetOptions();
        if (targetOptions.Count == 0)
        {
            ReplaceCloseRaceTargets([]);
            CloseRaceTargetSummaryText = "접전 대상 확인에 사용할 선거구가 없습니다.";
            return;
        }

        IsRefreshingCloseRaceTargets = true;
        CloseRaceTargetSummaryText = $"{electionType} 접전 대상을 불러오는 중입니다.";
        try
        {
            var refreshResults = await _apiClient
                .GetCountingSnapshotsAsync(electionType, targetOptions, CancellationToken.None);
            var targets = refreshResults
                .Select(result => CreateCloseRaceTargetViewModel(electionType, result))
                .Where(target => target is not null)
                .Cast<CloseRaceTargetViewModel>()
                .OrderBy(target => target.Gap)
                .ThenBy(target => target.RegionName, StringComparer.Ordinal)
                .ThenBy(target => target.DistrictName, StringComparer.Ordinal)
                .ToArray();

            ReplaceCloseRaceTargets(targets);

            var superCount = targets.Count(target => string.Equals(target.Level, "초접전", StringComparison.Ordinal));
            var closeCount = targets.Length - superCount;
            CloseRaceTargetSummaryText = targets.Length == 0
                ? $"{electionType} / {targetOptions.Count}개 선거구 확인 완료. 현재 기준에 맞는 접전 대상이 없습니다."
                : $"{electionType} / {targetOptions.Count}개 선거구 확인 완료. 초접전 {superCount}곳, 접전 {closeCount}곳.";
            CloseRaceThresholdStatusText = $"{DateTimeOffset.Now:HH:mm:ss} 기준으로 접전 대상을 확인했습니다.";
        }
        catch (Exception ex)
        {
            ReplaceCloseRaceTargets([]);
            CloseRaceTargetSummaryText = $"접전 대상 확인 실패: {ex.Message}";
            _logService.Warning($"접전 대상 확인 실패: {ex.Message}");
        }
        finally
        {
            IsRefreshingCloseRaceTargets = false;
        }
    }

    private IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> ResolveCloseRaceTargetOptions()
    {
        var sourceOptions = _districtSelectionSource.Count > 0
            ? _districtSelectionSource
            : DefaultDistrictOptions;

        if (IsStationRegionOverviewMode)
        {
            return GetConfiguredDistrictSelectionOptions(sourceOptions);
        }

        if (IsDistrictOverviewSelection(SelectedDistrictViewName))
        {
            return GetVisibleDistrictSelectionOptions(sourceOptions);
        }

        return _districtOptionMap.TryGetValue(SelectedDistrictViewName, out var selectedOption)
            ? [selectedOption]
            : GetVisibleDistrictSelectionOptions(sourceOptions);
    }

    private CloseRaceTargetViewModel? CreateCloseRaceTargetViewModel(
        string electionType,
        SbsElectionApiClient.SbsElectionRefreshResult result)
    {
        if (!TryResolveVoteRateGap(result.Candidates ?? Array.Empty<CandidateEntry>(), out var first, out var second, out var gap))
        {
            return null;
        }

        var level = ResolveCloseRaceLevel(gap);
        if (level is null)
        {
            return null;
        }

        return new CloseRaceTargetViewModel
        {
            ElectionType = electionType,
            DistrictName = FirstNonWhiteSpace(result.ElectionDistrictName, result.DistrictName),
            RegionName = result.RegionName,
            Level = level,
            FirstCandidateText = BuildCloseRaceCandidateText(first),
            SecondCandidateText = BuildCloseRaceCandidateText(second),
            FirstVoteRateDisplay = $"{first.VoteRate:0.0}%",
            SecondVoteRateDisplay = $"{second.VoteRate:0.0}%",
            GapDisplay = $"{gap:0.0}%p",
            Gap = gap
        };
    }

    private void ReplaceCloseRaceTargets(IEnumerable<CloseRaceTargetViewModel> targets)
    {
        CloseRaceTargets.Clear();
        foreach (var target in targets)
        {
            CloseRaceTargets.Add(target);
        }

        OnPropertyChanged(nameof(CloseRaceTargetsVisibility));
    }

    private static string BuildCloseRaceCandidateText(CandidateEntry candidate)
    {
        return string.IsNullOrWhiteSpace(candidate.Party)
            ? candidate.Name
            : $"{candidate.Name}({candidate.Party})";
    }

    private static double NormalizeRaceThreshold(double value)
    {
        if (double.IsNaN(value) || double.IsInfinity(value))
        {
            return 0d;
        }

        return Math.Clamp(Math.Round(value, 1, MidpointRounding.AwayFromZero), 0d, 100d);
    }

    private static (double CloseThreshold, double SuperThreshold) NormalizeRaceThresholdPair(
        double closeRaceThresholdPercent,
        double superCloseRaceThresholdPercent)
    {
        var closeThreshold = NormalizeRaceThreshold(closeRaceThresholdPercent);
        var superThreshold = NormalizeRaceThreshold(superCloseRaceThresholdPercent);
        if (superThreshold > closeThreshold)
        {
            superThreshold = closeThreshold;
        }

        return (closeThreshold, superThreshold);
    }

    private void NotifyCloseRaceDraftChanged()
    {
        OnPropertyChanged(nameof(HasPendingCloseRaceThresholdChange));
        ApplyCloseRaceThresholdsCommand.NotifyCanExecuteChanged();
    }

    private static bool IsCloseRaceElectionType(string? electionType)
    {
        return string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
               string.Equals(electionType, "기초단체장", StringComparison.Ordinal);
    }

    private static bool IsCareerPromiseElectionType(string? electionType)
    {
        return string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
               string.Equals(electionType, "기초단체장", StringComparison.Ordinal);
    }

    private static bool TryResolveVoteRateGap(
        IReadOnlyList<CandidateEntry> candidates,
        out CandidateEntry first,
        out CandidateEntry second,
        out double gap)
    {
        var orderedCandidates = candidates
            .Where(candidate => !string.IsNullOrWhiteSpace(candidate.Name))
            .OrderByDescending(candidate => candidate.VoteRate)
            .ThenByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .Take(2)
            .ToArray();

        if (orderedCandidates.Length < 2 ||
            orderedCandidates.All(candidate => candidate.VoteRate <= 0d))
        {
            first = new CandidateEntry();
            second = new CandidateEntry();
            gap = 0d;
            return false;
        }

        first = orderedCandidates[0];
        second = orderedCandidates[1];
        gap = Math.Round(Math.Abs(first.VoteRate - second.VoteRate), 1, MidpointRounding.AwayFromZero);
        return true;
    }

    private string? ResolveCloseRaceLevel(double gap)
    {
        if (gap <= SuperCloseRaceThresholdPercent)
        {
            return "초접전";
        }

        if (gap <= CloseRaceThresholdPercent)
        {
            return "접전";
        }

        return null;
    }

    private void SyncSelectedDistrictViewName(string districtName)
    {
        if (IsDistrictOverviewMode)
        {
            return;
        }

        var normalizedName = ResolveDistrictViewSelectionName(districtName, DistrictCode);
        if (string.Equals(_selectedDistrictViewName, normalizedName, StringComparison.Ordinal))
        {
            return;
        }

        _selectedDistrictViewName = normalizedName;
        OnPropertyChanged(nameof(SelectedDistrictViewName));
    }

    private void UpdateDistrictOverviewStatus()
    {
        if (!IsDistrictOverviewMode)
        {
            DistrictOverviewStatusText = $"전체보기나 선택권역보기를 선택하면 지역별 {GetDistrictOverviewMetricLabel()}을 확인할 수 있습니다.";
            return;
        }

        if (!IsCurrentApiSelectionSupported)
        {
            DistrictOverviewStatusText = $"{ElectionType} {BroadcastPhaseLabel} 단계는 {GetDistrictOverviewModeLabel()}를 지원하지 않습니다.";
            return;
        }

        if (_isRefreshingDistrictOverview)
        {
            return;
        }

        if (DistrictOverviewCards.Count == 0)
        {
            DistrictOverviewStatusText = GetDistrictOverviewEmptyStateText();
        }
    }

    private string GetDistrictOverviewEmptyStateText()
    {
        if (IsStationRegionOverviewMode)
        {
            return _configuredRegions.Count == 0
                ? $"설정에서 권역을 선택하면 선택권역보기로 해당 지역 {GetDistrictOverviewMetricLabel()}을 볼 수 있습니다."
                : "선택한 권역에 표시할 선거구가 없습니다.";
        }

        return $"전체보기에서 지역별 {GetDistrictOverviewMetricLabel()}을 불러올 수 있습니다.";
    }

    private string GetDistrictOverviewModeLabel()
    {
        return IsStationRegionOverviewMode ? StationRegionOverviewOptionLabel : DistrictOverviewOptionLabel;
    }

    private string GetDistrictOverviewMetricLabel()
    {
        return IsPreElectionPhase ? "투표율" : "개표율";
    }

    private static IEnumerable<SelectionOption<string>> CreateDistrictViewSelectionOptions(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options)
    {
        yield return new SelectionOption<string>(DistrictOverviewOptionValue, DistrictOverviewOptionLabel);
        yield return new SelectionOption<string>(StationRegionOverviewOptionValue, StationRegionOverviewOptionLabel);

        foreach (var option in options)
        {
            yield return new SelectionOption<string>(option.DisplayName, option.DisplayName);
        }
    }

    private async Task<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> GetScheduleDistrictOptionsAsync(
        string electionType,
        FormatTemplateDefinition? template,
        CancellationToken cancellationToken)
    {
        if (UsesHistoricalScheduleOptions(template))
        {
            var historicalOptions = GetHistoricalScheduleDistrictOptions(electionType);
            if (historicalOptions.Count > 0)
            {
                return historicalOptions;
            }
        }

        return await GetScheduleDistrictOptionsAsync(electionType, cancellationToken).ConfigureAwait(false);
    }

    private IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> GetHistoricalScheduleDistrictOptions(string electionType)
    {
        var records = _preElectionHistoryService.GetSelectionRecords(electionType);
        if (records.Count == 0)
        {
            return Array.Empty<SbsElectionApiClient.DistrictSelectionOption>();
        }

        return records
            .Where(record => !string.IsNullOrWhiteSpace(ResolveHistoricalScheduleOptionDisplayName(record)))
            .OrderBy(record => ResolveDefaultRegionOrder(record.RegionName))
            .ThenBy(record => ResolveHistoricalScheduleOptionDisplayName(record), StringComparer.Ordinal)
            .Select(CreateHistoricalScheduleDistrictOption)
            .ToArray();
    }

    private static SbsElectionApiClient.DistrictSelectionOption CreateHistoricalScheduleDistrictOption(
        PreElectionHistoryRecord record)
    {
        var regionName = string.IsNullOrWhiteSpace(record.RegionName)
            ? record.DisplayName
            : record.RegionName;
        var districtName = string.IsNullOrWhiteSpace(record.DistrictName)
            ? ResolveHistoricalScheduleOptionDisplayName(record)
            : record.DistrictName;
        var parentRegionCode = SbsElectionApiClient.ResolveBasicApiSidoCode(regionName);
        var canonicalElectionType = PreElectionHistoryService.NormalizeElectionType(record.ElectionType);
        var districtCode = string.Equals(canonicalElectionType, "기초단체장", StringComparison.Ordinal)
            ? record.Key
            : parentRegionCode;

        return new SbsElectionApiClient.DistrictSelectionOption(
            DisplayName: ResolveHistoricalScheduleOptionDisplayName(record),
            DistrictCode: districtCode,
            RegionName: regionName,
            DistrictName: districtName,
            ParentRegionCode: parentRegionCode);
    }

    private static string ResolveHistoricalScheduleOptionDisplayName(PreElectionHistoryRecord record)
    {
        if (!string.IsNullOrWhiteSpace(record.DisplayName))
        {
            return record.DisplayName;
        }

        if (!string.IsNullOrWhiteSpace(record.DistrictName))
        {
            return record.DistrictName;
        }

        return record.RegionName ?? string.Empty;
    }

    private async Task<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> GetScheduleDistrictOptionsAsync(
        string electionType,
        CancellationToken cancellationToken)
    {
        if (string.Equals(electionType, ElectionType, StringComparison.Ordinal) && _districtSelectionSource.Count > 0)
        {
            return _districtSelectionSource;
        }

        if (SupportsApiDistrictOptions(electionType))
        {
            try
            {
                var options = await _apiClient
                    .GetDistrictOptionsAsync(electionType, ResolveApiDistrictRegionScope(electionType), cancellationToken)
                    .ConfigureAwait(false);
                if (options.Count > 0)
                {
                    return options;
                }

                if (IsBasicCouncilElectionType(electionType))
                {
                    return Array.Empty<SbsElectionApiClient.DistrictSelectionOption>();
                }
            }
            catch (Exception ex)
            {
                _logService.Warning($"{electionType} 스케줄 선거구 목록 수신 실패: {ex.Message}");
                if (IsBasicCouncilElectionType(electionType))
                {
                    return Array.Empty<SbsElectionApiClient.DistrictSelectionOption>();
                }
            }
        }

        return _districtSelectionSource.Count > 0 ? _districtSelectionSource : DefaultDistrictOptions;
    }

    private string ResolveScheduleElectionType(FormatTemplateDefinition? template, string? preferredElectionType = null)
    {
        if (template is not null && IsBottomTurnoutDistrictTemplate(template))
        {
            return "기초단체장";
        }

        if (template is not null && IsBottomTurnoutSidoTemplate(template))
        {
            return "광역단체장";
        }

        if (template is not null && IsTopTurnoutDistrictBoardTemplate(template))
        {
            return "기초단체장";
        }

        if (IsNormalPreElectionTurnoutDistrictBoardTemplate(template))
        {
            return "기초단체장";
        }

        if (template is not null && ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name))
        {
            return "광역단체장";
        }

        if (template is not null && IsTurnoutTemplate(template))
        {
            return ResolvePreElectionTurnoutElectionType(preferredElectionType);
        }

        return ResolveScheduleElectionType(template?.Name, preferredElectionType);
    }

    private string ResolveScheduleElectionType(string? formatName, string? preferredElectionType = null)
    {
        var resolvedFormatName = formatName ?? string.Empty;
        if (TryResolveHistoricalWinnerElectionType(resolvedFormatName, out var historicalWinnerElectionType))
        {
            return historicalWinnerElectionType;
        }

        if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(resolvedFormatName))
        {
            return "광역단체장";
        }

        if (IsBottomTurnoutDistrictTemplateName(resolvedFormatName))
        {
            return "기초단체장";
        }

        if (IsBottomTurnoutSidoTemplateName(resolvedFormatName))
        {
            return "광역단체장";
        }

        if (resolvedFormatName.Contains("교육감", StringComparison.Ordinal))
        {
            return "교육감";
        }

        if (resolvedFormatName.Contains("기초의원", StringComparison.Ordinal))
        {
            return "기초의원";
        }

        if (resolvedFormatName.Contains("기초단체장", StringComparison.Ordinal))
        {
            return "기초단체장";
        }

        if (resolvedFormatName.Contains("광역의원", StringComparison.Ordinal))
        {
            return "광역의원";
        }

        if (resolvedFormatName.Contains("보궐선거", StringComparison.Ordinal))
        {
            return "국회의원";
        }

        if (resolvedFormatName.Contains("광역단체장", StringComparison.Ordinal))
        {
            return "광역단체장";
        }

        if (BroadcastPhase == BroadcastPhase.PreElection)
        {
            return ResolvePreElectionTurnoutElectionType(preferredElectionType);
        }

        return string.IsNullOrWhiteSpace(preferredElectionType)
            ? ElectionType
            : preferredElectionType;
    }

    private string ResolvePreElectionTurnoutElectionType(string? preferredElectionType = null)
    {
        return SupportsPreElectionTurnout(electionType: preferredElectionType)
            ? preferredElectionType!
            : SupportsPreElectionTurnout(ElectionType)
                ? ElectionType
                : "광역단체장";
    }

    private static bool TryResolveHistoricalWinnerElectionType(string formatName, out string electionType)
    {
        var sourceName = System.IO.Path.GetFileNameWithoutExtension(
            (formatName ?? string.Empty)
                .Replace('/', System.IO.Path.DirectorySeparatorChar)
                .Replace('\\', System.IO.Path.DirectorySeparatorChar));

        if (!sourceName.StartsWith("사전_역대당선", StringComparison.Ordinal))
        {
            electionType = string.Empty;
            return false;
        }

        if (sourceName.Contains("교육감", StringComparison.Ordinal))
        {
            electionType = "교육감";
            return true;
        }

        if (sourceName.Contains("기초단체장", StringComparison.Ordinal))
        {
            electionType = "기초단체장";
            return true;
        }

        electionType = "광역단체장";
        return true;
    }

    private IReadOnlyList<ScheduleRegionTarget> ResolveStationRegionTargets(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string electionType,
        BroadcastStationProfile station)
    {
        var configuredRegions = NormalizeConfiguredRegions(station.RegionFilters);
        if (configuredRegions.Count == 0)
        {
            return options.Select(option => CreateScheduleRegionTarget(option, electionType)).ToArray();
        }

        return options
            .Where(option => MatchesConfiguredRegion(option.RegionName, configuredRegions))
            .Select(option => CreateScheduleRegionTarget(option, electionType))
            .ToArray();
    }

    private IReadOnlyList<ScheduleRegionTarget> ResolveSingleRegionTarget(
        ChannelScheduleItem item,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string electionType)
    {
        var matchedOption = options.FirstOrDefault(option =>
            !string.IsNullOrWhiteSpace(item.RegionCode) &&
            string.Equals(option.DistrictCode, item.RegionCode, StringComparison.OrdinalIgnoreCase));

        matchedOption ??= options.FirstOrDefault(option =>
            MatchesScheduleRegionOptionLabel(option, item.RegionLabel));

        if (matchedOption is null)
        {
            return Array.Empty<ScheduleRegionTarget>();
        }

        return [CreateScheduleRegionTarget(matchedOption, electionType)];
    }

    private static IReadOnlyList<ScheduleRegionTarget> ResolveRegionGroupTargets(
        ChannelScheduleItem item,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string electionType)
    {
        var normalizedRegionLabel = NormalizeConfiguredRegion(item.RegionLabel);
        return options
            .Where(option => MatchesScheduleRegionGroup(option, item.RegionCode, normalizedRegionLabel))
            .Select(option => CreateScheduleRegionTarget(option, electionType))
            .ToArray();
    }

    private static IReadOnlyList<ScheduleRegionTarget> ResolveCombinedHistoricalRegionTargets(
        ChannelScheduleItem item,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> options,
        string electionType)
    {
        if (!IsGwangjuJeonnamCombinedLabel(item.RegionLabel))
        {
            return Array.Empty<ScheduleRegionTarget>();
        }

        return options
            .Where(option => GetNormalizedRegionKeys(option.RegionName)
                .Any(key => string.Equals(key, "광주", StringComparison.OrdinalIgnoreCase) ||
                            string.Equals(key, "전남", StringComparison.OrdinalIgnoreCase)))
            .OrderBy(option => ResolveDefaultRegionOrder(option.RegionName))
            .Select(option => CreateScheduleRegionTarget(option, electionType))
            .ToArray();
    }

    private static bool UsesHistoricalScheduleOptions(FormatTemplateDefinition? template)
    {
        return ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template?.Name) ||
               ScheduleTemplatePolicy.IsHistoricalWinnerFormat(template?.Name);
    }

    private static bool IsGwangjuJeonnamCombinedLabel(string? value)
    {
        var normalized = NormalizeConfiguredRegion(value);
        return string.Equals(normalized, "전남광주", StringComparison.OrdinalIgnoreCase) ||
               string.Equals(normalized, "광주전남", StringComparison.OrdinalIgnoreCase);
    }

    private static bool MatchesScheduleRegionGroup(
        SbsElectionApiClient.DistrictSelectionOption option,
        string regionCode,
        string normalizedRegionLabel)
    {
        if (!string.IsNullOrWhiteSpace(regionCode) &&
            (string.Equals(option.ParentRegionCode, regionCode, StringComparison.OrdinalIgnoreCase) ||
             string.Equals(SbsElectionApiClient.ResolveBasicApiSidoCode(option.RegionName), regionCode, StringComparison.OrdinalIgnoreCase)))
        {
            return true;
        }

        return !string.IsNullOrWhiteSpace(normalizedRegionLabel) &&
               GetNormalizedRegionKeys(option.RegionName).Any(key =>
                   string.Equals(key, normalizedRegionLabel, StringComparison.OrdinalIgnoreCase));
    }

    private static bool MatchesScheduleRegionOptionLabel(
        SbsElectionApiClient.DistrictSelectionOption option,
        string regionLabel)
    {
        if (string.IsNullOrWhiteSpace(regionLabel))
        {
            return false;
        }

        return string.Equals(option.DisplayName, regionLabel, StringComparison.Ordinal) ||
               string.Equals(option.RegionName, regionLabel, StringComparison.Ordinal) ||
               regionLabel.EndsWith($" / {option.DisplayName}", StringComparison.Ordinal) ||
               regionLabel.EndsWith($" / {option.RegionName}", StringComparison.Ordinal);
    }

    private ElectionDataSnapshot CreateSnapshotFromRefreshResult(
        string electionType,
        SbsElectionApiClient.SbsElectionRefreshResult refreshResult,
        BroadcastPhase broadcastPhase)
    {
        var districtName = string.IsNullOrWhiteSpace(refreshResult.DistrictName)
            ? refreshResult.ElectionDistrictName
            : refreshResult.DistrictName;
        var regionName = string.IsNullOrWhiteSpace(refreshResult.RegionName)
            ? districtName
            : refreshResult.RegionName;
        var electionDistrictName = string.IsNullOrWhiteSpace(refreshResult.ElectionDistrictName)
            ? districtName
            : refreshResult.ElectionDistrictName;
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = broadcastPhase,
            ElectionType = electionType,
            DistrictName = districtName ?? string.Empty,
            DistrictCode = refreshResult.DistrictCode ?? string.Empty,
            RegionName = regionName ?? string.Empty,
            ElectionDistrictName = electionDistrictName ?? string.Empty,
            Candidates = BuildSnapshotCandidatesWithCareerPromises(
                refreshResult.Candidates ?? Array.Empty<CandidateEntry>(),
                electionType,
                refreshResult.DistrictCode ?? string.Empty,
                districtName ?? string.Empty),
            TotalExpectedVotes = refreshResult.TotalExpectedVotes,
            TurnoutVotes = refreshResult.TurnoutVotes,
            CountedVotesFromApi = refreshResult.CountedVotes,
            RemainingVotesFromApi = refreshResult.RemainingVotes,
            CountedRateFromApi = refreshResult.CountedRate,
            ReceivedAt = refreshResult.ReceivedAt == default ? DateTimeOffset.Now : refreshResult.ReceivedAt,
            ReferenceTimeLabel = refreshResult.ReferenceTimeLabel,
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private async Task<ElectionDataSnapshot> CreateCouncilWinnerScheduleSnapshotAsync(
        string electionType,
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        if (!SupportsApiDistrictOptions(electionType))
        {
            throw new InvalidOperationException($"{electionType} 당선자 목록은 현재 SBS API 연동 대상이 아닙니다.");
        }

        var selectedTargets = ResolveSelectedScheduleTargets(regionTargets);
        if (selectedTargets.Length == 0)
        {
            throw new InvalidOperationException("당선자 목록 대상 선거구가 없습니다.");
        }

        var refreshResults = await GetCountingSnapshotsForScheduleTargetsAsync(
                electionType,
                selectedTargets,
                cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("당선자 목록용 개표 데이터가 없습니다.");
        }

        var winners = refreshResults
            .SelectMany(result => (result.Candidates ?? Array.Empty<CandidateEntry>())
                .Select(candidate => PrepareCouncilWinnerCandidate(candidate, result)))
            .Where(candidate => IsCouncilWinnerJudgement(candidate.EffectiveJudgement))
            .ToArray();
        if (winners.Length == 0)
        {
            throw new InvalidOperationException("당선 판정을 받은 후보가 없습니다.");
        }

        var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
        var turnoutVotes = refreshResults.Sum(result => Math.Max(0, result.TurnoutVotes));
        var countedVotes = refreshResults.Sum(result => Math.Max(0, result.CountedVotes ?? 0));
        var remainingVotes = refreshResults.Sum(result => Math.Max(0, result.RemainingVotes ?? 0));
        var countedRate = refreshResults.All(result => result.CountingClosed)
            ? 100d
            : ResolveAggregateCountedRate(totalVotes, countedVotes, refreshResults);
        var regionName = ResolveCouncilSeatAggregateRegionLabel(station, selectedTargets);
        var districtName = selectedTargets.Length == 1
            ? selectedTargets[0].DisplayName
            : regionName;
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = selectedTargets.Length == 1 ? selectedTargets[0].DistrictCode : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedTargets.Length == 1 ? selectedTargets[0].DistrictName : regionName,
            Candidates = winners,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private async Task<ElectionDataSnapshot> CreateCouncilSeatScheduleSnapshotAsync(
        string electionType,
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        if (!SupportsApiDistrictOptions(electionType))
        {
            throw new InvalidOperationException($"{electionType} 의석 집계는 현재 SBS API 연동 대상이 아닙니다.");
        }

        var selectedTargets = ResolveSelectedScheduleTargets(regionTargets);
        if (selectedTargets.Length == 0)
        {
            throw new InvalidOperationException("의석 집계 대상 선거구가 없습니다.");
        }

        var refreshResults = await GetCountingSnapshotsForScheduleTargetsAsync(
                electionType,
                selectedTargets,
                cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("의석 집계용 개표 데이터가 없습니다.");
        }

        var seatCandidates = BuildCouncilSeatSummaryCandidates(electionType, refreshResults);
        if (seatCandidates.Length == 0)
        {
            throw new InvalidOperationException("의석으로 집계할 현재 득표 기준 후보가 없습니다.");
        }

        var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
        var turnoutVotes = refreshResults.Sum(result => Math.Max(0, result.TurnoutVotes));
        var countedVotes = refreshResults.Sum(result => Math.Max(0, result.CountedVotes ?? 0));
        var remainingVotes = refreshResults.Sum(result => Math.Max(0, result.RemainingVotes ?? 0));
        var countedRate = ResolveCouncilSeatAggregateCountedRate(refreshResults);
        var regionName = ResolveCouncilSeatAggregateRegionLabel(station, selectedTargets);
        var districtName = selectedTargets.Length == 1
            ? selectedTargets[0].DisplayName
            : regionName;
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = selectedTargets.Length == 1 ? selectedTargets[0].DistrictCode : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedTargets.Length == 1 ? selectedTargets[0].DistrictName : regionName,
            Candidates = seatCandidates,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private ScheduleRegionTarget[] ResolveSelectedScheduleTargets(IReadOnlyList<ScheduleRegionTarget> regionTargets)
    {
        return regionTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .GroupBy(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();
    }

    private Task<IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult>> GetCountingSnapshotsForScheduleTargetsAsync(
        string electionType,
        IReadOnlyList<ScheduleRegionTarget> selectedTargets,
        CancellationToken cancellationToken)
    {
        var districtOptions = selectedTargets
            .Select(target => new SbsElectionApiClient.DistrictSelectionOption(
                target.DisplayName,
                target.DistrictCode,
                target.RegionName,
                target.DistrictName,
                ResolveScheduleTargetParentRegionCode(target)))
            .ToArray();

        return _apiClient.GetCountingSnapshotsAsync(electionType, districtOptions, cancellationToken);
    }

    private static double ResolveAggregateCountedRate(
        int totalVotes,
        int countedVotes,
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        return totalVotes <= 0
            ? refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max()
            : Math.Round(countedVotes * 100d / totalVotes, 1, MidpointRounding.AwayFromZero);
    }

    private static double ResolveCouncilSeatAggregateCountedRate(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        return refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max();
    }

    private static CandidateEntry PrepareCouncilWinnerCandidate(
        CandidateEntry candidate,
        SbsElectionApiClient.SbsElectionRefreshResult result)
    {
        var winner = candidate.Clone();
        winner.BroadcastDistrictName = FirstNonWhiteSpace(candidate.BroadcastDistrictName, result.DistrictName, result.ElectionDistrictName);
        winner.BroadcastRegionName = FirstNonWhiteSpace(candidate.BroadcastRegionName, result.RegionName);
        winner.BroadcastElectionDistrictName = FirstNonWhiteSpace(candidate.BroadcastElectionDistrictName, result.ElectionDistrictName, winner.BroadcastDistrictName);
        winner.BroadcastDistrictCode = FirstNonWhiteSpace(candidate.BroadcastDistrictCode, result.DistrictCode);
        winner.BroadcastCountedRate = result.CountingClosed ? 100d : candidate.BroadcastCountedRate ?? result.CountedRate;
        winner.BroadcastSeatCount = winner.BroadcastSeatCount > 0 ? winner.BroadcastSeatCount : result.SeatCount;
        winner.BroadcastCountingClosed = winner.BroadcastCountingClosed || result.CountingClosed;

        if (winner.EffectiveJudgement == CandidateJudgement.None &&
            result.CountingClosed &&
            winner.IsWithinBroadcastSeatCount)
        {
            winner.AutomaticJudgement = CandidateJudgement.ElectedAfterCountComplete;
        }

        return winner;
    }

    private static bool IsCouncilWinnerJudgement(CandidateJudgement judgement)
    {
        return judgement is CandidateJudgement.Elected or
            CandidateJudgement.ElectedInProgress or
            CandidateJudgement.UnopposedElected or
            CandidateJudgement.ElectedAfterCountComplete;
    }

    private static int ResolveCouncilWinnerSortPriority(CandidateJudgement judgement)
    {
        return judgement switch
        {
            CandidateJudgement.UnopposedElected => 0,
            CandidateJudgement.ElectedAfterCountComplete => 1,
            CandidateJudgement.Elected => 2,
            CandidateJudgement.ElectedInProgress => 3,
            _ => int.MaxValue
        };
    }

    private async Task<ElectionDataSnapshot> CreateCurrentLeaderScheduleSnapshotAsync(
        string electionType,
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        if (!SupportsApiDistrictOptions(electionType))
        {
            throw new InvalidOperationException($"{electionType} 이시각 1위는 현재 SBS API 연동 대상이 아닙니다.");
        }

        var selectedTargets = regionTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .GroupBy(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();
        if (selectedTargets.Length == 0)
        {
            throw new InvalidOperationException("이시각 1위 대상 선거구가 없습니다.");
        }

        var districtOptions = selectedTargets
            .Select(target => new SbsElectionApiClient.DistrictSelectionOption(
                target.DisplayName,
                target.DistrictCode,
                target.RegionName,
                target.DistrictName,
                ResolveScheduleTargetParentRegionCode(target)))
            .ToArray();
        var refreshResults = await _apiClient
            .GetCountingSnapshotsAsync(electionType, districtOptions, cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("이시각 1위 개표 데이터가 없습니다.");
        }

        var targetMap = selectedTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .ToDictionary(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase);
        var leaders = refreshResults
            .Select(result =>
            {
                targetMap.TryGetValue(result.DistrictCode, out var target);
                return CreateCurrentLeaderCandidate(result, target);
            })
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
        if (leaders.Length == 0)
        {
            throw new InvalidOperationException("이시각 1위로 송출할 후보 데이터가 없습니다.");
        }

        var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
        var turnoutVotes = refreshResults.Sum(result => Math.Max(0, result.TurnoutVotes));
        var countedVotes = refreshResults.Sum(result => Math.Max(0, result.CountedVotes ?? 0));
        var remainingVotes = refreshResults.Sum(result => Math.Max(0, result.RemainingVotes ?? 0));
        var countedRate = totalVotes <= 0
            ? refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max()
            : Math.Round(countedVotes * 100d / totalVotes, 1, MidpointRounding.AwayFromZero);
        var regionName = ResolveCouncilSeatAggregateRegionLabel(station, selectedTargets);
        var districtName = selectedTargets.Length == 1
            ? selectedTargets[0].DisplayName
            : regionName;
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = selectedTargets.Length == 1 ? selectedTargets[0].DistrictCode : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedTargets.Length == 1 ? selectedTargets[0].DistrictName : regionName,
            Candidates = leaders,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private async Task<ElectionDataSnapshot> CreateNormalPanseMapScheduleSnapshotAsync(
        string electionType,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        if (!SupportsApiDistrictOptions(electionType))
        {
            throw new InvalidOperationException($"{electionType} 전국 판세 지도는 현재 SBS API 연동 대상이 아닙니다.");
        }

        var selectedTargets = regionTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .GroupBy(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();
        if (selectedTargets.Length == 0)
        {
            throw new InvalidOperationException("전국 판세 지도 대상 선거구가 없습니다.");
        }

        var districtOptions = selectedTargets
            .Select(target => new SbsElectionApiClient.DistrictSelectionOption(
                target.DisplayName,
                target.DistrictCode,
                target.RegionName,
                target.DistrictName,
                ResolveScheduleTargetParentRegionCode(target)))
            .ToArray();
        var refreshResults = await _apiClient
            .GetCountingSnapshotsAsync(electionType, districtOptions, cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("전국 판세 지도용 개표 데이터가 없습니다.");
        }

        var targetMap = selectedTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .ToDictionary(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase);
        var leaders = refreshResults
            .Select(result =>
            {
                targetMap.TryGetValue(result.DistrictCode, out var target);
                return CreateCurrentLeaderCandidate(result, target);
            })
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
        if (leaders.Length == 0)
        {
            throw new InvalidOperationException("전국 판세 지도에 반영할 1위 후보 데이터가 없습니다.");
        }

        var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
        var turnoutVotes = refreshResults.Sum(result => Math.Max(0, result.TurnoutVotes));
        var countedVotes = refreshResults.Sum(result => Math.Max(0, result.CountedVotes ?? 0));
        var remainingVotes = refreshResults.Sum(result => Math.Max(0, result.RemainingVotes ?? 0));
        var countedRate = totalVotes <= 0
            ? refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max()
            : Math.Round(countedVotes * 100d / totalVotes, 1, MidpointRounding.AwayFromZero);
        var history = ResolvePreElectionHistoryRecords(electionType, "전국", "전국");

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = "전국",
            DistrictCode = string.Empty,
            RegionName = "전국",
            ElectionDistrictName = "전국",
            Candidates = leaders,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private async Task<ElectionDataSnapshot> CreateTopPanseScheduleSnapshotAsync(
        string electionType,
        FormatTemplateDefinition template,
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        CancellationToken cancellationToken)
    {
        if (!SupportsApiDistrictOptions(electionType))
        {
            throw new InvalidOperationException($"{electionType} 판세 집계는 현재 SBS API 연동 대상이 아닙니다.");
        }

        var selectedTargets = regionTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .GroupBy(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();
        if (selectedTargets.Length == 0)
        {
            throw new InvalidOperationException("판세 집계 대상 선거구가 없습니다.");
        }

        var districtOptions = selectedTargets
            .Select(target => new SbsElectionApiClient.DistrictSelectionOption(
                target.DisplayName,
                target.DistrictCode,
                target.RegionName,
                target.DistrictName,
                ResolveScheduleTargetParentRegionCode(target)))
            .ToArray();
        var refreshResults = await _apiClient
            .GetCountingSnapshotsAsync(electionType, districtOptions, cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("판세 집계용 개표 데이터가 없습니다.");
        }

        var panseCandidates = IsPanseEducationTemplate(template)
            ? BuildEducationPanseSummaryCandidates(refreshResults)
            : BuildPartyPanseSummaryCandidates(electionType, refreshResults);
        if (panseCandidates.Length == 0)
        {
            throw new InvalidOperationException("판세로 집계할 후보 데이터가 없습니다.");
        }

        var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
        var turnoutVotes = refreshResults.Sum(result => Math.Max(0, result.TurnoutVotes));
        var countedVotes = refreshResults.Sum(result => Math.Max(0, result.CountedVotes ?? 0));
        var remainingVotes = refreshResults.Sum(result => Math.Max(0, result.RemainingVotes ?? 0));
        var countedRate = totalVotes <= 0
            ? refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max()
            : Math.Round(countedVotes * 100d / totalVotes, 1, MidpointRounding.AwayFromZero);
        var regionName = ResolveCouncilSeatAggregateRegionLabel(station, selectedTargets);
        var districtName = selectedTargets.Length == 1
            ? selectedTargets[0].DisplayName
            : regionName;
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = selectedTargets.Length == 1 ? selectedTargets[0].DistrictCode : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedTargets.Length == 1 ? selectedTargets[0].DistrictName : regionName,
            Candidates = panseCandidates,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private static CandidateEntry? CreateCurrentLeaderCandidate(
        SbsElectionApiClient.SbsElectionRefreshResult result,
        ScheduleRegionTarget? target)
    {
        var leader = (result.Candidates ?? Array.Empty<CandidateEntry>())
            .OrderByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .FirstOrDefault();
        if (leader is null)
        {
            return null;
        }

        var candidate = leader.Clone();
        candidate.BroadcastDistrictName = FirstNonWhiteSpace(result.DistrictName, target?.DisplayName, result.ElectionDistrictName);
        candidate.BroadcastRegionName = FirstNonWhiteSpace(result.RegionName, target?.RegionName);
        candidate.BroadcastElectionDistrictName = FirstNonWhiteSpace(result.ElectionDistrictName, target?.DistrictName, candidate.BroadcastDistrictName);
        candidate.BroadcastDistrictCode = FirstNonWhiteSpace(result.DistrictCode, target?.DistrictCode);
        candidate.BroadcastCountedRate = result.CountedRate;
        return candidate;
    }

    private static CandidateEntry[] BuildEducationPanseSummaryCandidates(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        var leaders = refreshResults
            .Select(result => CreateCurrentLeaderCandidate(result, null))
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
        var counts = leaders
            .GroupBy(candidate => NormalizePansePartyKey(ResolveEducationPanseParty(candidate)), StringComparer.OrdinalIgnoreCase)
            .ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);

        var rows = new[]
        {
            new PanseSummaryRow("진보", counts.TryGetValue("진보", out var progressiveCount) ? progressiveCount : 0),
            new PanseSummaryRow("보수", counts.TryGetValue("보수", out var conservativeCount) ? conservativeCount : 0),
            new PanseSummaryRow("중도", counts.TryGetValue("중도", out var moderateCount) ? moderateCount : 0)
        };

        var extraCount = counts
            .Where(pair => pair.Key is not ("진보" or "보수" or "중도"))
            .Sum(pair => pair.Value);
        if (extraCount > 0)
        {
            rows[^1] = new PanseSummaryRow("기타", rows[^1].Count + extraCount);
        }

        return CreatePanseSummaryCandidates(rows);
    }

    private static CandidateEntry[] BuildPartyPanseSummaryCandidates(
        string electionType,
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        var leaders = refreshResults
            .Select(result => CreateCurrentLeaderCandidate(result, null))
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
        return BuildPanseSummaryCandidates(leaders, ResolveCouncilSeatParty, countByVoteCount: false);
    }

    private static CandidateEntry[] BuildPanseSummaryCandidates(
        IReadOnlyList<CandidateEntry> candidates,
        Func<CandidateEntry, string> partySelector,
        bool countByVoteCount)
    {
        var counts = candidates
            .GroupBy(candidate => ResolvePartyPanseGroup(partySelector(candidate)), StringComparer.Ordinal)
            .ToDictionary(
                group => group.Key,
                group => group.Sum(candidate => countByVoteCount ? Math.Max(0, candidate.VoteCount) : 1),
                StringComparer.Ordinal);

        var rows = new[]
        {
            new PanseSummaryRow(PanseDemocraticPartyLabel, counts.TryGetValue(PanseDemocraticPartyLabel, out var democraticCount) ? democraticCount : 0),
            new PanseSummaryRow(PansePeoplePowerPartyLabel, counts.TryGetValue(PansePeoplePowerPartyLabel, out var peoplePowerCount) ? peoplePowerCount : 0),
            new PanseSummaryRow(PanseOtherPartyLabel, counts.TryGetValue(PanseOtherPartyLabel, out var otherCount) ? otherCount : 0)
        };

        return CreatePanseSummaryCandidates(rows);
    }

    private static string ResolvePartyPanseGroup(string party)
    {
        var normalized = NormalizePansePartyKey(party);
        if (IsPanseDemocraticPartyKey(normalized))
        {
            return PanseDemocraticPartyLabel;
        }

        if (IsPansePeoplePowerPartyKey(normalized))
        {
            return PansePeoplePowerPartyLabel;
        }

        return PanseOtherPartyLabel;
    }

    private static bool IsPanseDemocraticPartyKey(string normalizedParty)
    {
        return string.Equals(normalizedParty, "더불어민주당", StringComparison.Ordinal) ||
               string.Equals(normalizedParty, "민주당", StringComparison.Ordinal);
    }

    private static bool IsPansePeoplePowerPartyKey(string normalizedParty)
    {
        return string.Equals(normalizedParty, "국민의힘", StringComparison.Ordinal);
    }

    private static CandidateEntry[] CreatePanseSummaryCandidates(IReadOnlyList<PanseSummaryRow> rows)
    {
        return rows
            .Select((row, index) => new CandidateEntry
            {
                CandidateCode = $"{PanseSummaryCandidateCodePrefix}{index + 1:00}",
                BallotNumber = (index + 1).ToString(),
                Name = row.Party,
                Party = row.Party,
                ColorParty = row.Party,
                VoteCount = row.Count,
                VoteRate = row.Count,
                HasImage = false,
                ManualJudgement = CandidateJudgement.None,
                AutomaticJudgement = CandidateJudgement.Elected
            })
            .ToArray();
    }

    private static string ResolveEducationPanseParty(CandidateEntry candidate)
    {
        var party = FirstNonWhiteSpace(candidate.Party, candidate.EffectiveColorParty, "중도");
        var normalized = NormalizePansePartyKey(party);
        if (normalized.Contains("진보", StringComparison.Ordinal))
        {
            return "진보";
        }

        if (normalized.Contains("보수", StringComparison.Ordinal))
        {
            return "보수";
        }

        if (normalized.Contains("중도", StringComparison.Ordinal))
        {
            return "중도";
        }

        return string.IsNullOrWhiteSpace(party) ? "중도" : party.Trim();
    }

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

    private static CandidateEntry[] BuildCouncilSeatSummaryCandidates(
        string electionType,
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        if (IsBasicCouncilElectionType(electionType))
        {
            return BuildCurrentCouncilSeatSummaryCandidates(refreshResults);
        }

        return BuildCouncilSeatSummaryCandidates(
            refreshResults
                .SelectMany(result => result.Candidates ?? Array.Empty<CandidateEntry>())
                .Where(candidate => CountsAsCouncilSeat(candidate.EffectiveJudgement))
                .ToArray());
    }

    private static CandidateEntry[] BuildCurrentCouncilSeatSummaryCandidates(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        var seatCandidates = refreshResults
            .SelectMany(ResolveCurrentCouncilSeatCandidates)
            .ToArray();

        return BuildCouncilSeatSummaryCandidates(seatCandidates);
    }

    private static IEnumerable<CandidateEntry> ResolveCurrentCouncilSeatCandidates(
        SbsElectionApiClient.SbsElectionRefreshResult result)
    {
        var candidates = result.Candidates ?? Array.Empty<CandidateEntry>();
        if (candidates.Count == 0)
        {
            return Array.Empty<CandidateEntry>();
        }

        var seatCount = Math.Max(
            result.SeatCount,
            candidates.Select(candidate => candidate.BroadcastSeatCount).DefaultIfEmpty(0).Max());
        if (seatCount <= 0)
        {
            return Array.Empty<CandidateEntry>();
        }

        var rankedCandidates = candidates
            .Where(candidate => candidate.BroadcastRank > 0 && candidate.BroadcastRank <= seatCount)
            .OrderBy(candidate => candidate.BroadcastRank)
            .ThenByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .ToArray();
        if (rankedCandidates.Length > 0)
        {
            return rankedCandidates;
        }

        return candidates
            .OrderByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .Take(seatCount)
            .ToArray();
    }

    private static CandidateEntry[] BuildCouncilSeatSummaryCandidates(IReadOnlyList<CandidateEntry> candidates)
    {
        return candidates
            .GroupBy(candidate => ResolveCouncilSeatParty(candidate), StringComparer.OrdinalIgnoreCase)
            .Select(group =>
            {
                var first = group.First();
                return new
                {
                    Party = group.Key,
                    ColorParty = ResolveCouncilSeatColorParty(first),
                    SeatCount = group.Count()
                };
            })
            .Where(row => row.SeatCount > 0)
            .OrderByDescending(row => row.SeatCount)
            .ThenBy(row => row.Party, StringComparer.Ordinal)
            .Select((row, index) => new CandidateEntry
            {
                CandidateCode = $"{CouncilSeatDistrictCandidateCodePrefix}{index + 1:00}",
                BallotNumber = (index + 1).ToString(),
                Name = row.Party,
                Party = row.Party,
                ColorParty = row.ColorParty,
                VoteCount = row.SeatCount,
                VoteRate = row.SeatCount,
                HasImage = false,
                ManualJudgement = CandidateJudgement.None,
                AutomaticJudgement = CandidateJudgement.Elected
            })
            .ToArray();
    }

    private static bool CountsAsCouncilSeat(CandidateJudgement judgement)
    {
        return judgement is CandidateJudgement.Leading or
            CandidateJudgement.Confirmed or
            CandidateJudgement.Elected or
            CandidateJudgement.ElectedInProgress or
            CandidateJudgement.UnopposedElected or
            CandidateJudgement.ElectedAfterCountComplete;
    }

    private static string ResolveCouncilSeatParty(CandidateEntry candidate)
    {
        if (!string.IsNullOrWhiteSpace(candidate.Party))
        {
            return candidate.Party.Trim();
        }

        return string.IsNullOrWhiteSpace(candidate.EffectiveColorParty)
            ? "무소속"
            : candidate.EffectiveColorParty.Trim();
    }

    private static string ResolveCouncilSeatColorParty(CandidateEntry candidate)
    {
        return string.IsNullOrWhiteSpace(candidate.EffectiveColorParty)
            ? ResolveCouncilSeatParty(candidate)
            : candidate.EffectiveColorParty.Trim();
    }

    private static string ResolveCouncilSeatAggregateRegionLabel(
        BroadcastStationProfile station,
        IReadOnlyList<ScheduleRegionTarget> selectedTargets)
    {
        var regionNames = selectedTargets
            .Select(target => target.RegionName)
            .Where(regionName => !string.IsNullOrWhiteSpace(regionName))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (regionNames.Length == 1)
        {
            return regionNames[0];
        }

        return string.IsNullOrWhiteSpace(station.Name) ? "전체" : station.Name;
    }

    private async Task<ElectionDataSnapshot> CreateTurnoutScheduleSnapshotAsync(
        string electionType,
        IReadOnlyList<ScheduleRegionTarget> regionTargets,
        bool includeNationalSlot,
        bool includeRegionalBoardSlots,
        bool includeEmptyRegionalBoardSlots,
        int maxRegionalSlots,
        CancellationToken cancellationToken)
    {
        var allOptions = await GetScheduleDistrictOptionsAsync(electionType, cancellationToken).ConfigureAwait(false);
        var turnoutOverview = await _apiClient.GetTurnoutOverviewAsync(electionType, allOptions, cancellationToken).ConfigureAwait(false);
        var selectedTargets = regionTargets
            .Where(target => !string.IsNullOrWhiteSpace(target.DistrictCode))
            .GroupBy(target => target.DistrictCode, StringComparer.OrdinalIgnoreCase)
            .Select(group => group.First())
            .ToArray();

        var primaryTarget = selectedTargets.FirstOrDefault();
        var primaryItem = FindTurnoutOverviewItem(turnoutOverview.Items, primaryTarget);
        if (!includeRegionalBoardSlots && !HasPositiveTurnoutOverview(primaryItem))
        {
            throw new InvalidOperationException("선택한 지역의 투표율이 0이라 송출할 수 없습니다.");
        }

        var regionName = primaryItem?.RegionName ?? primaryTarget?.RegionName ?? string.Empty;
        var districtName = primaryItem?.DisplayName ?? primaryTarget?.DisplayName ?? regionName;
        var electionDistrictName = ResolveTurnoutElectionDistrictName(
            electionType,
            primaryItem,
            primaryTarget,
            regionName,
            districtName);
        var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);
        var turnoutBoardSlots = new List<TurnoutBoardSlotEntry>();

        if (includeNationalSlot)
        {
            turnoutBoardSlots.Add(new TurnoutBoardSlotEntry(1, "전국", turnoutOverview.NationalTurnoutRate, true));
        }

        if (includeRegionalBoardSlots)
        {
            var nextSlot = turnoutBoardSlots.Count + 1;
            foreach (var target in selectedTargets.Take(Math.Max(maxRegionalSlots, 0)))
            {
                var slotItem = FindTurnoutOverviewItem(turnoutOverview.Items, target);
                var hasTurnoutData = HasPositiveTurnoutOverview(slotItem);
                if (!includeEmptyRegionalBoardSlots && !hasTurnoutData)
                {
                    continue;
                }

                turnoutBoardSlots.Add(new TurnoutBoardSlotEntry(
                    nextSlot++,
                    ResolveTurnoutBoardDistrictLabel(electionType, slotItem, target),
                    hasTurnoutData ? slotItem!.TurnoutRate : 0d,
                    RegionLabel: ResolveTurnoutBoardRegionLabel(slotItem, target),
                    HasTurnoutData: hasTurnoutData));
            }
        }

        if (includeRegionalBoardSlots &&
            turnoutBoardSlots.Count == (includeNationalSlot ? 1 : 0))
        {
            throw new InvalidOperationException("표출 가능한 지역 투표율 데이터가 없습니다.");
        }

        var snapshotTurnoutVotes = includeNationalSlot
            ? turnoutOverview.TurnoutVotes
            : primaryItem?.TurnoutVotes ?? 0;
        var snapshotTotalExpectedVotes = includeNationalSlot
            ? turnoutOverview.TotalExpectedVotes
            : primaryItem?.TotalExpectedVotes ?? 0;
        var snapshotReceivedAt = turnoutOverview.ReceivedAt == default
            ? DateTimeOffset.Now
            : turnoutOverview.ReceivedAt;
        var snapshotReferenceTimeLabel = includeNationalSlot
            ? turnoutOverview.ReferenceTimeLabel
            : FirstNonWhiteSpace(primaryItem?.ReferenceTimeLabel, turnoutOverview.ReferenceTimeLabel);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.PreElection,
            ElectionType = electionType,
            DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
            DistrictCode = primaryItem?.DistrictCode ?? primaryTarget?.DistrictCode ?? string.Empty,
            RegionName = regionName,
            ElectionDistrictName = electionDistrictName,
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = Math.Max(snapshotTotalExpectedVotes, 0),
            TurnoutVotes = Math.Max(snapshotTurnoutVotes, 0),
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = snapshotReceivedAt,
            ReferenceTimeLabel = snapshotReferenceTimeLabel,
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory,
            TurnoutBoardSlots = turnoutBoardSlots,
            NationalTurnoutRateOverride = turnoutOverview.NationalTurnoutRate
        };
    }

    private static bool HasPositiveTurnoutOverview(SbsElectionApiClient.TurnoutOverviewItem? item)
    {
        return item is not null &&
               item.TurnoutVotes > 0 &&
               item.TurnoutRate > 0;
    }

    private static string ResolveTurnoutElectionDistrictName(
        string electionType,
        SbsElectionApiClient.TurnoutOverviewItem? item,
        ScheduleRegionTarget? target,
        string regionName,
        string districtName)
    {
        if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
        {
            return FirstNonWhiteSpace(
                item?.DistrictName,
                target?.DistrictName,
                districtName,
                regionName);
        }

        return string.IsNullOrWhiteSpace(regionName) ? districtName : regionName;
    }

    private static string ResolveTurnoutBoardDistrictLabel(
        string electionType,
        SbsElectionApiClient.TurnoutOverviewItem? item,
        ScheduleRegionTarget target)
    {
        if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
        {
            return FirstNonWhiteSpace(
                item?.DistrictName,
                target.DistrictName,
                item?.DisplayName,
                target.DisplayName,
                item?.RegionName,
                target.RegionName);
        }

        return ResolveTurnoutBoardRegionLabel(item, target);
    }

    private static string ResolveTurnoutBoardRegionLabel(
        SbsElectionApiClient.TurnoutOverviewItem? item,
        ScheduleRegionTarget target)
    {
        return FirstNonWhiteSpace(
            item?.RegionName,
            target.RegionName,
            item?.DisplayName,
            target.DisplayName);
    }

    private static string FirstNonWhiteSpace(params string?[] values)
    {
        foreach (var value in values)
        {
            if (!string.IsNullOrWhiteSpace(value))
            {
                return value;
            }
        }

        return string.Empty;
    }

    private static bool ShouldUseTurnoutOverviewSnapshot(FormatTemplateDefinition template, string electionType)
    {
        return SupportsPreElectionTurnout(electionType) &&
               !ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name) &&
               template.Name.Contains("투표율", StringComparison.Ordinal);
    }

    private static bool IsCurrentLeaderTemplate(FormatTemplateDefinition template)
    {
        return template.Name.StartsWith("이시각1위_", StringComparison.Ordinal) ||
               (template.RecommendedChannel == BroadcastChannel.Bottom &&
                template.Name.StartsWith("1위_", 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 bool IsPanseEducationTemplate(FormatTemplateDefinition template)
    {
        return string.Equals(template.Name, "판세_교육감", StringComparison.Ordinal);
    }

    private IEnumerable<string> ResolveApiDistrictRegionScope(string electionType)
    {
        return IsBasicCouncilElectionType(electionType)
            ? _configuredRegions
            : Array.Empty<string>();
    }

    private string ResolveScheduleTargetParentRegionCode(ScheduleRegionTarget target)
    {
        var matchedOption = _districtSelectionSource.FirstOrDefault(option =>
            string.Equals(option.DistrictCode, target.DistrictCode, StringComparison.OrdinalIgnoreCase));
        if (matchedOption is not null && !string.IsNullOrWhiteSpace(matchedOption.ParentRegionCode))
        {
            return matchedOption.ParentRegionCode;
        }

        return SbsElectionApiClient.ResolveBasicApiSidoCode(target.RegionName);
    }

    private static bool IsBottomTurnoutBoardTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               IsBottomTurnoutTemplateName(template.Name);
    }

    private static bool IsBottomTurnoutSidoTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               IsBottomTurnoutSidoTemplateName(template.Name);
    }

    private static bool IsBottomTurnoutDistrictTemplate(FormatTemplateDefinition template)
    {
        return template.RecommendedChannel == BroadcastChannel.Bottom &&
               IsBottomTurnoutDistrictTemplateName(template.Name);
    }

    private static bool IsBottomTurnoutTemplateName(string? templateName)
    {
        return string.Equals(templateName, "사전투표율", StringComparison.Ordinal) ||
               string.Equals(templateName, "투표율", StringComparison.Ordinal) ||
               IsBottomTurnoutSidoTemplateName(templateName) ||
               IsBottomTurnoutDistrictTemplateName(templateName);
    }

    private static bool IsBottomTurnoutSidoTemplateName(string? templateName)
    {
        return string.Equals(templateName, "사전투표율_시도", StringComparison.Ordinal) ||
               string.Equals(templateName, "투표율_시도", StringComparison.Ordinal);
    }

    private static bool IsBottomTurnoutDistrictTemplateName(string? templateName)
    {
        return string.Equals(templateName, "사전투표율_시군구", StringComparison.Ordinal) ||
               string.Equals(templateName, "투표율_시군구", StringComparison.Ordinal);
    }

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

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

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

    private static int ResolveRegionalTurnoutBoardMaxSlots(FormatTemplateDefinition template)
    {
        return IsTopTurnoutDistrictBoardTemplate(template) ? 3 : 7;
    }

    private static SbsElectionApiClient.TurnoutOverviewItem? FindTurnoutOverviewItem(
        IReadOnlyList<SbsElectionApiClient.TurnoutOverviewItem> items,
        ScheduleRegionTarget? target)
    {
        if (target is null || items.Count == 0)
        {
            return null;
        }

        if (!string.IsNullOrWhiteSpace(target.DistrictCode))
        {
            var matchedByCode = items.FirstOrDefault(item =>
                string.Equals(item.DistrictCode, target.DistrictCode, StringComparison.OrdinalIgnoreCase));
            if (matchedByCode is not null)
            {
                return matchedByCode;
            }
        }

        return items.FirstOrDefault(item =>
            string.Equals(item.RegionName, target.RegionName, StringComparison.Ordinal) ||
            string.Equals(item.DisplayName, target.DisplayName, StringComparison.Ordinal) ||
            string.Equals(item.DistrictName, target.DistrictName, StringComparison.Ordinal));
    }

    private static ScheduleRegionTarget CreateScheduleRegionTarget(
        SbsElectionApiClient.DistrictSelectionOption option,
        string electionType)
    {
        return new ScheduleRegionTarget(
            electionType,
            option.DisplayName,
            option.RegionName,
            option.DistrictName,
            option.DistrictCode);
    }

    private static ScheduleRegionTarget CreateSingleScheduleRegionTarget(string electionType)
    {
        return new ScheduleRegionTarget(
            electionType,
            ScheduleTemplatePolicy.SingleRegionLabel,
            string.Empty,
            ScheduleTemplatePolicy.SingleRegionLabel,
            string.Empty);
    }

    private ElectionDataSnapshot CreateSingleScheduleSnapshot(
        string electionType,
        ScheduleRegionTarget target)
    {
        var displayName = string.IsNullOrWhiteSpace(target.DisplayName)
            ? ScheduleTemplatePolicy.SingleRegionLabel
            : target.DisplayName;

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase,
            ElectionType = electionType,
            DistrictName = displayName,
            DistrictCode = string.Empty,
            RegionName = string.Empty,
            ElectionDistrictName = displayName,
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = 0,
            TurnoutVotes = 0,
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = DateTimeOffset.Now
        };
    }

    private ElectionDataSnapshot CreateHistoricalTurnoutScheduleSnapshot(
        string electionType,
        ScheduleRegionTarget target)
    {
        var regionName = target.RegionName ?? string.Empty;
        var districtName = !string.IsNullOrWhiteSpace(target.DistrictName)
            ? target.DistrictName
            : !string.IsNullOrWhiteSpace(target.DisplayName)
                ? target.DisplayName
                : regionName;
        var history = ResolvePreElectionHistoryRecords(
            electionType,
            regionName,
            districtName,
            includeOutsidePreElection: true);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = target.DistrictCode,
            RegionName = regionName,
            ElectionDistrictName = ResolveHistoricalElectionDistrictName(electionType, regionName, districtName),
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = 0,
            TurnoutVotes = 0,
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = DateTimeOffset.Now,
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private ElectionDataSnapshot CreateHistoricalWinnerScheduleSnapshot(
        string electionType,
        ScheduleRegionTarget target)
    {
        var regionName = target.RegionName ?? string.Empty;
        var districtName = !string.IsNullOrWhiteSpace(target.DistrictName)
            ? target.DistrictName
            : !string.IsNullOrWhiteSpace(target.DisplayName)
                ? target.DisplayName
                : regionName;
        var history = ResolvePreElectionHistoryRecords(
            electionType,
            regionName,
            districtName,
            includeOutsidePreElection: true);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase,
            ElectionType = electionType,
            DistrictName = districtName,
            DistrictCode = target.DistrictCode,
            RegionName = regionName,
            ElectionDistrictName = ResolveHistoricalElectionDistrictName(electionType, regionName, districtName),
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = 0,
            TurnoutVotes = 0,
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = DateTimeOffset.Now,
            HistoricalTurnoutHistory = history.TurnoutHistory,
            HistoricalWinnerHistory = history.WinnerHistory
        };
    }

    private static string ResolveHistoricalElectionDistrictName(
        string electionType,
        string regionName,
        string districtName)
    {
        if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
        {
            return string.IsNullOrWhiteSpace(districtName) ? regionName : districtName;
        }

        return string.IsNullOrWhiteSpace(regionName) ? districtName : regionName;
    }

    private static string NormalizeDistrictKey(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        return value
            .Trim()
            .Replace("특별자치도", string.Empty, StringComparison.Ordinal)
            .Replace("특별자치시", string.Empty, StringComparison.Ordinal)
            .Replace("특별시", string.Empty, StringComparison.Ordinal)
            .Replace("광역시", string.Empty, StringComparison.Ordinal)
            .Replace("구청장", "구", StringComparison.Ordinal)
            .Replace("시장", "시", StringComparison.Ordinal)
            .Replace("군수", "군", StringComparison.Ordinal)
            .Replace("교육감", string.Empty, StringComparison.Ordinal)
            .Replace(" ", string.Empty, StringComparison.Ordinal);
    }

    private static bool SupportsApiDistrictOptions(string electionType)
    {
        return string.Equals(electionType, "국회의원", StringComparison.Ordinal) ||
            string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
            string.Equals(electionType, "교육감", StringComparison.Ordinal) ||
            string.Equals(electionType, "광역의원", StringComparison.Ordinal) ||
            string.Equals(electionType, "기초단체장", StringComparison.Ordinal) ||
            string.Equals(electionType, "기초의원", StringComparison.Ordinal);
    }

    private static bool IsBasicCouncilElectionType(string electionType)
    {
        return string.Equals(electionType, "광역의원", StringComparison.Ordinal) ||
            string.Equals(electionType, "기초의원", StringComparison.Ordinal);
    }

    private static bool SupportsApiRefresh(BroadcastPhase phase, string electionType)
    {
        return phase switch
        {
            BroadcastPhase.PreElection => SupportsPreElectionTurnout(electionType),
            BroadcastPhase.Counting => SupportsApiDistrictOptions(electionType),
            _ => false
        };
    }

    private static bool SupportsPreElectionTurnout(string? electionType)
    {
        return string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
            string.Equals(electionType, "교육감", StringComparison.Ordinal) ||
            string.Equals(electionType, "기초단체장", StringComparison.Ordinal);
    }

    private static HashSet<string> NormalizeConfiguredRegions(IEnumerable<string> regionFilters)
    {
        var normalizedRegions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var regionFilter in regionFilters ?? Array.Empty<string>())
        {
            var normalizedRegion = NormalizeConfiguredRegion(regionFilter);
            if (!string.IsNullOrWhiteSpace(normalizedRegion))
            {
                normalizedRegions.Add(normalizedRegion);
            }
        }

        return normalizedRegions;
    }

    private static string NormalizeConfiguredRegion(string? regionName)
    {
        if (string.IsNullOrWhiteSpace(regionName))
        {
            return string.Empty;
        }

        var trimmedRegion = regionName.Trim();
        return RegionAliases.TryGetValue(trimmedRegion, out var normalizedRegion)
            ? normalizedRegion
            : trimmedRegion;
    }

    private static bool MatchesConfiguredRegion(string? regionName, ISet<string> configuredRegions)
    {
        if (configuredRegions.Count == 0)
        {
            return true;
        }

        return GetNormalizedRegionKeys(regionName).Any(configuredRegions.Contains);
    }

    private static IEnumerable<string> GetNormalizedRegionKeys(string? regionName)
    {
        var normalizedRegion = NormalizeConfiguredRegion(regionName);
        if (string.IsNullOrWhiteSpace(normalizedRegion))
        {
            yield break;
        }

        yield return normalizedRegion;

        if (string.Equals(normalizedRegion, "전남광주", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(normalizedRegion, "광주전남", StringComparison.OrdinalIgnoreCase))
        {
            yield return "광주";
            yield return "전남";
        }
    }

    private bool IsStationRegionOverviewMode =>
        string.Equals(SelectedDistrictViewName, StationRegionOverviewOptionValue, StringComparison.Ordinal);

    private static bool IsDistrictOverviewSelection(string? selectedValue)
    {
        return string.Equals(selectedValue, DistrictOverviewOptionValue, StringComparison.Ordinal) ||
            string.Equals(selectedValue, StationRegionOverviewOptionValue, StringComparison.Ordinal);
    }

    private static SbsElectionApiClient.DistrictSelectionOption CreateDistrictOption(string displayName, string districtCode)
    {
        return new SbsElectionApiClient.DistrictSelectionOption(
            DisplayName: displayName,
            DistrictCode: districtCode,
            RegionName: displayName,
            DistrictName: displayName,
            ParentRegionCode: districtCode);
    }

    private static void EnsureOptionExists(ObservableCollection<SelectionOption<string>> options, string value)
    {
        if (string.IsNullOrWhiteSpace(value) || options.Any(option => string.Equals(option.Value, value, StringComparison.Ordinal)))
        {
            return;
        }

        options.Add(new SelectionOption<string>(value, value));
    }

    private readonly record struct PanseSummaryRow(string Party, int Count);
}
