using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using Tornado3_2026Election.Domain;
using Tornado3_2026Election.Services;

internal static class CurrentApiCutDiagnostics
{
    private const string PanseSummaryCandidateCodePrefix = "PANSE:";
    private const string CouncilSeatDistrictCandidateCodePrefix = "SEAT:D:";
    private const string PanseDemocraticPartyLabel = "더불어민주당";
    private const string PansePeoplePowerPartyLabel = "국민의힘";
    private const string PanseOtherPartyLabel = "무·기타";
    private static readonly Regex TopRankSlotCountPattern = new(@"1-(\d+)위", RegexOptions.Compiled);
    private static readonly Regex PeopleSlotCountPattern = new(@"(\d+)인", RegexOptions.Compiled);
    private static readonly IReadOnlyDictionary<string, string> RegionAliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["서울"] = "서울",
        ["서울특별시"] = "서울",
        ["부산"] = "부산",
        ["부산광역시"] = "부산",
        ["대구"] = "대구",
        ["대구광역시"] = "대구",
        ["인천"] = "인천",
        ["인천광역시"] = "인천",
        ["광주"] = "광주",
        ["광주광역시"] = "광주",
        ["대전"] = "대전",
        ["대전광역시"] = "대전",
        ["울산"] = "울산",
        ["울산광역시"] = "울산",
        ["세종"] = "세종",
        ["세종특별자치시"] = "세종",
        ["경기"] = "경기",
        ["경기도"] = "경기",
        ["강원"] = "강원",
        ["강원도"] = "강원",
        ["강원특별자치도"] = "강원",
        ["충북"] = "충북",
        ["충청북도"] = "충북",
        ["충남"] = "충남",
        ["충청남도"] = "충남",
        ["전북"] = "전북",
        ["전라북도"] = "전북",
        ["전북특별자치도"] = "전북",
        ["전남"] = "전남",
        ["전라남도"] = "전남",
        ["경북"] = "경북",
        ["경상북도"] = "경북",
        ["경남"] = "경남",
        ["경상남도"] = "경남",
        ["제주"] = "제주",
        ["제주도"] = "제주",
        ["제주특별자치도"] = "제주"
    };

    public static async Task<int> RunAsync(string[] args)
    {
        var options = CurrentApiCutDiagnosticsOptions.Parse(args);
        Directory.CreateDirectory(options.OutputPath);

        Console.WriteLine("Current API cut diagnostics starting.");
        Console.WriteLine($"- Phase: {options.Phase}");
        Console.WriteLine($"- Station: {(options.AllStations ? "ALL" : options.StationId)}");
        Console.WriteLine($"- Region Scope: {options.RegionScope}");
        Console.WriteLine($"- Max Regions: {(options.MaxRegions <= 0 ? "all" : options.MaxRegions)}");
        Console.WriteLine($"- Send Mode: {ResolveSendModeLabel(options)}");
        Console.WriteLine($"- Scene Capture: {(options.CaptureSceneImages ? "on" : "off")}");
        Console.WriteLine($"- Mixed Preview Capture: {(options.CaptureMixedPreviewImages ? "on" : "off")}");
        Console.WriteLine($"- PGM Capture: {(options.CapturePgmImages ? "on" : "off")}");
        Console.WriteLine($"- Keep On Air Between Sends: {(options.KeepOnAirBetweenSends ? "yes" : "no")}");
        Console.WriteLine($"- Output: {options.OutputPath}");

        var stationCatalog = new StationCatalogService().GetAll();
        var stations = options.AllStations
            ? stationCatalog
            : stationCatalog
                .Where(station => string.Equals(station.Id, options.StationId, StringComparison.OrdinalIgnoreCase))
                .ToArray();

        if (stations.Count == 0)
        {
            Console.WriteLine($"No station matched '{options.StationId}'.");
            return 1;
        }

        var formats = new FormatCatalogService(options.ImageRootPath)
            .GetAll()
            .Where(template => options.IncludeVideoWall || template.RecommendedChannel != BroadcastChannel.VideoWall)
            .Where(template => string.IsNullOrWhiteSpace(options.Filter) ||
                               template.Id.Contains(options.Filter, StringComparison.OrdinalIgnoreCase) ||
                               template.Name.Contains(options.Filter, StringComparison.OrdinalIgnoreCase))
            .Where(template => string.IsNullOrWhiteSpace(options.ExcludeFilter) ||
                               (!template.Id.Contains(options.ExcludeFilter, StringComparison.OrdinalIgnoreCase) &&
                                !template.Name.Contains(options.ExcludeFilter, StringComparison.OrdinalIgnoreCase)))
            .ToArray();

        if (options.TemplateLimit is int templateLimit && templateLimit > 0)
        {
            formats = formats.Take(templateLimit).ToArray();
        }

        var phase = options.Phase == BroadcastPhase.PreElection
            ? BroadcastPhase.PreElection
            : BroadcastPhase.Counting;
        formats = formats
            .Where(template => template.IsAvailableInPhase(phase))
            .ToArray();

        Console.WriteLine($"- Templates: {formats.Length}");
        Console.WriteLine();

        using var apiClient = new SbsElectionApiClient();
        var logService = new LogService();
        var preElectionHistoryService = new PreElectionHistoryService(logService);
        var sceneVariableCatalog = KarismaSceneVariableCatalog.Load(logService);
        ITornado3Adapter? adapter;
        try
        {
            adapter = CreateSendAdapter(options, logService);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            return 1;
        }

        var pgmWindow = options.CapturePgmImages ? TryFindPgmWindow() : null;
        if (options.CapturePgmImages && pgmWindow is null)
        {
            Console.WriteLine("PGM window was not found. Start Tornado3 PGM before using --capture-pgm-images.");
            return 1;
        }

        var districtCache = new Dictionary<string, IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>>(StringComparer.Ordinal);
        var results = new List<CurrentApiCutDiagnosticResult>();
        var simulatedSendCount = 0;

        foreach (var station in stations)
        {
            foreach (var template in formats)
            {
                var electionType = ResolveScheduleElectionType(template, phase, options.DefaultElectionType);
                IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> districts;
                try
                {
                    districts = await GetDistrictsAsync(
                            apiClient,
                            districtCache,
                            electionType,
                            station,
                            options.RegionScope == "all" || IsNormalPanseMapTemplate(template),
                            template,
                            preElectionHistoryService)
                        .ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    results.Add(CurrentApiCutDiagnosticResult.DistrictLoadFailed(station, template, phase, electionType, ex.Message));
                    continue;
                }

                var targets = IsNormalPanseMapTemplate(template)
                    ? districts.ToArray()
                    : ResolveTargets(districts, station, options).ToArray();

                if (targets.Length == 0)
                {
                    results.Add(CurrentApiCutDiagnosticResult.NoTarget(station, template, phase, electionType));
                    continue;
                }

                if (ShouldUseAggregateCurrentLeaderSnapshot(template, phase))
                {
                    foreach (var targetGroup in ResolveCurrentLeaderGroups(template, targets))
                    {
                        var result = new CurrentApiCutDiagnosticResult
                        {
                            Station = station.Id,
                            Channel = template.RecommendedChannel.ToString(),
                            TemplateId = template.Id,
                            TemplateName = template.Name,
                            Phase = phase.ToString(),
                            ElectionType = electionType,
                            Region = string.Join(", ", targetGroup.Select(target => target.DisplayName)),
                            DistrictCode = string.Join(",", targetGroup.Select(target => target.DistrictCode)),
                            Status = "unknown"
                        };

                        try
                        {
                            var snapshot = await CreateAggregatePanseSnapshotAsync(
                                    apiClient,
                                    electionType,
                                    targetGroup,
                                    template,
                                    CancellationToken.None)
                                .ConfigureAwait(false);
                            PopulateDataFields(result, snapshot, "GET /gaepyo aggregate current leader");

                            var sceneVariables = ResolveSceneVariablesForTemplate(sceneVariableCatalog, options.ImageRootPath, template);
                            if (!ValidateSnapshotForFormat(template, snapshot, sceneVariables, out var validationError, out var warning))
                            {
                                result.Status = "validation-failed";
                                result.Detail = validationError;
                                result.Warning = warning;
                            }
                            else if (adapter is not null && simulatedSendCount < options.SendLimit)
                            {
                                await SimulateSendAsync(adapter, station, template, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                                simulatedSendCount++;
                                result.Status = options.LiveSend ? "sent-live" : "sent-mock";
                                result.Detail = options.LiveSend
                                    ? "validated and live send completed"
                                    : "validated and mock send completed";
                                result.Warning = warning;
                            }
                            else
                            {
                                result.Status = "valid";
                                result.Detail = adapter is null ? "validated" : "validated; send limit reached";
                                result.Warning = warning;
                            }
                        }
                        catch (Exception ex)
                        {
                            result.Status = "api-or-send-failed";
                            result.Detail = ex.Message;
                        }

                        results.Add(result);
                    }

                    continue;
                }

                if (ShouldUseAggregateTurnoutSnapshot(template, phase, electionType))
                {
                    var result = new CurrentApiCutDiagnosticResult
                    {
                        Station = station.Id,
                        Channel = template.RecommendedChannel.ToString(),
                        TemplateId = template.Id,
                        TemplateName = template.Name,
                        Phase = phase.ToString(),
                        ElectionType = electionType,
                        Region = string.Join(", ", targets.Select(target => target.DisplayName)),
                        DistrictCode = string.Join(",", targets.Select(target => target.DistrictCode)),
                        Status = "unknown"
                    };

                    try
                    {
                        var snapshot = await CreateAggregateTurnoutSnapshotAsync(
                                apiClient,
                                electionType,
                                districts,
                                targets,
                                template,
                                CancellationToken.None)
                            .ConfigureAwait(false);
                        PopulateDataFields(result, snapshot, "GET /tupyo aggregate overview");

                        var sceneVariables = ResolveSceneVariablesForTemplate(sceneVariableCatalog, options.ImageRootPath, template);
                        if (!ValidateSnapshotForFormat(template, snapshot, sceneVariables, out var validationError, out var warning))
                        {
                            result.Status = "validation-failed";
                            result.Detail = validationError;
                            result.Warning = warning;
                        }
                        else if (adapter is not null && simulatedSendCount < options.SendLimit)
                        {
                            await SimulateSendAsync(adapter, station, template, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                            simulatedSendCount++;
                            result.Status = options.LiveSend ? "sent-live" : "sent-mock";
                            result.Detail = options.LiveSend
                                ? "validated and live send completed"
                                : "validated and mock send completed";
                            result.Warning = warning;
                        }
                        else
                        {
                            result.Status = "valid";
                            result.Detail = adapter is null ? "validated" : "validated; send limit reached";
                            result.Warning = warning;
                        }
                    }
                    catch (Exception ex)
                    {
                        result.Status = "api-or-send-failed";
                        result.Detail = ex.Message;
                    }

                    results.Add(result);
                    continue;
                }

                if (ShouldUseAggregateCouncilSeatSnapshot(template, phase))
                {
                    foreach (var targetGroup in ResolveAggregateCouncilSeatGroups(targets))
                    {
                        var result = new CurrentApiCutDiagnosticResult
                        {
                            Station = station.Id,
                            Channel = template.RecommendedChannel.ToString(),
                            TemplateId = template.Id,
                            TemplateName = template.Name,
                            Phase = phase.ToString(),
                            ElectionType = electionType,
                            Region = string.Join(", ", targetGroup.Select(target => target.DisplayName)),
                            DistrictCode = string.Join(",", targetGroup.Select(target => target.DistrictCode)),
                            Status = "unknown"
                        };

                        try
                        {
                            var snapshot = await CreateAggregateCouncilSeatSnapshotAsync(
                                    apiClient,
                                    electionType,
                                    targetGroup,
                                    CancellationToken.None)
                                .ConfigureAwait(false);
                            PopulateDataFields(result, snapshot, "GET /gaepyo aggregate council seats");

                            var sceneVariables = ResolveSceneVariablesForTemplate(sceneVariableCatalog, options.ImageRootPath, template);
                            if (!ValidateSnapshotForFormat(template, snapshot, sceneVariables, out var validationError, out var warning))
                            {
                                result.Status = "validation-failed";
                                result.Detail = validationError;
                                result.Warning = warning;
                            }
                            else if (adapter is not null && simulatedSendCount < options.SendLimit)
                            {
                                await SimulateSendAsync(adapter, station, template, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                                simulatedSendCount++;
                                result.Status = options.LiveSend ? "sent-live" : "sent-mock";
                                result.Detail = options.LiveSend
                                    ? "validated and live send completed"
                                    : "validated and mock send completed";
                                result.Warning = warning;
                            }
                            else
                            {
                                result.Status = "valid";
                                result.Detail = adapter is null ? "validated" : "validated; send limit reached";
                                result.Warning = warning;
                            }
                        }
                        catch (Exception ex)
                        {
                            result.Status = "api-or-send-failed";
                            result.Detail = ex.Message;
                        }

                        results.Add(result);
                    }

                    continue;
                }

                if (ShouldUseAggregatePanseSnapshot(template, phase))
                {
                    var result = new CurrentApiCutDiagnosticResult
                    {
                        Station = station.Id,
                        Channel = template.RecommendedChannel.ToString(),
                        TemplateId = template.Id,
                        TemplateName = template.Name,
                        Phase = phase.ToString(),
                        ElectionType = electionType,
                        Region = string.Join(", ", targets.Select(target => target.DisplayName)),
                        DistrictCode = string.Join(",", targets.Select(target => target.DistrictCode)),
                        Status = "unknown"
                    };

                    try
                    {
                        var snapshot = await CreateAggregatePanseSnapshotAsync(
                                apiClient,
                                electionType,
                                targets,
                                template,
                                CancellationToken.None)
                            .ConfigureAwait(false);
                        PopulateDataFields(result, snapshot, "GET /gaepyo aggregate panse");

                        var sceneVariables = ResolveSceneVariablesForTemplate(sceneVariableCatalog, options.ImageRootPath, template);
                        if (!ValidateSnapshotForFormat(template, snapshot, sceneVariables, out var validationError, out var warning))
                        {
                            result.Status = "validation-failed";
                            result.Detail = validationError;
                            result.Warning = warning;
                        }
                        else if (adapter is not null && simulatedSendCount < options.SendLimit)
                        {
                            await SimulateSendAsync(adapter, station, template, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                            simulatedSendCount++;
                            result.Status = options.LiveSend ? "sent-live" : "sent-mock";
                            result.Detail = options.LiveSend
                                ? "validated and live send completed"
                                : "validated and mock send completed";
                            result.Warning = warning;
                        }
                        else
                        {
                            result.Status = "valid";
                            result.Detail = adapter is null ? "validated" : "validated; send limit reached";
                            result.Warning = warning;
                        }
                    }
                    catch (Exception ex)
                    {
                        result.Status = "api-or-send-failed";
                        result.Detail = ex.Message;
                    }

                    results.Add(result);
                    continue;
                }

                foreach (var target in targets)
                {
                    var result = new CurrentApiCutDiagnosticResult
                    {
                        Station = station.Id,
                        Channel = template.RecommendedChannel.ToString(),
                        TemplateId = template.Id,
                        TemplateName = template.Name,
                        Phase = phase.ToString(),
                        ElectionType = electionType,
                        Region = target.DisplayName,
                        DistrictCode = target.DistrictCode,
                        Status = "unknown"
                    };

                    try
                    {
                        ElectionDataSnapshot snapshot;
                        if (UsesStoredPreElectionHistory(template))
                        {
                            snapshot = CreateStoredPreElectionHistorySnapshot(
                                phase,
                                electionType,
                                target,
                                preElectionHistoryService);
                            PopulateDataFields(result, snapshot, "stored pre-election history");
                        }
                        else if (ShouldUseSingleTurnoutOverviewSnapshot(template, phase, electionType))
                        {
                            snapshot = await CreateSingleTurnoutOverviewSnapshotAsync(
                                    apiClient,
                                    electionType,
                                    districts,
                                    target,
                                    CancellationToken.None)
                                .ConfigureAwait(false);
                            PopulateDataFields(result, snapshot, "GET /tupyo overview");
                        }
                        else
                        {
                            var refreshPhase = ResolveScheduleRefreshPhase(template, phase);
                            var refreshResult = await apiClient
                                .RefreshAsync(refreshPhase, electionType, target.DisplayName, target.DistrictCode, CancellationToken.None)
                                .ConfigureAwait(false);
                            snapshot = CreateSnapshot(refreshPhase, electionType, refreshResult);
                            PopulateDataFields(result, snapshot, refreshResult.SourcePath);
                        }

                        var sceneVariables = ResolveSceneVariablesForTemplate(sceneVariableCatalog, options.ImageRootPath, template);
                        if (!ValidateSnapshotForFormat(template, snapshot, sceneVariables, out var validationError, out var warning))
                        {
                            result.Status = "validation-failed";
                            result.Detail = validationError;
                            result.Warning = warning;
                        }
                        else if (adapter is not null && simulatedSendCount < options.SendLimit)
                        {
                            await SimulateSendAsync(adapter, station, template, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                            simulatedSendCount++;
                            result.Status = options.LiveSend ? "sent-live" : "sent-mock";
                            result.Detail = options.LiveSend
                                ? "validated and live send completed"
                                : "validated and mock send completed";
                            result.Warning = warning;
                        }
                        else
                        {
                            result.Status = "valid";
                            result.Detail = adapter is null ? "validated" : "validated; send limit reached";
                            result.Warning = warning;
                        }
                    }
                    catch (Exception ex)
                    {
                        result.Status = "api-or-send-failed";
                        result.Detail = ex.Message;
                    }

                    results.Add(result);
                }
            }
        }

        if (adapter is not null && options.KeepOnAirBetweenSends)
        {
            await TryOutAllAsync(adapter).ConfigureAwait(false);
        }

        if (adapter is IDisposable disposable)
        {
            disposable.Dispose();
        }

        WriteKarismaLog(options, logService);
        WriteReports(options, results);
        PrintSummary(results, options.OutputPath);

        return results.Any(result => result.Status is "validation-failed" or "api-or-send-failed" or "no-target")
            ? 1
            : 0;
    }

    private static ITornado3Adapter? CreateSendAdapter(CurrentApiCutDiagnosticsOptions options, LogService logService)
    {
        if (!options.SimulateSend)
        {
            return null;
        }

        if (!options.LiveSend)
        {
            return new MockTornado3Adapter(logService);
        }

        var cutDebugStateStore = new CutDebugStateStore();
        if (!KarismaTornado3Adapter.TryCreate(logService, () => options.ImageRootPath, cutDebugStateStore, out var adapter) ||
            !adapter.IsLiveCg)
        {
            throw new InvalidOperationException("Karisma adapter is not available. Live send cannot continue.");
        }

        return adapter;
    }

    private static string ResolveSendModeLabel(CurrentApiCutDiagnosticsOptions options)
    {
        if (!options.SimulateSend)
        {
            return "off";
        }

        return options.LiveSend
            ? $"live ({options.SendLimit})"
            : $"mock ({options.SendLimit})";
    }

    private static async Task<ElectionDataSnapshot> CreateAggregateTurnoutSnapshotAsync(
        SbsElectionApiClient apiClient,
        string electionType,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> allDistricts,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> selectedDistricts,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        var overview = await apiClient
            .GetTurnoutOverviewAsync(electionType, allDistricts, cancellationToken)
            .ConfigureAwait(false);
        var primaryDistrict = selectedDistricts.FirstOrDefault();
        var primaryItem = FindTurnoutOverviewItem(overview.Items, primaryDistrict);
        var includeNationalSlot = IsBottomTurnoutBoardTemplate(template);
        var maxRegionalSlots = includeNationalSlot
            ? 4
            : IsTopTurnoutDistrictBoardTemplate(template)
                ? 3
                : 7;
        var turnoutBoardSlots = new List<TurnoutBoardSlotEntry>();

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

        var nextSlot = turnoutBoardSlots.Count + 1;
        foreach (var district in selectedDistricts.Take(maxRegionalSlots))
        {
            var item = FindTurnoutOverviewItem(overview.Items, district);
            if (item is null || item.TurnoutVotes <= 0 || item.TurnoutRate <= 0)
            {
                continue;
            }

            turnoutBoardSlots.Add(new TurnoutBoardSlotEntry(
                nextSlot++,
                ResolveTurnoutBoardDistrictLabel(electionType, item, district),
                item.TurnoutRate,
                RegionLabel: ResolveTurnoutBoardRegionLabel(item, district)));
        }

        if (turnoutBoardSlots.Count == (includeNationalSlot ? 1 : 0))
        {
            throw new InvalidOperationException("No positive turnout board slots were available.");
        }

        var regionName = primaryItem?.RegionName ?? primaryDistrict?.RegionName ?? string.Empty;
        var districtName = primaryItem?.DisplayName ?? primaryDistrict?.DisplayName ?? regionName;
        var electionDistrictName = ResolveTurnoutElectionDistrictName(
            electionType,
            primaryItem,
            primaryDistrict,
            regionName,
            districtName);
        var totalExpectedVotes = includeNationalSlot
            ? overview.TotalExpectedVotes
            : primaryItem?.TotalExpectedVotes ?? 0;
        var turnoutVotes = includeNationalSlot
            ? overview.TurnoutVotes
            : primaryItem?.TurnoutVotes ?? 0;

        var snapshotReferenceTimeLabel = includeNationalSlot
            ? overview.ReferenceTimeLabel
            : FirstNonWhiteSpace(primaryItem?.ReferenceTimeLabel, overview.ReferenceTimeLabel);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.PreElection,
            ElectionType = electionType,
            DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
            DistrictCode = primaryItem?.DistrictCode ?? primaryDistrict?.DistrictCode ?? string.Empty,
            RegionName = regionName,
            ElectionDistrictName = electionDistrictName,
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = Math.Max(0, totalExpectedVotes),
            TurnoutVotes = Math.Max(0, turnoutVotes),
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = overview.ReceivedAt == default ? DateTimeOffset.Now : overview.ReceivedAt,
            ReferenceTimeLabel = snapshotReferenceTimeLabel,
            TurnoutBoardSlots = turnoutBoardSlots,
            NationalTurnoutRateOverride = overview.NationalTurnoutRate
        };
    }

    private static async Task<ElectionDataSnapshot> CreateAggregatePanseSnapshotAsync(
        SbsElectionApiClient apiClient,
        string electionType,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> selectedDistricts,
        FormatTemplateDefinition template,
        CancellationToken cancellationToken)
    {
        var refreshResults = await apiClient
            .GetCountingSnapshotsAsync(electionType, selectedDistricts, cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("No counting data was available for panse aggregation.");
        }

        var candidates = IsCurrentLeaderTemplate(template) || IsNormalPanseMapTemplate(template)
            ? BuildRegionalLeaderCandidates(refreshResults)
            : IsPanseEducationTemplate(template)
                ? BuildEducationPanseSummaryCandidates(refreshResults)
                : BuildPartyPanseSummaryCandidates(electionType, refreshResults);
        if (candidates.Length == 0)
        {
            throw new InvalidOperationException("No candidates were available for panse aggregation.");
        }

        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 primaryDistrict = selectedDistricts.FirstOrDefault();
        var regionName = ResolveAggregateRegionLabel(selectedDistricts);
        var districtName = selectedDistricts.Count == 1
            ? primaryDistrict?.DisplayName ?? regionName
            : regionName;

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
            DistrictCode = selectedDistricts.Count == 1 ? primaryDistrict?.DistrictCode ?? string.Empty : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedDistricts.Count == 1 ? primaryDistrict?.DistrictName ?? districtName : regionName,
            Candidates = candidates,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max()
        };
    }

    private static async Task<ElectionDataSnapshot> CreateAggregateCouncilSeatSnapshotAsync(
        SbsElectionApiClient apiClient,
        string electionType,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> selectedDistricts,
        CancellationToken cancellationToken)
    {
        var refreshResults = await apiClient
            .GetCountingSnapshotsAsync(electionType, selectedDistricts, cancellationToken)
            .ConfigureAwait(false);
        if (refreshResults.Count == 0)
        {
            throw new InvalidOperationException("No counting data was available for council seat aggregation.");
        }

        var candidates = BuildCouncilSeatSummaryCandidates(refreshResults);
        if (candidates.Length == 0)
        {
            throw new InvalidOperationException("No council seat candidates were available for aggregation.");
        }

        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 primaryDistrict = selectedDistricts.FirstOrDefault();
        var regionName = ResolveAggregateRegionLabel(selectedDistricts);
        var districtName = selectedDistricts.Count == 1
            ? primaryDistrict?.DisplayName ?? regionName
            : regionName;

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.Counting,
            ElectionType = electionType,
            DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
            DistrictCode = selectedDistricts.Count == 1 ? primaryDistrict?.DistrictCode ?? string.Empty : string.Empty,
            RegionName = regionName,
            ElectionDistrictName = selectedDistricts.Count == 1 ? primaryDistrict?.DistrictName ?? districtName : regionName,
            Candidates = candidates,
            TotalExpectedVotes = totalVotes,
            TurnoutVotes = turnoutVotes,
            CountedVotesFromApi = countedVotes,
            RemainingVotesFromApi = remainingVotes,
            CountedRateFromApi = countedRate,
            ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max()
        };
    }

    private static IReadOnlyList<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> ResolveAggregateCouncilSeatGroups(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> targets)
    {
        return targets
            .GroupBy(ResolveCouncilSeatGroupKey, StringComparer.OrdinalIgnoreCase)
            .Select(group => (IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>)group.ToArray())
            .ToArray();
    }

    private static IReadOnlyList<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> ResolveCurrentLeaderGroups(
        FormatTemplateDefinition template,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> targets)
    {
        if (IsBottomCurrentLeaderTemplate(template))
        {
            return [targets];
        }

        var pageSize = Math.Max(1, ResolveCurrentLeaderPageSize(template));
        return targets
            .Select((target, index) => new { Target = target, PageIndex = index / pageSize })
            .GroupBy(item => item.PageIndex)
            .Select(group => (IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>)group
                .Select(item => item.Target)
                .ToArray())
            .ToArray();
    }

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

    private static string ResolveCouncilSeatGroupKey(SbsElectionApiClient.DistrictSelectionOption target)
    {
        return FirstNonWhiteSpace(target.RegionName, target.ParentRegionCode, target.DisplayName, "ALL");
    }

    private static CandidateEntry[] BuildCouncilSeatSummaryCandidates(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        var seatCandidates = refreshResults.Any(result => !result.CountingClosed)
            ? refreshResults.SelectMany(ResolveCurrentCouncilSeatCandidates).ToArray()
            : refreshResults
                .SelectMany(result => result.Candidates ?? Array.Empty<CandidateEntry>())
                .Where(candidate => CountsAsCouncilSeat(candidate.EffectiveJudgement))
                .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 => ResolvePanseParty(candidate), StringComparer.OrdinalIgnoreCase)
            .Select(group =>
            {
                var first = group.First();
                return new
                {
                    Party = group.Key,
                    ColorParty = FirstNonWhiteSpace(first.EffectiveColorParty, first.Party, group.Key),
                    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(CultureInfo.InvariantCulture),
                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 string ResolveAggregateRegionLabel(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> selectedDistricts)
    {
        var regionNames = selectedDistricts
            .Select(target => target.RegionName)
            .Where(regionName => !string.IsNullOrWhiteSpace(regionName))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (regionNames.Length == 1)
        {
            return regionNames[0];
        }

        return "전체";
    }

    private static CandidateEntry[] BuildRegionalLeaderCandidates(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        return refreshResults
            .Select(CreateCurrentLeaderCandidate)
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
    }

    private static CandidateEntry[] BuildEducationPanseSummaryCandidates(
        IReadOnlyList<SbsElectionApiClient.SbsElectionRefreshResult> refreshResults)
    {
        var leaders = refreshResults
            .Select(CreateCurrentLeaderCandidate)
            .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(CreateCurrentLeaderCandidate)
            .Where(candidate => candidate is not null)
            .Cast<CandidateEntry>()
            .ToArray();
        return BuildPanseSummaryCandidates(leaders, ResolvePanseParty, countByVoteCount: false);
    }

    private static CandidateEntry? CreateCurrentLeaderCandidate(
        SbsElectionApiClient.SbsElectionRefreshResult result)
    {
        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, result.ElectionDistrictName);
        candidate.BroadcastRegionName = result.RegionName;
        candidate.BroadcastElectionDistrictName = FirstNonWhiteSpace(result.ElectionDistrictName, result.DistrictName);
        candidate.BroadcastDistrictCode = result.DistrictCode;
        candidate.BroadcastCountedRate = result.CountedRate;
        return candidate;
    }

    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 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 ResolvePanseParty(CandidateEntry candidate)
    {
        return FirstNonWhiteSpace(candidate.Party, candidate.EffectiveColorParty, "무소속");
    }

    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 async Task<ElectionDataSnapshot> CreateSingleTurnoutOverviewSnapshotAsync(
        SbsElectionApiClient apiClient,
        string electionType,
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> allDistricts,
        SbsElectionApiClient.DistrictSelectionOption target,
        CancellationToken cancellationToken)
    {
        var overview = await apiClient
            .GetTurnoutOverviewAsync(electionType, allDistricts, cancellationToken)
            .ConfigureAwait(false);
        var item = FindTurnoutOverviewItem(overview.Items, target);
        if (item is null || item.TurnoutVotes <= 0 || item.TurnoutRate <= 0)
        {
            throw new InvalidOperationException("Selected region has no positive turnout overview data.");
        }

        var regionName = item.RegionName ?? target.RegionName ?? string.Empty;
        var districtName = item.DisplayName ?? target.DisplayName ?? regionName;
        var electionDistrictName = ResolveTurnoutElectionDistrictName(
            electionType,
            item,
            target,
            regionName,
            districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = BroadcastPhase.PreElection,
            ElectionType = electionType,
            DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
            DistrictCode = item.DistrictCode ?? target.DistrictCode ?? string.Empty,
            RegionName = regionName,
            ElectionDistrictName = electionDistrictName,
            Candidates = Array.Empty<CandidateEntry>(),
            TotalExpectedVotes = Math.Max(0, item.TotalExpectedVotes),
            TurnoutVotes = Math.Max(0, item.TurnoutVotes),
            CountedVotesFromApi = null,
            RemainingVotesFromApi = null,
            CountedRateFromApi = null,
            ReceivedAt = overview.ReceivedAt == default ? DateTimeOffset.Now : overview.ReceivedAt,
            ReferenceTimeLabel = FirstNonWhiteSpace(item.ReferenceTimeLabel, overview.ReferenceTimeLabel),
            NationalTurnoutRateOverride = overview.NationalTurnoutRate
        };
    }

    private static bool ShouldUseAggregateTurnoutSnapshot(
        FormatTemplateDefinition template,
        BroadcastPhase phase,
        string electionType)
    {
        return phase == BroadcastPhase.PreElection &&
               SupportsPreElectionTurnout(electionType) &&
               (IsBottomTurnoutBoardTemplate(template) ||
                IsRegionalTurnoutBoardTemplate(template) ||
                IsTopTurnoutDistrictBoardTemplate(template));
    }

    private static bool ShouldUseAggregatePanseSnapshot(
        FormatTemplateDefinition template,
        BroadcastPhase phase)
    {
        return phase == BroadcastPhase.Counting &&
               ((template.RecommendedChannel == BroadcastChannel.TopLeft &&
                 template.Name.StartsWith("판세_", StringComparison.Ordinal)) ||
                IsNormalPanseMapTemplate(template));
    }

    private static bool ShouldUseAggregateCurrentLeaderSnapshot(
        FormatTemplateDefinition template,
        BroadcastPhase phase)
    {
        return phase == BroadcastPhase.Counting &&
               IsCurrentLeaderTemplate(template);
    }

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

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

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

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

        return template.Name.Contains("_HD", StringComparison.Ordinal) ||
               template.Id.Contains("_HD", StringComparison.Ordinal)
            ? 2
            : 1;
    }

    private static bool ShouldUseAggregateCouncilSeatSnapshot(
        FormatTemplateDefinition template,
        BroadcastPhase phase)
    {
        return phase == BroadcastPhase.Counting &&
               ScheduleTemplatePolicy.IsCouncilSeatTableFormat(template.Name);
    }

    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 static bool IsCouncilElectionType(string electionType)
    {
        return string.Equals(electionType, "광역의원", StringComparison.Ordinal) ||
               string.Equals(electionType, "기초의원", StringComparison.Ordinal);
    }

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

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

    private static bool IsRegionalTurnoutBoardTemplate(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 SbsElectionApiClient.TurnoutOverviewItem? FindTurnoutOverviewItem(
        IReadOnlyList<SbsElectionApiClient.TurnoutOverviewItem> items,
        SbsElectionApiClient.DistrictSelectionOption? district)
    {
        if (district is null || items.Count == 0)
        {
            return null;
        }

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

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

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

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

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

        return ResolveTurnoutBoardRegionLabel(item, district);
    }

    private static string ResolveTurnoutBoardRegionLabel(
        SbsElectionApiClient.TurnoutOverviewItem item,
        SbsElectionApiClient.DistrictSelectionOption district)
    {
        return FirstNonWhiteSpace(
            item.RegionName,
            district.RegionName,
            item.DisplayName,
            district.DisplayName);
    }

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

        return string.Empty;
    }

    private static async Task<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> GetDistrictsAsync(
        SbsElectionApiClient apiClient,
        IDictionary<string, IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> districtCache,
        string electionType,
        BroadcastStationProfile station,
        bool useAllRegions,
        FormatTemplateDefinition template,
        PreElectionHistoryService preElectionHistoryService)
    {
        if (UsesHistoricalStoredOptions(template))
        {
            return GetStoredHistoryDistricts(electionType, preElectionHistoryService);
        }

        var regionFilters = useAllRegions ? Array.Empty<string>() : station.RegionFilters;
        var cacheKey = $"{electionType}|{string.Join(",", regionFilters)}";
        if (!districtCache.TryGetValue(cacheKey, out var districts))
        {
            districts = await apiClient
                .GetDistrictOptionsAsync(electionType, regionFilters, CancellationToken.None)
                .ConfigureAwait(false);
            districtCache[cacheKey] = districts;
        }

        return districts;
    }

    private static IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> GetStoredHistoryDistricts(
        string electionType,
        PreElectionHistoryService preElectionHistoryService)
    {
        return preElectionHistoryService
            .GetSelectionRecords(electionType)
            .Where(record => !string.IsNullOrWhiteSpace(ResolveStoredHistoryDisplayName(record)))
            .OrderBy(record => SbsElectionApiClient.ResolveBasicApiSidoCode(record.RegionName), StringComparer.Ordinal)
            .ThenBy(record => ResolveStoredHistoryDisplayName(record), StringComparer.Ordinal)
            .Select(CreateStoredHistoryDistrict)
            .ToArray();
    }

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

        return new SbsElectionApiClient.DistrictSelectionOption(
            ResolveStoredHistoryDisplayName(record),
            districtCode,
            regionName,
            districtName,
            parentRegionCode);
    }

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

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

        return record.RegionName ?? string.Empty;
    }

    private static IEnumerable<SbsElectionApiClient.DistrictSelectionOption> ResolveTargets(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> districts,
        BroadcastStationProfile station,
        CurrentApiCutDiagnosticsOptions options)
    {
        IEnumerable<SbsElectionApiClient.DistrictSelectionOption> targets = options.RegionScope switch
        {
            "all" => districts,
            _ => ResolveStationTargets(districts, station)
        };

        if (options.MaxRegions > 0)
        {
            targets = targets.Take(options.MaxRegions);
        }

        return targets;
    }

    private static IEnumerable<SbsElectionApiClient.DistrictSelectionOption> ResolveStationTargets(
        IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> districts,
        BroadcastStationProfile station)
    {
        var configuredRegions = station.RegionFilters
            .Select(NormalizeRegion)
            .Where(region => !string.IsNullOrWhiteSpace(region))
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        if (configuredRegions.Count == 0)
        {
            return districts;
        }

        return districts.Where(district => GetNormalizedRegionKeys(district.RegionName).Any(configuredRegions.Contains));
    }

    private static ElectionDataSnapshot CreateSnapshot(
        BroadcastPhase phase,
        string electionType,
        SbsElectionApiClient.SbsElectionRefreshResult refreshResult)
    {
        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;

        return new ElectionDataSnapshot
        {
            BroadcastPhase = phase,
            ElectionType = electionType,
            DistrictName = districtName ?? string.Empty,
            DistrictCode = refreshResult.DistrictCode ?? string.Empty,
            RegionName = regionName ?? string.Empty,
            ElectionDistrictName = electionDistrictName ?? string.Empty,
            Candidates = refreshResult.Candidates ?? Array.Empty<CandidateEntry>(),
            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
        };
    }

    private static ElectionDataSnapshot CreateStoredPreElectionHistorySnapshot(
        BroadcastPhase phase,
        string electionType,
        SbsElectionApiClient.DistrictSelectionOption target,
        PreElectionHistoryService preElectionHistoryService)
    {
        var regionName = target.RegionName ?? string.Empty;
        var districtName = !string.IsNullOrWhiteSpace(target.DistrictName)
            ? target.DistrictName
            : !string.IsNullOrWhiteSpace(target.DisplayName)
                ? target.DisplayName
                : regionName;
        var history = preElectionHistoryService.ResolveHistory(electionType, regionName, districtName);

        return new ElectionDataSnapshot
        {
            BroadcastPhase = phase,
            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.OrderBy(entry => entry.Year).ToArray()
                ?? Array.Empty<PreElectionHistoricalTurnoutEntry>(),
            HistoricalWinnerHistory = history?.WinnerHistory.OrderBy(entry => entry.ElectionOrder).ToArray()
                ?? Array.Empty<PreElectionHistoricalWinnerEntry>()
        };
    }

    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 async Task SimulateSendAsync(
        ITornado3Adapter adapter,
        BroadcastStationProfile station,
        FormatTemplateDefinition template,
        ElectionDataSnapshot snapshot,
        CurrentApiCutDiagnosticsOptions options,
        CurrentApiCutDiagnosticResult result,
        PgmWindow? pgmWindow)
    {
        foreach (var cut in ResolveDiagnosticPlaybackCuts(template, snapshot))
        {
            Exception? lastException = null;
            for (var attempt = 1; attempt <= 3; attempt++)
            {
                try
                {
                    await SendSingleCutAsync(adapter, station, template, cut, snapshot, options, result, pgmWindow).ConfigureAwait(false);
                    lastException = null;
                    break;
                }
                catch (Exception ex) when (attempt < 3)
                {
                    lastException = ex;
                    await TryOutAsync(adapter, template.RecommendedChannel).ConfigureAwait(false);
                    await Task.Delay(750, CancellationToken.None).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    lastException = ex;
                }
            }

            if (lastException is not null)
            {
                throw lastException;
            }
        }
    }

    private static IReadOnlyList<FormatCutDefinition> ResolveDiagnosticPlaybackCuts(
        FormatTemplateDefinition template,
        ElectionDataSnapshot snapshot)
    {
        if (!IsCandidatePagedTemplate(template) || template.Cuts.Count == 0)
        {
            return template.Cuts;
        }

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

        return playbackCuts;
    }

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

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

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

            return ResolveCandidatePageSize(template) == 1
                ? ResolveSuffixedCutName(cutName, "_loop")
                : cutName;
        }

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

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

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

        return cutName + suffix;
    }

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

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

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

    private static BroadcastPhase ResolveScheduleRefreshPhase(FormatTemplateDefinition template, BroadcastPhase phase)
    {
        return phase == BroadcastPhase.PreElection && IsCareerTemplate(template)
            ? BroadcastPhase.Counting
            : phase;
    }

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

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

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

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

    private static async Task SendSingleCutAsync(
        ITornado3Adapter adapter,
        BroadcastStationProfile station,
        FormatTemplateDefinition template,
        FormatCutDefinition cut,
        ElectionDataSnapshot snapshot,
        CurrentApiCutDiagnosticsOptions options,
        CurrentApiCutDiagnosticResult result,
        PgmWindow? pgmWindow)
    {
        await adapter.EnsureConnectedAsync(CancellationToken.None).ConfigureAwait(false);
        ThrowIfAdapterErrored(adapter, "connect");
        try
        {
            await adapter.ApplyCutAsync(template.RecommendedChannel, template, cut, snapshot, station, options.ImageRootPath, CancellationToken.None).ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "apply");
            await adapter.PrepareAsync(template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "prepare");
            await adapter.TakeAsync(template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "take");
            if (options.CaptureMixedPreviewImages)
            {
                await CaptureMixedPreviewImagesIfRequestedAsync(adapter, template, cut, options, result).ConfigureAwait(false);
            }

            if (options.CaptureSceneImages || options.CapturePgmImages)
            {
                await Task.Delay(options.OnAirDelayMs, CancellationToken.None).ConfigureAwait(false);
            }

            await CaptureSceneImageIfRequestedAsync(adapter, template, cut, options, result).ConfigureAwait(false);
            CapturePgmImageIfRequested(pgmWindow, template, cut, options, result);
        }
        finally
        {
            if (!options.KeepOnAirBetweenSends)
            {
                await TryOutAsync(adapter, template.RecommendedChannel).ConfigureAwait(false);
                if (adapter.IsLiveCg)
                {
                    await Task.Delay(250, CancellationToken.None).ConfigureAwait(false);
                }
            }
        }
    }

    private static async Task TryOutAsync(ITornado3Adapter adapter, BroadcastChannel channel)
    {
        try
        {
            await adapter.OutAsync(channel, CancellationToken.None).ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "out");
        }
        catch
        {
            if (!adapter.IsLiveCg)
            {
                throw;
            }
        }
    }

    private static async Task TryOutAllAsync(ITornado3Adapter adapter)
    {
        foreach (var channel in Enum.GetValues<BroadcastChannel>())
        {
            try
            {
                await TryOutAsync(adapter, channel).ConfigureAwait(false);
            }
            catch
            {
            }
        }
    }

    private static async Task CaptureSceneImageIfRequestedAsync(
        ITornado3Adapter adapter,
        FormatTemplateDefinition template,
        FormatCutDefinition cut,
        CurrentApiCutDiagnosticsOptions options,
        CurrentApiCutDiagnosticResult result)
    {
        if (!options.CaptureSceneImages || adapter is not KarismaTornado3Adapter karismaAdapter)
        {
            return;
        }

        var captureDirectory = Path.Combine(options.OutputPath, "captures");
        Directory.CreateDirectory(captureDirectory);
        var districtToken = result.DistrictCode.Replace(",", "-", StringComparison.Ordinal);
        if (districtToken.Length > 40)
        {
            districtToken = districtToken[..40];
        }

        var fileStem = SanitizeFileName(
            $"{result.Station}_{result.Channel}_{result.ElectionType}_{template.Name}_{districtToken}_{cut.Name}");
        var (width, height) = ResolveSceneCaptureSize(template);
        var captureFrames = options.SceneCaptureFrames.Count == 0
            ? new[] { -1 }
            : options.SceneCaptureFrames;

        for (var index = 0; index < captureFrames.Count; index++)
        {
            var frame = captureFrames[index];
            var frameToken = frame < 0 ? "final" : $"f{frame:0000}";
            var outputPath = Path.GetFullPath(Path.Combine(captureDirectory, $"{fileStem}_{frameToken}.png"));

            await karismaAdapter.SavePendingSceneImageAsync(
                template.RecommendedChannel,
                outputPath,
                width,
                height,
                frame,
                CancellationToken.None)
            .ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "capture");

            if (index == captureFrames.Count - 1)
            {
                result.CapturePath = outputPath;
                result.CaptureHash = ComputeSha256(outputPath);
                result.CaptureBytes = new FileInfo(outputPath).Length;
            }
        }
    }

    private static async Task CaptureMixedPreviewImagesIfRequestedAsync(
        ITornado3Adapter adapter,
        FormatTemplateDefinition template,
        FormatCutDefinition cut,
        CurrentApiCutDiagnosticsOptions options,
        CurrentApiCutDiagnosticResult result)
    {
        if (!options.CaptureMixedPreviewImages)
        {
            return;
        }

        var captureDirectory = Path.Combine(options.OutputPath, "captures");
        Directory.CreateDirectory(captureDirectory);
        var districtToken = result.DistrictCode.Replace(",", "-", StringComparison.Ordinal);
        if (districtToken.Length > 40)
        {
            districtToken = districtToken[..40];
        }

        var fileStem = SanitizeFileName(
            $"{result.Station}_{result.Channel}_{result.ElectionType}_{template.Name}_{districtToken}_{cut.Name}_MIXED");
        var (width, height) = ResolveSceneCaptureSize(template);
        var captureDelays = options.MixedPreviewCaptureDelaysMs.Count == 0
            ? new[] { options.OnAirDelayMs }
            : options.MixedPreviewCaptureDelaysMs
                .Where(delay => delay >= 0)
                .Distinct()
                .OrderBy(delay => delay)
                .ToArray();

        var elapsedMs = 0;
        for (var index = 0; index < captureDelays.Length; index++)
        {
            var delayMs = captureDelays[index];
            var waitMs = Math.Max(0, delayMs - elapsedMs);
            if (waitMs > 0)
            {
                await Task.Delay(waitMs, CancellationToken.None).ConfigureAwait(false);
            }

            elapsedMs = delayMs;
            var outputPath = Path.GetFullPath(Path.Combine(captureDirectory, $"{fileStem}_d{delayMs:0000}.png"));
            var captured = await adapter.TryCapturePendingCutPreviewAsync(
                    template.RecommendedChannel,
                    outputPath,
                    width,
                    height,
                    frame: -1,
                    CancellationToken.None)
                .ConfigureAwait(false);
            ThrowIfAdapterErrored(adapter, "mixed-preview-capture");

            if (!captured)
            {
                result.Warning = AppendWarning(result.Warning, $"mixed preview capture failed at {delayMs}ms");
                continue;
            }

            if (index == captureDelays.Length - 1)
            {
                result.MixedPreviewCapturePath = outputPath;
                result.MixedPreviewCaptureHash = ComputeSha256(outputPath);
                result.MixedPreviewCaptureBytes = new FileInfo(outputPath).Length;
            }
        }
    }

    private static void CapturePgmImageIfRequested(
        PgmWindow? pgmWindow,
        FormatTemplateDefinition template,
        FormatCutDefinition cut,
        CurrentApiCutDiagnosticsOptions options,
        CurrentApiCutDiagnosticResult result)
    {
        if (!options.CapturePgmImages || pgmWindow is null)
        {
            return;
        }

        var captureDirectory = Path.Combine(options.OutputPath, "captures");
        Directory.CreateDirectory(captureDirectory);
        var districtToken = result.DistrictCode.Replace(",", "-", StringComparison.Ordinal);
        if (districtToken.Length > 40)
        {
            districtToken = districtToken[..40];
        }

        var fileStem = SanitizeFileName(
            $"{result.Station}_{result.Channel}_{result.ElectionType}_{template.Name}_{districtToken}_{cut.Name}_PGM");
        var outputPath = Path.GetFullPath(Path.Combine(captureDirectory, $"{fileStem}.png"));

        using var bitmap = CaptureWindowBitmap(pgmWindow.Value.Bounds);
        bitmap.Save(outputPath, ImageFormat.Png);

        result.PgmCapturePath = outputPath;
        result.PgmCaptureHash = ComputeSha256(outputPath);
        result.PgmCaptureBytes = new FileInfo(outputPath).Length;
    }

    private static PgmWindow? TryFindPgmWindow()
    {
        var process = Process.GetProcessesByName("Tornado3")
            .FirstOrDefault(candidate => string.Equals(candidate.MainWindowTitle, "PGM", StringComparison.Ordinal));
        if (process is not null && process.MainWindowHandle != IntPtr.Zero)
        {
            var processWindow = TryBuildPgmWindow(process.MainWindowHandle);
            if (processWindow is not null)
            {
                return processWindow;
            }
        }

        var windows = new List<PgmWindow>();
        EnumWindows((windowHandle, _) =>
        {
            if (!IsWindowVisible(windowHandle) ||
                !string.Equals(GetWindowTitle(windowHandle), "PGM", StringComparison.Ordinal))
            {
                return true;
            }

            var window = TryBuildPgmWindow(windowHandle);
            if (window is not null)
            {
                windows.Add(window.Value);
            }

            return true;
        }, IntPtr.Zero);

        return windows.Count == 0
            ? null
            : windows
                .OrderByDescending(window => window.Bounds.Width * window.Bounds.Height)
                .First();
    }

    private static PgmWindow? TryBuildPgmWindow(IntPtr windowHandle)
    {
        if (!GetWindowRect(windowHandle, out var rect))
        {
            return null;
        }

        var width = rect.Right - rect.Left;
        var height = rect.Bottom - rect.Top;
        if (width <= 0 || height <= 0)
        {
            return null;
        }

        return new PgmWindow(windowHandle, new Rect(rect.Left, rect.Top, width, height));
    }

    private static string GetWindowTitle(IntPtr windowHandle)
    {
        var length = GetWindowTextLength(windowHandle);
        if (length <= 0)
        {
            return string.Empty;
        }

        var builder = new StringBuilder(length + 1);
        _ = GetWindowText(windowHandle, builder, builder.Capacity);
        return builder.ToString();
    }

    private static Bitmap CaptureWindowBitmap(Rect bounds)
    {
        var width = Math.Max(1, bounds.Width);
        var height = Math.Max(1, bounds.Height);
        var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
        using var graphics = Graphics.FromImage(bitmap);

        graphics.CopyFromScreen(bounds.Left, bounds.Top, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
        return bitmap;
    }

    private static (int Width, int Height) ResolveSceneCaptureSize(FormatTemplateDefinition template)
    {
        var sourceWidth = template.SceneWidth.GetValueOrDefault(1920);
        var sourceHeight = template.SceneHeight.GetValueOrDefault(1080);
        if (sourceWidth <= 0 || sourceHeight <= 0)
        {
            return (1280, 720);
        }

        const int maxWidth = 1280;
        if (sourceWidth <= maxWidth)
        {
            return (sourceWidth, sourceHeight);
        }

        var scale = maxWidth / (double)sourceWidth;
        return (maxWidth, Math.Max(1, (int)Math.Round(sourceHeight * scale, MidpointRounding.AwayFromZero)));
    }

    private static string ComputeSha256(string path)
    {
        using var stream = File.OpenRead(path);
        return Convert.ToHexString(SHA256.HashData(stream));
    }

    private static string SanitizeFileName(string value)
    {
        var invalidChars = Path.GetInvalidFileNameChars();
        var sanitized = new string(value.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray()).Trim();
        if (sanitized.Length > 80)
        {
            sanitized = sanitized[..80];
        }

        return string.IsNullOrWhiteSpace(sanitized) ? "capture.png" : sanitized;
    }

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowRect(IntPtr hWnd, out NativeRect lpRect);

    private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern int GetWindowTextLength(IntPtr hWnd);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool IsWindowVisible(IntPtr hWnd);

    private static void ThrowIfAdapterErrored(ITornado3Adapter adapter, string action)
    {
        if (adapter.State == TornadoConnectionState.Error)
        {
            throw new InvalidOperationException($"Karisma live send failed during {action}.");
        }
    }

    private static bool ValidateSnapshotForFormat(
        FormatTemplateDefinition template,
        ElectionDataSnapshot snapshot,
        IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables,
        out string errorMessage,
        out string warning)
    {
        warning = string.Empty;

        if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(template.Name))
        {
            if (snapshot.HistoricalTurnoutHistory.Count == 0)
            {
                errorMessage = "historical turnout data is empty";
                return false;
            }

            errorMessage = string.Empty;
            return true;
        }

        if (ScheduleTemplatePolicy.IsHistoricalWinnerFormat(template.Name))
        {
            if (snapshot.HistoricalWinnerHistory.Count == 0)
            {
                errorMessage = "historical winner data is empty";
                return false;
            }

            errorMessage = string.Empty;
            return true;
        }

        if (IsTurnoutTemplate(template) &&
            (snapshot.TurnoutVotes <= 0 || snapshot.TurnoutRate <= 0))
        {
            errorMessage = "turnout votes/rate is zero";
            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
                ? "winner candidate list is empty"
                : "candidate list is empty";
            return false;
        }

        if (isWinnerTemplate &&
            !validCandidates.Any(candidate => IsElectedJudgement(candidate.EffectiveJudgement)))
        {
            errorMessage = "elected judgement is missing";
            return false;
        }

        if (!isWinnerTemplate && validCandidates.Length != snapshot.Candidates.Count)
        {
            errorMessage = "required candidate fields are blank";
            return false;
        }

        if (sceneVariables.Count > 0 &&
            RequiresTopTwoCandidateNameVariables(template) &&
            !HasCandidateNameVariables(sceneVariables, Math.Min(2, validCandidates.Length), out var missingCandidateNameVariables))
        {
            errorMessage = $"candidate name variables are missing from scene: {string.Join(", ", missingCandidateNameVariables)}";
            return false;
        }

        var requiredCandidateCount = ResolveRequiredCandidateCount(template);
        if (requiredCandidateCount > 0 && validCandidates.Length < requiredCandidateCount)
        {
            warning = $"template-slot-count-{requiredCandidateCount}-with-{validCandidates.Length}-candidates";
        }

        if (!isWinnerTemplate && !IsCareerTemplate(template) && snapshot.BroadcastPhase == BroadcastPhase.Counting)
        {
            if (snapshot.CountedVotes <= 0 && snapshot.CountedRate <= 0)
            {
                errorMessage = "counted votes and counted rate are both zero";
                return false;
            }

            if (snapshot.CountedVotes > 0 && snapshot.CountedRate <= 0)
            {
                warning = JoinWarning(warning, "counted-rate-zero-with-positive-counted-votes");
            }

            if (!validCandidates.Any(candidate => candidate.VoteCount > 0 || candidate.VoteRate > 0))
            {
                errorMessage = "candidate vote data is empty";
                return false;
            }
        }

        if (template.RequiresImage && snapshot.Candidates.Any(candidate => !candidate.HasImage))
        {
            errorMessage = "candidate image is required but missing";
            return false;
        }

        errorMessage = string.Empty;
        return true;
    }

    private static IReadOnlyDictionary<string, KarismaSceneVariableDefinition> ResolveSceneVariablesForTemplate(
        KarismaSceneVariableCatalog sceneVariableCatalog,
        string t3CutPath,
        FormatTemplateDefinition template)
    {
        try
        {
            var resolvedScene = KarismaSceneResolver.ResolveScene(template, t3CutPath, useLoop: false);
            return sceneVariableCatalog.GetSceneVariables(t3CutPath, resolvedScene.Path);
        }
        catch
        {
            return new Dictionary<string, KarismaSceneVariableDefinition>(StringComparer.OrdinalIgnoreCase);
        }
    }

    private static bool RequiresTopTwoCandidateNameVariables(FormatTemplateDefinition template)
    {
        if (template.RecommendedChannel != BroadcastChannel.TopLeft)
        {
            return false;
        }

        return string.Equals(template.Name, "광역단체장_2인", StringComparison.Ordinal) ||
               string.Equals(template.Name, "광역단체장_2인_텍스트", StringComparison.Ordinal) ||
               string.Equals(template.Name, "기초단체장_2인", StringComparison.Ordinal) ||
               string.Equals(template.Name, "기초단체장_2인_텍스트", StringComparison.Ordinal);
    }

    private static bool HasCandidateNameVariables(
        IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables,
        int requiredSlotCount,
        out IReadOnlyList<string> missingVariables)
    {
        var missing = new List<string>();
        for (var slot = 1; slot <= requiredSlotCount; slot++)
        {
            var variableName = $"후보명{slot:00}";
            if (!sceneVariables.ContainsKey(variableName))
            {
                missing.Add(variableName);
            }
        }

        missingVariables = missing;
        return missing.Count == 0;
    }

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

    private static string JoinWarning(string current, string next)
    {
        if (string.IsNullOrWhiteSpace(current))
        {
            return next;
        }

        return $"{current}; {next}";
    }

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

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

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

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

    private static bool IsWinnerTemplate(string? source)
    {
        return GetTemplateBaseName(source).StartsWith("당선_", StringComparison.Ordinal);
    }

    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 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 = Path.GetFileNameWithoutExtension(
            source
                .Replace('/', Path.DirectorySeparatorChar)
                .Replace('\\', 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;
    }

    private static string ResolveScheduleElectionType(
        FormatTemplateDefinition template,
        BroadcastPhase phase,
        string defaultElectionType)
    {
        if (IsTopTurnoutDistrictBoardTemplate(template))
        {
            return "기초단체장";
        }

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

        return ResolveScheduleElectionType(template.Name, phase, defaultElectionType);
    }

    private static string ResolveScheduleElectionType(string? formatName, BroadcastPhase phase, string defaultElectionType)
    {
        var resolvedFormatName = formatName ?? string.Empty;
        if (ScheduleTemplatePolicy.IsHistoricalTurnoutFormat(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 (phase == BroadcastPhase.PreElection)
        {
            return SupportsPreElectionTurnout(defaultElectionType)
                ? defaultElectionType
                : "광역단체장";
        }

        return defaultElectionType;
    }

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

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

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

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

    private static IEnumerable<string> GetNormalizedRegionKeys(string? regionName)
    {
        var normalizedRegion = NormalizeRegion(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 static void PopulateDataFields(
        CurrentApiCutDiagnosticResult result,
        ElectionDataSnapshot snapshot,
        string sourcePath)
    {
        result.SourcePath = sourcePath;
        result.CandidateCount = snapshot.Candidates.Count;
        result.PositiveCandidateVoteCount = snapshot.Candidates.Count(candidate => candidate.VoteCount > 0 || candidate.VoteRate > 0);
        result.CountedVotes = snapshot.CountedVotes;
        result.CountedRate = snapshot.CountedRate;
        result.TurnoutVotes = snapshot.TurnoutVotes;
        result.TurnoutRate = snapshot.TurnoutRate;
        result.NationalTurnoutRate = snapshot.NationalTurnoutRate;
        result.Leader = snapshot.Candidates
            .OrderByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .FirstOrDefault()?.Name ?? string.Empty;
        result.Candidates = snapshot.Candidates
            .OrderByDescending(candidate => candidate.VoteCount)
            .ThenBy(candidate => candidate.Name, StringComparer.Ordinal)
            .Select((candidate, index) => new CurrentApiCandidateDiagnostic
            {
                Rank = index + 1,
                BallotNumber = candidate.BallotNumber,
                Name = candidate.Name,
                Party = candidate.Party,
                VoteCount = candidate.VoteCount,
                VoteRate = candidate.VoteRate,
                VoteRateDisplay = candidate.VoteRateDisplay,
                Judgement = candidate.EffectiveJudgement.ToString()
            })
            .ToList();
    }

    private static void WriteReports(
        CurrentApiCutDiagnosticsOptions options,
        IReadOnlyList<CurrentApiCutDiagnosticResult> results)
    {
        var jsonPath = Path.Combine(options.OutputPath, "current-api-cut-diagnostics.json");
        File.WriteAllText(
            jsonPath,
            JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));

        var summaryPath = Path.Combine(options.OutputPath, "summary.md");
        using var writer = new StreamWriter(summaryPath);
        writer.WriteLine("# Current API Cut Diagnostics");
        writer.WriteLine();
        writer.WriteLine($"- Phase: {options.Phase}");
        writer.WriteLine($"- Station: {(options.AllStations ? "ALL" : options.StationId)}");
        writer.WriteLine($"- Results: {results.Count}");
        writer.WriteLine();
        writer.WriteLine("## Status Counts");
        writer.WriteLine();
        foreach (var group in results.GroupBy(result => result.Status).OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
        {
            writer.WriteLine($"- {group.Key}: {group.Count()}");
        }

        var warningGroups = results
            .Where(result => !string.IsNullOrWhiteSpace(result.Warning))
            .GroupBy(result => result.Warning)
            .OrderByDescending(group => group.Count())
            .ToArray();
        if (warningGroups.Length > 0)
        {
            writer.WriteLine();
            writer.WriteLine("## Warning Counts");
            writer.WriteLine();
            foreach (var group in warningGroups)
            {
                writer.WriteLine($"- {group.Key}: {group.Count()}");
            }
        }

        var failures = results
            .Where(result => result.Status is "validation-failed" or "api-or-send-failed" or "no-target")
            .Take(60)
            .ToArray();
        if (failures.Length > 0)
        {
            writer.WriteLine();
            writer.WriteLine("## Failures");
            writer.WriteLine();
            foreach (var failure in failures)
            {
                writer.WriteLine($"- [{failure.Status}] {failure.Station} {failure.TemplateName} / {failure.Region}: {failure.Detail}");
            }
        }
    }

    private static void PrintSummary(IReadOnlyList<CurrentApiCutDiagnosticResult> results, string outputPath)
    {
        Console.WriteLine();
        Console.WriteLine("Summary");
        foreach (var group in results.GroupBy(result => result.Status).OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
        {
            Console.WriteLine($"- {group.Key}: {group.Count()}");
        }

        var warnings = results.Count(result => !string.IsNullOrWhiteSpace(result.Warning));
        Console.WriteLine($"- warnings: {warnings}");
        Console.WriteLine($"- report: {Path.Combine(outputPath, "summary.md")}");
    }

    private static string AppendWarning(string current, string warning)
    {
        if (string.IsNullOrWhiteSpace(current))
        {
            return warning;
        }

        return current + "; " + warning;
    }

    private static void WriteKarismaLog(CurrentApiCutDiagnosticsOptions options, LogService logService)
    {
        var logPath = Path.Combine(options.OutputPath, "karisma-log.txt");
        var lines = logService.Entries
            .Reverse()
            .Where(entry => entry is not null)
            .Select(entry => $"{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{entry.Level}] {entry.Message ?? string.Empty}");
        File.WriteAllLines(logPath, lines);
    }

    private readonly record struct PgmWindow(IntPtr Handle, Rect Bounds);

    private readonly record struct Rect(int Left, int Top, int Width, int Height);

    [StructLayout(LayoutKind.Sequential)]
    private readonly struct NativeRect
    {
        public int Left { get; init; }
        public int Top { get; init; }
        public int Right { get; init; }
        public int Bottom { get; init; }
    }

    private sealed class CurrentApiCutDiagnosticsOptions
    {
        public BroadcastPhase Phase { get; init; } = BroadcastPhase.Counting;

        public string StationId { get; init; } = "KNN";

        public bool AllStations { get; init; }

        public string RegionScope { get; init; } = "station";

        public int MaxRegions { get; init; }

        public bool IncludeVideoWall { get; init; }

        public int? TemplateLimit { get; init; }

        public string Filter { get; init; } = string.Empty;

        public string ExcludeFilter { get; init; } = string.Empty;

        public bool SimulateSend { get; init; } = true;

        public bool LiveSend { get; init; }

        public bool CaptureSceneImages { get; init; }

        public bool CaptureMixedPreviewImages { get; init; }

        public bool CapturePgmImages { get; init; }

        public IReadOnlyList<int> SceneCaptureFrames { get; init; } = Array.Empty<int>();

        public IReadOnlyList<int> MixedPreviewCaptureDelaysMs { get; init; } = Array.Empty<int>();

        public bool KeepOnAirBetweenSends { get; init; }

        public int OnAirDelayMs { get; init; } = 900;

        public int SendLimit { get; init; } = 24;

        public string ImageRootPath { get; init; } = TornadoPathResolver.GetDefaultT3CutPath();

        public string OutputPath { get; init; } = Path.Combine(
            "artifacts",
            "current-api-cut-diagnostics",
            DateTime.Now.ToString("yyyyMMdd_HHmmss"));

        public string DefaultElectionType { get; init; } = "광역단체장";

        public static CurrentApiCutDiagnosticsOptions Parse(string[] args)
        {
            var phase = BroadcastPhase.Counting;
            var stationId = "KNN";
            var allStations = false;
            var regionScope = "station";
            var maxRegions = 0;
            var includeVideoWall = false;
            int? templateLimit = null;
            var filter = string.Empty;
            var excludeFilter = string.Empty;
            var simulateSend = true;
            var liveSend = false;
            var captureSceneImages = false;
            var captureMixedPreviewImages = false;
            var capturePgmImages = false;
            IReadOnlyList<int> sceneCaptureFrames = Array.Empty<int>();
            IReadOnlyList<int> mixedPreviewCaptureDelaysMs = Array.Empty<int>();
            var keepOnAirBetweenSends = false;
            var onAirDelayMs = 900;
            var sendLimit = 24;
            var outputPath = Path.Combine(
                "artifacts",
                "current-api-cut-diagnostics",
                DateTime.Now.ToString("yyyyMMdd_HHmmss"));
            var defaultElectionType = "광역단체장";

            for (var index = 0; index < args.Length; index++)
            {
                var arg = args[index];
                string NextValue() => index + 1 < args.Length ? args[++index] : string.Empty;

                switch (arg.ToLowerInvariant())
                {
                    case "--phase":
                        phase = ParsePhase(NextValue());
                        break;
                    case "--station":
                        stationId = NextValue();
                        break;
                    case "--all-stations":
                        allStations = true;
                        break;
                    case "--region-scope":
                        regionScope = NextValue().ToLowerInvariant() == "all" ? "all" : "station";
                        break;
                    case "--max-regions":
                        int.TryParse(NextValue(), out maxRegions);
                        break;
                    case "--include-video-wall":
                        includeVideoWall = true;
                        break;
                    case "--limit":
                        if (int.TryParse(NextValue(), out var parsedLimit))
                        {
                            templateLimit = parsedLimit;
                        }
                        break;
                    case "--filter":
                        filter = NextValue();
                        break;
                    case "--exclude-filter":
                        excludeFilter = NextValue();
                        break;
                    case "--no-send":
                        simulateSend = false;
                        liveSend = false;
                        break;
                    case "--live-send":
                        simulateSend = true;
                        liveSend = true;
                        break;
                    case "--capture-scene-images":
                        captureSceneImages = true;
                        break;
                    case "--capture-scene-frames":
                        sceneCaptureFrames = ParseFrameList(NextValue());
                        captureSceneImages = true;
                        break;
                    case "--capture-mixed-preview-images":
                        captureMixedPreviewImages = true;
                        break;
                    case "--capture-mixed-preview-delays-ms":
                        mixedPreviewCaptureDelaysMs = ParseFrameList(NextValue());
                        captureMixedPreviewImages = true;
                        break;
                    case "--capture-pgm-images":
                        capturePgmImages = true;
                        break;
                    case "--keep-on-air-between-sends":
                        keepOnAirBetweenSends = true;
                        break;
                    case "--on-air-delay-ms":
                        if (int.TryParse(NextValue(), out var parsedOnAirDelayMs))
                        {
                            onAirDelayMs = Math.Max(0, parsedOnAirDelayMs);
                        }
                        break;
                    case "--send-limit":
                        if (int.TryParse(NextValue(), out var parsedSendLimit))
                        {
                            sendLimit = Math.Max(0, parsedSendLimit);
                        }
                        break;
                    case "--image-root":
                        _ = NextValue();
                        break;
                    case "--output":
                        outputPath = NextValue();
                        break;
                    case "--election-type":
                        defaultElectionType = NextValue();
                        break;
                }
            }

            return new CurrentApiCutDiagnosticsOptions
            {
                Phase = phase,
                StationId = stationId,
                AllStations = allStations,
                RegionScope = regionScope,
                MaxRegions = Math.Max(0, maxRegions),
                IncludeVideoWall = includeVideoWall,
                TemplateLimit = templateLimit,
                Filter = filter,
                ExcludeFilter = excludeFilter,
                SimulateSend = simulateSend,
                LiveSend = liveSend,
                CaptureSceneImages = captureSceneImages,
                CaptureMixedPreviewImages = captureMixedPreviewImages,
                CapturePgmImages = capturePgmImages,
                SceneCaptureFrames = sceneCaptureFrames,
                MixedPreviewCaptureDelaysMs = mixedPreviewCaptureDelaysMs,
                KeepOnAirBetweenSends = keepOnAirBetweenSends,
                OnAirDelayMs = onAirDelayMs,
                SendLimit = sendLimit,
                ImageRootPath = TornadoPathResolver.GetDefaultT3CutPath(),
                OutputPath = Path.GetFullPath(outputPath),
                DefaultElectionType = defaultElectionType
            };
        }

        private static BroadcastPhase ParsePhase(string value)
        {
            return value.ToLowerInvariant() switch
            {
                "pre" or "pre-election" or "preelection" => BroadcastPhase.PreElection,
                _ => BroadcastPhase.Counting
            };
        }

        private static IReadOnlyList<int> ParseFrameList(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return Array.Empty<int>();
            }

            return value
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                .Select(part => int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var frame)
                    ? frame
                    : (int?)null)
                .Where(frame => frame.HasValue)
                .Select(frame => frame!.Value)
                .Distinct()
                .ToArray();
        }
    }

    private sealed class CurrentApiCutDiagnosticResult
    {
        public string Station { get; set; } = string.Empty;

        public string Channel { get; set; } = string.Empty;

        public string TemplateId { get; set; } = string.Empty;

        public string TemplateName { get; set; } = string.Empty;

        public string Phase { get; set; } = string.Empty;

        public string ElectionType { get; set; } = string.Empty;

        public string Region { get; set; } = string.Empty;

        public string DistrictCode { get; set; } = string.Empty;

        public string Status { get; set; } = string.Empty;

        public string Detail { get; set; } = string.Empty;

        public string Warning { get; set; } = string.Empty;

        public string SourcePath { get; set; } = string.Empty;

        public string CapturePath { get; set; } = string.Empty;

        public string CaptureHash { get; set; } = string.Empty;

        public long CaptureBytes { get; set; }

        public string MixedPreviewCapturePath { get; set; } = string.Empty;

        public string MixedPreviewCaptureHash { get; set; } = string.Empty;

        public long MixedPreviewCaptureBytes { get; set; }

        public string PgmCapturePath { get; set; } = string.Empty;

        public string PgmCaptureHash { get; set; } = string.Empty;

        public long PgmCaptureBytes { get; set; }

        public int CandidateCount { get; set; }

        public int PositiveCandidateVoteCount { get; set; }

        public int CountedVotes { get; set; }

        public double CountedRate { get; set; }

        public int TurnoutVotes { get; set; }

        public double TurnoutRate { get; set; }

        public double NationalTurnoutRate { get; set; }

        public string Leader { get; set; } = string.Empty;

        public List<CurrentApiCandidateDiagnostic> Candidates { get; set; } = new List<CurrentApiCandidateDiagnostic>();

        public static CurrentApiCutDiagnosticResult NoTarget(
            BroadcastStationProfile station,
            FormatTemplateDefinition template,
            BroadcastPhase phase,
            string electionType)
        {
            return new CurrentApiCutDiagnosticResult
            {
                Station = station.Id,
                Channel = template.RecommendedChannel.ToString(),
                TemplateId = template.Id,
                TemplateName = template.Name,
                Phase = phase.ToString(),
                ElectionType = electionType,
                Status = "no-target",
                Detail = "no matching schedule regions"
            };
        }

        public static CurrentApiCutDiagnosticResult DistrictLoadFailed(
            BroadcastStationProfile station,
            FormatTemplateDefinition template,
            BroadcastPhase phase,
            string electionType,
            string detail)
        {
            return new CurrentApiCutDiagnosticResult
            {
                Station = station.Id,
                Channel = template.RecommendedChannel.ToString(),
                TemplateId = template.Id,
                TemplateName = template.Name,
                Phase = phase.ToString(),
                ElectionType = electionType,
                Status = "api-or-send-failed",
                Detail = detail
            };
        }
    }

    private sealed class CurrentApiCandidateDiagnostic
    {
        public int Rank { get; set; }

        public string BallotNumber { get; set; } = string.Empty;

        public string Name { get; set; } = string.Empty;

        public string Party { get; set; } = string.Empty;

        public int VoteCount { get; set; }

        public double VoteRate { get; set; }

        public string VoteRateDisplay { get; set; } = string.Empty;

        public string Judgement { get; set; } = string.Empty;
    }

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