using S5025ManualAudience =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.S5025ManualAudience;
using CoreMovingAverageSelection =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneMovingAverageSelection;
using CoreMovingAverageSource =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.MutableLegacySceneMovingAverageSelectionSource;
using CorePagePlanProvider =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.ILegacyScenePagePlanProvider;
using CorePlaylistEntry =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacyPlaylistEntry;
using CoreScenePaging =
    MBN_STOCK_WEBVIEW.Core.Playout.ScenePaging;
using CoreScenePageSize =
    MBN_STOCK_WEBVIEW.Core.Playout.ScenePageSize;
using CoreSceneSelection =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection;
using S5011DetailBranch =
    MBN_STOCK_WEBVIEW.Core.Playout.Scenes.S5011DetailBranch;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacySearchTrigger
{
    Button,
    Enter
}

public enum LegacyOperatorStatusKind
{
    Neutral,
    Information,
    Warning,
    Error
}

public enum LegacyPlaylistMoveDirection
{
    Up,
    Down
}

public enum LegacyDragDropPosition
{
    Before,
    After
}

public enum LegacyCutKeyboardKey
{
    ArrowUp,
    ArrowDown,
    Home,
    End,
    Space
}

public enum LegacyOperatorTabId
{
    Overseas,
    Exchange,
    Index,
    KospiIndustry,
    KosdaqIndustry,
    Comparison,
    Theme,
    OverseasStocks,
    Expert,
    TradingHalt
}

public sealed record LegacyStockResultRow(int Index, string DisplayName);

public sealed record LegacyCutViewRow(
    int PhysicalIndex,
    int? DisplayOrdinal,
    string RawLabel,
    bool IsSeparator,
    bool IsSelected,
    bool IsFocused = false);

public sealed record LegacyDialogState(
    string Message,
    string Caption = "MmoneyCoder",
    bool IsConfirmation = false);

public sealed record LegacyOperatorCommandResult(
    long Sequence,
    string Command,
    string TargetId,
    bool Succeeded);

public sealed record LegacyOperatorTabState(
    LegacyOperatorTabId Id,
    string Label,
    bool IsActive);

public sealed record LegacyFixedActionView(
    string Id,
    string Label,
    string Detail,
    bool IsAvailable,
    string Prerequisite);

public sealed record LegacyFixedSectionView(
    int Index,
    string Name,
    IReadOnlyList<LegacyFixedActionView> Actions);

public sealed record LegacyFixedCatalogView(
    LegacyFixedMarket Market,
    IReadOnlyList<LegacyFixedSectionView> Sections);

/// <summary>
/// A fixed UC1 parent-node activation is staged until every modal manual child has
/// been confirmed. The playlist remains unchanged while this view is present.
/// </summary>
public sealed record LegacyFixedSectionBatchView(
    int SectionIndex,
    string SectionName,
    int TotalActionCount,
    int StagedActionCount,
    int ManualStepNumber,
    int ManualStepCount,
    string PendingActionId,
    string PendingActionLabel);

public sealed record LegacyOperatorSnapshot(
    long Revision,
    string SearchText,
    IReadOnlyList<LegacyStockResultRow> SearchResults,
    int? SelectedStockIndex,
    IReadOnlyList<LegacyCutViewRow> CutRows,
    IReadOnlyList<LegacyOperatorPlaylistRow> Playlist,
    LegacyDialogState? Dialog,
    string StatusMessage,
    LegacyOperatorStatusKind StatusKind,
    IReadOnlyList<LegacyOperatorTabState>? Tabs = null,
    LegacyMovingAverageSelection MovingAverages = default,
    LegacyFixedCatalogView? FixedCatalog = null,
    LegacyIndustrySelectionSnapshot? Industry = null,
    LegacyThemeWorkflowSnapshot? Theme = null,
    LegacyExpertWorkflowSnapshot? Expert = null,
    LegacyTradingHaltWorkflowSnapshot? TradingHalt = null,
    LegacyComparisonWorkflowSnapshot? Comparison = null,
    LegacyManualFinancialWorkflowSnapshot? ManualFinancial = null,
    LegacyNamedPlaylistWorkflowSnapshot? NamedPlaylist = null,
    LegacyOverseasWorkflowSnapshot? Overseas = null,
    LegacyManualListsSnapshot? ManualLists = null,
    LegacyOperatorCatalogWorkflowSnapshot? OperatorCatalog = null,
    LegacyOperatorCommandResult? CommandResult = null,
    LegacyFixedSectionBatchView? FixedSectionBatch = null);

/// <summary>
/// Authoritative C# state for the first legacy operator vertical slice. The WebView sends
/// input intent only; it never derives stock identity, cut aliases or playlist fields.
/// </summary>
public sealed class LegacyOperatorController
{
    private const long DoubleClickWindowMilliseconds = 500;

    private readonly ILegacyStockLookup _stockLookup;
    private readonly IReadOnlyList<LegacyCutCatalogRow> _cutRows;
    private readonly LegacyFixedActionCatalog _fixedCatalog;
    private readonly TimeProvider _timeProvider;
    private readonly LegacyIndustrySelectionWorkflow? _industryWorkflow;
    private readonly LegacyThemeWorkflow? _themeWorkflow;
    private readonly LegacyExpertWorkflowController? _expertWorkflow;
    private readonly LegacyTradingHaltWorkflowController? _tradingHaltWorkflow;
    private readonly LegacyComparisonWorkflowController? _comparisonWorkflow;
    private readonly LegacyManualFinancialWorkflow? _manualFinancialWorkflow;
    private readonly LegacyNamedPlaylistWorkflowController? _namedPlaylistWorkflow;
    private readonly LegacyOverseasSelectionWorkflow? _overseasWorkflow;
    private readonly LegacyManualListsWorkflow? _manualListsWorkflow;
    private readonly LegacyOperatorCatalogWorkflowController? _operatorCatalogWorkflow;
    private readonly CorePagePlanProvider? _fixedPagePlanProvider;
    private readonly CoreMovingAverageSource _movingAverageSource = new(
        new CoreMovingAverageSelection(true, true));
    private readonly SemaphoreSlim _tabActivationGate = new(1, 1);
    private readonly SortedSet<int> _legacyPointerRows = [];
    private readonly SortedSet<int> _nativeSelectedCutRows = [];
    private readonly List<LegacyOperatorPlaylistRow> _playlist = [];
    private readonly HashSet<string> _selectedPlaylistRowIds = new(StringComparer.Ordinal);
    private readonly List<LegacyOperatorTabId> _tabOrder =
    [
        LegacyOperatorTabId.Overseas,
        LegacyOperatorTabId.Exchange,
        LegacyOperatorTabId.Index,
        LegacyOperatorTabId.KospiIndustry,
        LegacyOperatorTabId.KosdaqIndustry,
        LegacyOperatorTabId.Comparison,
        LegacyOperatorTabId.Theme,
        LegacyOperatorTabId.OverseasStocks,
        LegacyOperatorTabId.Expert,
        LegacyOperatorTabId.TradingHalt
    ];
    private Queue<int>? _pendingDropCutRows;
    private LegacyStockSearchData _searchData = new(string.Empty, [], []);
    private int? _selectedStockIndex;
    private int? _cutAnchorIndex;
    private int _nativeSelectionAnchorIndex;
    private int _nativeFocusedCutIndex = -1;
    private int? _previousPointerRow;
    private int _previousPointerX;
    private int _previousPointerY;
    private long? _previousPointerTimestamp;
    private string? _activePlaylistRowId;
    private int _playlistCursorIndex;
    private int _playlistPriorCursorIndex;
    private long _revision;
    private long _playlistSequence;
    private string _searchText = string.Empty;
    private string _statusMessage = string.Empty;
    private LegacyOperatorStatusKind _statusKind = LegacyOperatorStatusKind.Neutral;
    private LegacyDialogState? _dialog;
    private PendingNativeConfirmation? _pendingNativeConfirmation;
    private bool _closeOperatorCatalogAfterDialog;
    private LegacyOperatorTabId _activeTab = LegacyOperatorTabId.Overseas;
    private LegacyMovingAverageSelection _movingAverages = new(true, true);
    private LegacyThemeWorkflowSnapshot? _themeSnapshot;
    private LegacyManualFinancialWorkflowSnapshot? _manualFinancialSnapshot;
    private bool _industryActivationListLoaded;
    private bool _comparisonActivationListLoaded;
    private bool _themeActivationListLoaded;
    private bool _expertActivationListLoaded;
    private long _tabActivationRequest;
    private long _commandSequence;
    private LegacyOperatorCommandResult? _commandResult;
    private PendingFixedSectionBatch? _pendingFixedSectionBatch;

    public LegacyOperatorController(
        ILegacyStockLookup stockLookup,
        LegacyFixedActionCatalog? fixedCatalog = null,
        TimeProvider? timeProvider = null,
        LegacyIndustrySelectionWorkflow? industryWorkflow = null,
        LegacyThemeWorkflow? themeWorkflow = null,
        LegacyExpertWorkflowController? expertWorkflow = null,
        LegacyTradingHaltWorkflowController? tradingHaltWorkflow = null,
        LegacyComparisonWorkflowController? comparisonWorkflow = null,
        LegacyManualFinancialWorkflow? manualFinancialWorkflow = null,
        LegacyNamedPlaylistWorkflowController? namedPlaylistWorkflow = null,
        LegacyOverseasSelectionWorkflow? overseasWorkflow = null,
        LegacyManualListsWorkflow? manualListsWorkflow = null,
        LegacyOperatorCatalogWorkflowController? operatorCatalogWorkflow = null,
        IReadOnlyList<LegacyCutCatalogRow>? cutRows = null,
        CorePagePlanProvider? fixedPagePlanProvider = null)
    {
        _stockLookup = stockLookup ?? throw new ArgumentNullException(nameof(stockLookup));
        _cutRows = CreateValidatedCutRows(cutRows);
        _fixedCatalog = fixedCatalog ?? LegacyFixedActionCatalog.Default;
        _timeProvider = timeProvider ?? TimeProvider.System;
        _industryWorkflow = industryWorkflow;
        _themeWorkflow = themeWorkflow;
        _themeSnapshot = themeWorkflow?.CreateInitial(CurrentThemeNxtSession());
        _expertWorkflow = expertWorkflow;
        _tradingHaltWorkflow = tradingHaltWorkflow;
        _comparisonWorkflow = comparisonWorkflow;
        _manualFinancialWorkflow = manualFinancialWorkflow;
        _namedPlaylistWorkflow = namedPlaylistWorkflow;
        _overseasWorkflow = overseasWorkflow;
        _manualListsWorkflow = manualListsWorkflow;
        _operatorCatalogWorkflow = operatorCatalogWorkflow;
        _fixedPagePlanProvider = fixedPagePlanProvider;
        _tradingHaltWorkflow?.SetMovingAverages(
            _movingAverages.Ma5,
            _movingAverages.Ma20);
        _overseasWorkflow?.SetMovingAverages(
            _movingAverages.Ma5,
            _movingAverages.Ma20);
    }

    public LegacyOperatorSnapshot Current => CreateSnapshot();

    public LegacyOperatorPlayoutRequest CreatePlayoutRequest() =>
        CreatePlayoutRequestCore(globalFadeDuration: null);

    public LegacyOperatorPlayoutRequest CreatePlayoutRequest(int globalFadeDuration)
    {
        if (globalFadeDuration is < 0 or > 19)
        {
            throw new ArgumentOutOfRangeException(
                nameof(globalFadeDuration),
                "The original DissolveTime index must be between 0 and 19.");
        }

        return CreatePlayoutRequestCore(globalFadeDuration);
    }

    private LegacyOperatorPlayoutRequest CreatePlayoutRequestCore(
        int? globalFadeDuration)
    {
        if (_playlist.Count == 0)
        {
            throw new InvalidOperationException("플레이리스트에 송출할 항목이 없습니다.");
        }

        var selectedIndex = _activePlaylistRowId is null
            ? -1
            : FindPlaylistIndex(_activePlaylistRowId);
        if (selectedIndex < 0)
        {
            throw new InvalidOperationException(
                "플레이리스트의 F8 대상 행이 선택되지 않았습니다.");
        }

        var entries = _playlist.Select(row =>
        {
            var entry = row.ToPlayoutEntry();
            if (MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneMovingAverageOverlay
                .Supports(entry.CutCode))
            {
                // Keep the row's seven persisted fields unchanged. The retained source
                // is sampled again by the workflow whenever s8010 is rebuilt.
                entry = entry with { MovingAverageSelectionSource = _movingAverageSource };
                entry = MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneMovingAverageOverlay
                    .ApplyCurrent(entry);
            }

            return globalFadeDuration.HasValue
                ? entry with { FadeDuration = globalFadeDuration.Value }
                : entry;
        }).ToArray();
        return new LegacyOperatorPlayoutRequest(
            Array.AsReadOnly(entries),
            selectedIndex);
    }

    /// <summary>
    /// Reflects a successfully prepared or played cue back into the operator grid.
    /// MainForm stored the current PageN value in column 5 instead of keeping it only
    /// in the Tornado workflow. TAKE/PREPARE may move the F8 candidate when a disabled
    /// row is skipped, but never rewrites bufdel/m_delcnt or the Shift anchors. NEXT
    /// deliberately leaves even that independent candidate alone.
    /// </summary>
    public LegacyOperatorSnapshot ReflectSuccessfulPlayout(
        string entryId,
        int pageIndexZeroBased,
        int pageCount,
        bool synchronizeOperatorSelection)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(entryId);
        if (pageCount <= 0 || pageIndexZeroBased < 0 || pageIndexZeroBased >= pageCount)
        {
            throw new ArgumentOutOfRangeException(
                nameof(pageIndexZeroBased),
                "The playout page position is outside the playlist page range.");
        }

        var index = FindPlaylistIndex(entryId);
        if (index < 0)
        {
            return CreateSnapshot();
        }

        var changed = false;
        var pageText = $"{pageIndexZeroBased + 1}/{pageCount}";
        if (!string.Equals(_playlist[index].PageText, pageText, StringComparison.Ordinal))
        {
            _playlist[index] = _playlist[index] with { PageText = pageText };
            changed = true;
        }

        if (synchronizeOperatorSelection)
        {
            var rowId = _playlist[index].RowId;
            changed |= !string.Equals(_activePlaylistRowId, rowId, StringComparison.Ordinal);
            _activePlaylistRowId = rowId;
        }

        if (changed)
        {
            Touch();
        }

        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchStocksAsync(
        string text,
        LegacySearchTrigger trigger,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(text);
        _searchText = text;

        // button9_Click returned before calling searchStock only for an exactly empty string.
        // txtSearch_KeyPress did not have that guard, so blank Enter still queries all rows.
        if (trigger == LegacySearchTrigger.Button && text.Length == 0)
        {
            Touch();
            return CreateSnapshot();
        }

        _searchData = await _stockLookup.SearchAsync(text, cancellationToken).ConfigureAwait(false);
        _selectedStockIndex = null;
        _statusMessage = string.Empty;
        _statusKind = LegacyOperatorStatusKind.Neutral;
        _dialog = null;
        _pendingDropCutRows = null;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectStock(int resultIndex)
    {
        if (resultIndex < 0 || resultIndex >= _searchData.DisplayNames.Count)
        {
            return CreateSnapshot();
        }

        _selectedStockIndex = resultIndex;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot CutPointerDown(
        int physicalIndex,
        bool control,
        bool shift,
        int x,
        int y,
        long timestampMilliseconds,
        bool allowAppend = true)
    {
        if (!allowAppend)
        {
            // Selection remains available while a scene is prepared or on air, but
            // an on-air click must neither activate a cut nor prime the next IDLE
            // click as the second half of a legacy pointer double-click.
            _previousPointerTimestamp = null;
        }

        if (_selectedStockIndex is null || !IsCutIndex(physicalIndex))
        {
            return CreateSnapshot();
        }

        var gap = _previousPointerTimestamp is long previousTimestamp
            ? timestampMilliseconds - previousTimestamp
            : long.MaxValue;
        if (allowAppend &&
            _previousPointerTimestamp is not null &&
            gap >= 0 && gap < DoubleClickWindowMilliseconds &&
            _previousPointerRow == physicalIndex &&
            _previousPointerX == x &&
            _previousPointerY == y &&
            _legacyPointerRows.Count == 1)
        {
            TryAppendCut(_legacyPointerRows.Min);
            _previousPointerTimestamp = null;
        }
        else
        {
            _previousPointerTimestamp = allowAppend ? timestampMilliseconds : null;
        }

        _previousPointerRow = physicalIndex;
        _previousPointerX = x;
        _previousPointerY = y;

        ApplyLegacyPointerSelection(physicalIndex, control, shift);
        ApplyNativeListSelection(physicalIndex, control, shift);

        _cutAnchorIndex = physicalIndex;
        _nativeFocusedCutIndex = physicalIndex;
        // Win32 ListView's selection mark remains the fixed anchor across
        // consecutive Shift/Ctrl+Shift gestures. Only a non-Shift click moves it.
        if (!shift)
        {
            _nativeSelectionAnchorIndex = physicalIndex;
        }
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot CutKeyDown(
        LegacyCutKeyboardKey key,
        bool control,
        bool shift)
    {
        if (_selectedStockIndex is null || _cutRows.Count == 0)
        {
            return CreateSnapshot();
        }

        var focusedIndex = IsCutIndex(_nativeFocusedCutIndex)
            ? _nativeFocusedCutIndex
            : 0;
        var targetIndex = key switch
        {
            LegacyCutKeyboardKey.ArrowUp => Math.Max(0, focusedIndex - 1),
            LegacyCutKeyboardKey.ArrowDown => Math.Min(_cutRows.Count - 1, focusedIndex + 1),
            LegacyCutKeyboardKey.Home => 0,
            LegacyCutKeyboardKey.End => _cutRows.Count - 1,
            LegacyCutKeyboardKey.Space => focusedIndex,
            _ => throw new ArgumentOutOfRangeException(nameof(key), key, null)
        };

        _nativeFocusedCutIndex = targetIndex;
        if (key == LegacyCutKeyboardKey.Space)
        {
            ApplyNativeSpaceSelection(targetIndex, control, shift);
        }
        else
        {
            ApplyNativeKeyboardNavigationSelection(targetIndex, control, shift);
        }

        // MainForm's selectedRowsList was populated only by listView1_MouseDown.
        // Keyboard selection belongs exclusively to the native ListView state used
        // by owner drawing and PlayList_DragDrop, so do not alter the legacy list.
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DropSelectedCutsOnPlaylist()
    {
        if (_selectedStockIndex is null || _dialog is not null)
        {
            return CreateSnapshot();
        }

        _pendingDropCutRows = new Queue<int>(_nativeSelectedCutRows);
        ContinuePendingDrop();

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ClearCutSelection()
    {
        if (_selectedStockIndex is null)
        {
            return CreateSnapshot();
        }

        var changed = _legacyPointerRows.Count != 0 ||
            _nativeSelectedCutRows.Count != 0 ||
            _cutAnchorIndex != -1 ||
            _previousPointerTimestamp is not null;
        _legacyPointerRows.Clear();
        _nativeSelectedCutRows.Clear();
        _cutAnchorIndex = -1;
        // A Win32 ListView blank-area click clears Selected items but preserves
        // its selection mark, so a following Shift gesture keeps the old anchor.
        _previousPointerTimestamp = null;
        _previousPointerRow = -1;
        if (changed)
        {
            Touch();
        }
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SetPlaylistRowEnabled(string rowId, bool enabled)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        var index = _playlist.FindIndex(row =>
            string.Equals(row.RowId, rowId, StringComparison.Ordinal));
        if (index >= 0)
        {
            _playlist[index] = _playlist[index] with { IsEnabled = enabled };
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot TogglePlaylistRowEnabled(string rowId)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        var index = FindPlaylistIndex(rowId);
        if (index >= 0)
        {
            _playlist[index] = _playlist[index] with
            {
                IsEnabled = !_playlist[index].IsEnabled
            };
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SetAllPlaylistRowsEnabled(bool enabled)
    {
        if (_playlist.Count == 0)
        {
            return CreateSnapshot();
        }

        for (var index = 0; index < _playlist.Count; index++)
        {
            _playlist[index] = _playlist[index] with { IsEnabled = enabled };
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectTab(LegacyOperatorTabId tabId)
    {
        if (_pendingFixedSectionBatch is not null && _activeTab != tabId)
        {
            return ReportError(
                "고정 컷 섹션의 수동 입력은 원본 모달 창과 같이 완료하거나 취소하기 전까지 탭을 바꿀 수 없습니다.");
        }

        if (!_tabOrder.Contains(tabId) || _activeTab == tabId)
        {
            return CreateSnapshot();
        }

        BeginTabActivation(tabId);
        _activeTab = tabId;
        Interlocked.Increment(ref _tabActivationRequest);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SelectTabAsync(
        LegacyOperatorTabId tabId,
        CancellationToken cancellationToken = default)
    {
        SelectTab(tabId);
        var activationRequest = Volatile.Read(ref _tabActivationRequest);
        await _tabActivationGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (!IsCurrentTabActivation(tabId, activationRequest))
            {
                return CreateSnapshot();
            }

            var market = GetActiveIndustryMarket();
            var touched = false;
            if (_industryWorkflow is not null && market is not null &&
                !_industryActivationListLoaded)
            {
                await _industryWorkflow.LoadMarketAsync(market.Value, cancellationToken)
                    .ConfigureAwait(false);
                if (!IsCurrentTabActivation(tabId, activationRequest))
                {
                    return CreateSnapshot();
                }

                _industryActivationListLoaded = true;
                touched = true;
            }

            if (IsComparisonActive() && _comparisonWorkflow is not null &&
                !_comparisonActivationListLoaded)
            {
                await _comparisonWorkflow.LoadSavedPairsAsync(cancellationToken)
                    .ConfigureAwait(false);
                if (!IsCurrentTabActivation(tabId, activationRequest))
                {
                    return CreateSnapshot();
                }

                _comparisonActivationListLoaded = true;
                touched = true;
            }

            // Legacy UC4_Load starts a one-shot timer whose first tick calls
            // GetThemeByKeyword with the initial empty query. Keep that load in
            // native authority so Web code cannot decide when or what to query.
            if (IsThemeActive() && _themeWorkflow is not null &&
                _themeSnapshot is not null && !_themeActivationListLoaded)
            {
                var loaded = await _themeWorkflow.SearchAsync(
                    _themeSnapshot,
                    string.Empty,
                    CurrentThemeNxtSession(),
                    cancellationToken).ConfigureAwait(false);
                if (!IsCurrentTabActivation(tabId, activationRequest))
                {
                    return CreateSnapshot();
                }

                _themeSnapshot = loaded;
                _themeActivationListLoaded = true;
                touched = true;
            }

            // Legacy UC6_Load calls Data_Load immediately, which loads the full
            // EXPERT_LIST. An empty native search is its parameterized equivalent.
            if (IsExpertActive() && _expertWorkflow is not null &&
                !_expertActivationListLoaded)
            {
                await _expertWorkflow.SearchAsync(string.Empty, cancellationToken)
                    .ConfigureAwait(false);
                if (!IsCurrentTabActivation(tabId, activationRequest))
                {
                    return CreateSnapshot();
                }

                _expertActivationListLoaded = true;
                touched = true;
            }

            if (touched)
            {
                Touch();
            }

            return CreateSnapshot();
        }
        finally
        {
            _tabActivationGate.Release();
        }
    }

    public LegacyOperatorSnapshot SwapTabs(
        LegacyOperatorTabId source,
        LegacyOperatorTabId target)
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "고정 컷 섹션의 수동 입력을 완료하거나 취소하기 전에는 탭 순서를 바꿀 수 없습니다.");
        }

        var sourceIndex = _tabOrder.IndexOf(source);
        var targetIndex = _tabOrder.IndexOf(target);
        if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
        {
            return CreateSnapshot();
        }

        (_tabOrder[sourceIndex], _tabOrder[targetIndex]) =
            (_tabOrder[targetIndex], _tabOrder[sourceIndex]);
        if (_activeTab != source)
        {
            Interlocked.Increment(ref _tabActivationRequest);
            BeginTabActivation(source);
        }

        _activeTab = source;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SwapTabsAsync(
        LegacyOperatorTabId source,
        LegacyOperatorTabId target,
        CancellationToken cancellationToken = default)
    {
        if (!_tabOrder.Contains(source) || !_tabOrder.Contains(target) || source == target)
        {
            return SwapTabs(source, target);
        }

        SwapTabs(source, target);
        return await SelectTabAsync(source, cancellationToken).ConfigureAwait(false);
    }

    public Task<LegacyOperatorSnapshot> SwapTabsAtExpectedPositionsAsync(
        LegacyOperatorTabId source,
        LegacyOperatorTabId target,
        int expectedSourceIndex,
        int expectedTargetIndex,
        CancellationToken cancellationToken = default)
    {
        if (source == target ||
            expectedSourceIndex < 0 || expectedSourceIndex >= _tabOrder.Count ||
            expectedTargetIndex < 0 || expectedTargetIndex >= _tabOrder.Count ||
            expectedSourceIndex == expectedTargetIndex ||
            _tabOrder[expectedSourceIndex] != source ||
            _tabOrder[expectedTargetIndex] != target)
        {
            return Task.FromResult(CreateSnapshot());
        }

        // The expected positions make a repeated dragover message idempotent. Once
        // the first message swaps the two tabs, an identical delivery no longer
        // matches the C#-owned order and therefore cannot swap them back.
        return SwapTabsAsync(source, target, cancellationToken);
    }

    public LegacyOperatorSnapshot SetMovingAverages(bool ma5, bool ma20)
    {
        var next = new LegacyMovingAverageSelection(ma5, ma20);
        if (_movingAverages == next)
        {
            return CreateSnapshot();
        }

        _movingAverageSource.Update(new CoreMovingAverageSelection(ma5, ma20));
        _movingAverages = next;
        _tradingHaltWorkflow?.SetMovingAverages(ma5, ma20);
        _overseasWorkflow?.SetMovingAverages(ma5, ma20);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> OpenOperatorCatalogAsync(
        LegacyOperatorCatalogEntity entity,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.OpenAsync(entity, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> BeginThemeCatalogFromUc4Async(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.OpenAsync(
            LegacyOperatorCatalogEntity.Theme,
            cancellationToken).ConfigureAwait(false);

        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> EditSelectedThemeCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        var identity = _themeSnapshot?.SelectedIdentity;
        if (identity is null)
        {
            return ReportError("편집할 테마를 먼저 선택하세요.");
        }

        await _operatorCatalogWorkflow.OpenThemeForEditAsync(identity, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public Task<LegacyOperatorSnapshot> DeleteSelectedThemeCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (_operatorCatalogWorkflow is null)
        {
            return Task.FromResult(ReportError("ThemeA 관리 기능이 구성되지 않았습니다."));
        }

        var identity = _themeSnapshot?.SelectedIdentity;
        if (identity is null)
        {
            return Task.FromResult(ReportError("삭제할 테마를 먼저 선택하세요."));
        }

        if (_dialog is not null)
        {
            return Task.FromResult(CreateSnapshot());
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForTheme(identity);
        _dialog = new LegacyDialogState(
            "선택한 테마를 삭제하겠습니까?",
            IsConfirmation: true);
        Touch();
        return Task.FromResult(CreateSnapshot());
    }

    public async Task<LegacyOperatorSnapshot> BeginExpertCatalogFromUc6Async(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.OpenAsync(
            LegacyOperatorCatalogEntity.Expert,
            cancellationToken).ConfigureAwait(false);
        if (_operatorCatalogWorkflow.Current.CanBeginCreate)
        {
            await _operatorCatalogWorkflow.BeginCreateExpertAsync(cancellationToken)
                .ConfigureAwait(false);
        }

        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> EditSelectedExpertCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("EList 관리 기능이 구성되지 않았습니다.");
        }

        var selected = _expertWorkflow?.Current.SelectedExpert;
        if (selected is null)
        {
            return ReportError("편집할 전문가를 먼저 선택하세요.");
        }

        await _operatorCatalogWorkflow.OpenExpertForEditAsync(
            new MMoneyCoderSharp.Data.ExpertSelectionIdentity(
                selected.ExpertCode,
                selected.ExpertName),
            cancellationToken).ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public Task<LegacyOperatorSnapshot> DeleteSelectedExpertCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (_operatorCatalogWorkflow is null)
        {
            return Task.FromResult(ReportError("EList 관리 기능이 구성되지 않았습니다."));
        }

        var selected = _expertWorkflow?.Current.SelectedExpert;
        if (selected is null)
        {
            return Task.FromResult(ReportError("삭제할 전문가를 먼저 선택하세요."));
        }

        if (_dialog is not null)
        {
            return Task.FromResult(CreateSnapshot());
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForExpert(
            new MMoneyCoderSharp.Data.ExpertSelectionIdentity(
                selected.ExpertCode,
                selected.ExpertName));
        _dialog = new LegacyDialogState(
            "선택한 전문가를 삭제하겠습니까?",
            IsConfirmation: true);
        Touch();
        return Task.FromResult(CreateSnapshot());
    }

    public LegacyOperatorSnapshot CloseOperatorCatalog()
    {
        if (_operatorCatalogWorkflow is null)
        {
            return CreateSnapshot();
        }

        _operatorCatalogWorkflow.Close();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> CloseOperatorCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return CreateSnapshot();
        }

        var closing = _operatorCatalogWorkflow.Current;
        _operatorCatalogWorkflow.Close();
        _statusMessage = string.Empty;
        _statusKind = LegacyOperatorStatusKind.Neutral;
        if (closing.IsOpen &&
            closing.Entity == LegacyOperatorCatalogEntity.Expert &&
            closing.Mode == LegacyOperatorCatalogMode.Create &&
            _expertWorkflow is not null)
        {
            try
            {
                await _expertWorkflow.SearchAsync(string.Empty, cancellationToken)
                    .ConfigureAwait(false);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                throw;
            }
            catch
            {
                _statusMessage = "전문가 목록을 새로 읽지 못했습니다. 다시 조회하세요.";
                _statusKind = LegacyOperatorStatusKind.Warning;
            }
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchOperatorCatalogAsync(
        string query,
        MMoneyCoderSharp.Data.ThemeSession nxtSession,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.SearchAsync(query, nxtSession, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SelectOperatorCatalogResultAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.SelectAsync(resultId, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> BeginCreateThemeCatalogAsync(
        MMoneyCoderSharp.Data.ThemeMarket market,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.BeginCreateThemeAsync(market, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> ChangeCreateThemeCatalogMarketAsync(
        MMoneyCoderSharp.Data.ThemeMarket market,
        string editorName,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.ChangeCreateThemeMarketAsync(
            market,
            editorName,
            cancellationToken).ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> BeginCreateExpertCatalogAsync(
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.BeginCreateExpertAsync(cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchOperatorCatalogStocksAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.SearchStocksAsync(query, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectOperatorCatalogStock(string resultId)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.SelectStock(resultId);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot AddOperatorCatalogStock(decimal? buyAmount)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.AddSelectedStock(buyAmount);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot MoveOperatorCatalogDraftRow(
        string rowId,
        LegacyOperatorCatalogMoveDirection direction)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.MoveDraftRow(rowId, direction);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ReorderOperatorCatalogDraftRow(
        string sourceRowId,
        string targetRowId,
        LegacyDragDropPosition position)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        var beforeRevision = _operatorCatalogWorkflow.Current.Revision;
        var reordered = _operatorCatalogWorkflow.ReorderDraftRow(
            sourceRowId,
            targetRowId,
            position);
        if (reordered.Revision == beforeRevision)
        {
            return CreateSnapshot();
        }

        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SetOperatorCatalogDraftBuyAmount(
        string rowId,
        decimal? buyAmount)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.SetDraftBuyAmount(rowId, buyAmount);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ActivateOperatorCatalogStock(
        string resultId,
        decimal? buyAmount)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.SelectStock(resultId);
        if (string.Equals(
                _operatorCatalogWorkflow.Current.SelectedStockResultId,
                resultId,
                StringComparison.Ordinal))
        {
            _operatorCatalogWorkflow.AddSelectedStock(buyAmount);
        }

        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot RemoveOperatorCatalogDraftRow(string rowId)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.RemoveDraftRow(rowId);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectOperatorCatalogDraftRow(string rowId)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.SelectDraftRow(rowId);
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot RemoveActiveOperatorCatalogDraftRow()
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        _operatorCatalogWorkflow.RemoveActiveDraftRow();
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ClearOperatorCatalogDraftRows()
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        var catalog = _operatorCatalogWorkflow.Current;
        if (!catalog.IsOpen || catalog.Entity != LegacyOperatorCatalogEntity.Theme ||
            catalog.Mode is not (LegacyOperatorCatalogMode.Create or
                LegacyOperatorCatalogMode.Edit))
        {
            return ReportError("ThemeA 편집 목록이 열려 있지 않습니다.");
        }

        if (catalog.DraftRows.Count == 0 || _dialog is not null)
        {
            return CreateSnapshot();
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForThemeDraftClear(
            catalog.Revision);
        _dialog = new LegacyDialogState(
            "모두 삭제하겠습니까?",
            IsConfirmation: true);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> CheckOperatorCatalogThemeNameAsync(
        string editorName,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        await _operatorCatalogWorkflow.CheckThemeNameAvailabilityAsync(
            editorName,
            cancellationToken).ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        var availability = _operatorCatalogWorkflow.Current.ThemeNameAvailability;
        if (availability is LegacyThemeNameAvailability.Available or
            LegacyThemeNameAvailability.Duplicate)
        {
            _dialog = new LegacyDialogState(_operatorCatalogWorkflow.Current.StatusMessage);
        }
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SaveOperatorCatalogAsync(
        string editorName,
        CancellationToken cancellationToken = default)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
        }

        var before = _operatorCatalogWorkflow.Current;
        var selectedExpertId = before.Entity == LegacyOperatorCatalogEntity.Expert &&
            before.Mode == LegacyOperatorCatalogMode.Edit
                ? _expertWorkflow?.Current.SelectedExpert?.SelectionId
                : null;
        if (before.Entity == LegacyOperatorCatalogEntity.Expert &&
            before.Mode == LegacyOperatorCatalogMode.Create &&
            string.IsNullOrWhiteSpace(editorName))
        {
            _dialog = new LegacyDialogState("이름을 입력하세요");
            Touch();
            return CreateSnapshot();
        }

        await _operatorCatalogWorkflow.SaveAsync(editorName, cancellationToken)
            .ConfigureAwait(false);
        ApplyOperatorCatalogStatus();
        var after = _operatorCatalogWorkflow.Current;

        if (before.Entity == LegacyOperatorCatalogEntity.Theme)
        {
            if (after.Mode == LegacyOperatorCatalogMode.List)
            {
                _operatorCatalogWorkflow.Close();
                _statusMessage = string.Empty;
                _statusKind = LegacyOperatorStatusKind.Neutral;
            }
            else if (string.Equals(
                         after.StatusMessage,
                         LegacyOperatorCatalogWorkflowController.ThemeNameDuplicateMessage,
                         StringComparison.Ordinal))
            {
                _dialog = new LegacyDialogState(
                    LegacyOperatorCatalogWorkflowController.ThemeNameDuplicateMessage);
            }
        }
        else if (after.Mode == LegacyOperatorCatalogMode.List)
        {
            var refreshFailed = false;
            if (before.Mode == LegacyOperatorCatalogMode.Create)
            {
                _operatorCatalogWorkflow.Close();
                try
                {
                    if (_expertWorkflow is not null)
                    {
                        await _expertWorkflow.SearchAsync(string.Empty, cancellationToken)
                            .ConfigureAwait(false);
                    }
                }
                catch
                {
                    // The mutation receipt is already known-committed. A UC6 list refresh
                    // failure must not be reclassified as an unknown DB-write outcome.
                    _statusMessage = "전문가 목록을 새로 읽지 못했습니다. 다시 조회하세요.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                    refreshFailed = true;
                }
            }
            else if (before.Mode == LegacyOperatorCatalogMode.Edit)
            {
                try
                {
                    if (_expertWorkflow is not null && selectedExpertId is not null)
                    {
                        await _expertWorkflow.SelectExpertAsync(
                                selectedExpertId,
                                cancellationToken)
                            .ConfigureAwait(false);
                    }
                }
                catch
                {
                    // The expert transaction is known-committed. Preview refresh failure
                    // is a read failure and must never turn into an unknown write outcome.
                    refreshFailed = true;
                    _statusMessage = "저장된 추천 종목을 다시 읽지 못했습니다. 전문가를 다시 선택하세요.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                }
                _dialog = new LegacyDialogState("저장되었습니다");
                _closeOperatorCatalogAfterDialog = true;
            }

            if (!refreshFailed)
            {
                _statusMessage = string.Empty;
                _statusKind = LegacyOperatorStatusKind.Neutral;
            }
        }
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot InvalidateOperatorCatalogBrowserCorrelation()
    {
        if (_operatorCatalogWorkflow is null)
        {
            return CreateSnapshot();
        }

        _operatorCatalogWorkflow.InvalidateBrowserCorrelation();
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> OpenManualFinancialAsync(
        MMoneyCoderSharp.Data.ManualFinancialScreenKind screen,
        CancellationToken cancellationToken = default)
    {
        if (_manualFinancialWorkflow is null)
        {
            return ReportError("수동 재무 입력 기능이 구성되지 않았습니다.");
        }

        _manualFinancialSnapshot = _manualFinancialWorkflow.CreateInitial(screen);
        _manualFinancialSnapshot = await _manualFinancialWorkflow.SearchAsync(
            _manualFinancialSnapshot,
            string.Empty,
            cancellationToken).ConfigureAwait(false);
        ApplyManualFinancialListStatus(_manualFinancialSnapshot);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot CloseManualFinancial()
    {
        if (_manualFinancialSnapshot is null)
        {
            return CreateSnapshot();
        }

        _manualFinancialSnapshot = null;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchManualFinancialAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        _manualFinancialSnapshot = await workflow.SearchAsync(
            snapshot,
            query,
            cancellationToken).ConfigureAwait(false);
        ApplyManualFinancialListStatus(_manualFinancialSnapshot);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ToggleManualFinancialNameSort(long expectedRevision)
    {
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("Open a manual-financial screen before sorting its list.");
        }

        try
        {
            _manualFinancialSnapshot = workflow.ToggleNameSort(
                snapshot,
                expectedRevision);
            _statusMessage = "The GraphE stock-name order was changed.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            _statusMessage = exception.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> FindManualFinancialAsync(
        string query,
        LegacyManualFinancialFindDirection direction,
        long expectedRevision,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("Open a manual-financial screen before finding a stock.");
        }

        try
        {
            _manualFinancialSnapshot = workflow.FindByName(
                snapshot,
                query,
                direction,
                expectedRevision);
            if (_manualFinancialSnapshot.LastMessage is not null)
            {
                _statusMessage = _manualFinancialSnapshot.LastMessage;
                _statusKind = LegacyOperatorStatusKind.Warning;
                Touch();
                return CreateSnapshot();
            }

            return await LoadManualFinancialAsync(
                _manualFinancialSnapshot.SelectedRowId!,
                cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException or KeyNotFoundException or
            LegacyManualFinancialWorkflowDataException)
        {
            // A stale browser generation or a failed fresh detail read leaves the
            // GraphE modal open. The operator may inspect the failure; no read is retried.
            _dialog = new LegacyDialogState(exception.Message);
            _statusMessage = exception.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> LoadManualFinancialAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        // Commit the attempted row selection and clear any previous detail before
        // the strict exact read. If a malformed historical row fails, an earlier
        // valid record can never remain eligible for save, delete, or playout.
        _manualFinancialSnapshot = workflow.BeginLoad(snapshot, resultId);
        try
        {
            _manualFinancialSnapshot = await workflow.LoadAsync(
                _manualFinancialSnapshot,
                resultId,
                cancellationToken).ConfigureAwait(false);
        }
        catch (Exception exception) when (exception is ManualFinancialDataException or
            ArgumentException or InvalidOperationException or KeyNotFoundException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            _statusMessage = exception.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            Touch();
            return CreateSnapshot();
        }

        // GraphE identities contain STOCK_NAME only. Resolve the exact stock master
        // immediately after the fresh detail read when it is unambiguous; otherwise
        // leave the candidates visible so the operator can resolve them explicitly.
        var selectedIdentity = _manualFinancialSnapshot.SelectedRawRecord?.Identity ??
            _manualFinancialSnapshot.SelectedRecord?.Identity;
        if (selectedIdentity is null)
        {
            _dialog = new LegacyDialogState(
                "선택한 GraphE 행의 종목 식별정보를 읽을 수 없습니다.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            Touch();
            return CreateSnapshot();
        }

        var stockName = selectedIdentity.StockName;
        _manualFinancialSnapshot = await workflow.SearchStocksAsync(
            _manualFinancialSnapshot,
            stockName,
            cancellationToken).ConfigureAwait(false);
        var exact = _manualFinancialSnapshot.StockCandidates
            .Where(row => string.Equals(row.Name, stockName, StringComparison.Ordinal))
            .ToArray();
        if (exact.Length == 1)
        {
            try
            {
                _manualFinancialSnapshot = workflow.SelectStock(
                    _manualFinancialSnapshot,
                    exact[0].ResultId);
            }
            catch (ArgumentException)
            {
                // Ambiguous provider identities remain unverified and visible.
            }
        }


        if (_manualFinancialSnapshot.WritesQuarantined)
        {
            _statusMessage = _manualFinancialSnapshot.LastMessage ??
                "GraphE 저장 작업은 현재 차단되어 있습니다.";
            _statusKind = LegacyOperatorStatusKind.Warning;
        }
        else if (!_manualFinancialSnapshot.IsPlayoutReady)
        {
            _statusMessage =
                "기존 GraphE 저장값을 원문 그대로 불러왔습니다. 편집·삭제와 스케줄 추가는 가능하며 실제 송출 전 장면 검증에서 다시 확인합니다.";
            _statusKind = LegacyOperatorStatusKind.Warning;
        }
        else
        {
            _statusMessage =
                $"{stockName} {_manualFinancialSnapshot.Definition.Label} 데이터를 불러왔습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> ActivateManualFinancialResultAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        const string command = "activate-manual-financial-result";
        if (!TryGetManualFinancial(out _, out _))
        {
            CompleteCommand(command, resultId, succeeded: false);
            return ReportError("Open a manual-financial screen before activating a result.");
        }

        try
        {
            // A GraphE list identity is only an opaque handle. Re-read and validate the
            // exact record and stock-master identity before deriving the playlist row.
            await LoadManualFinancialAsync(resultId, cancellationToken).ConfigureAwait(false);
            if (!TryGetManualFinancial(out var workflow, out var snapshot))
            {
                throw new InvalidOperationException(
                    "The manual-financial result is no longer available.");
            }
            if ((!snapshot.IsRawDetailFresh && !snapshot.IsDetailFresh) ||
                snapshot.SelectedIdentity is null ||
                !string.Equals(snapshot.SelectedRowId, resultId, StringComparison.Ordinal))
            {
                throw new LegacyManualFinancialWorkflowDataException(
                    "선택한 기존 GraphE 행의 저장값을 읽을 수 없습니다.");
            }

            var draft = workflow.MaterializePlaylistDraft(
                snapshot,
                snapshot.Definition.ActionId);
            AppendManualFinancialRow(draft);
            _manualFinancialSnapshot = null;
            _statusMessage = $"{draft.Title} {draft.Detail} cut was added.";
            _statusKind = LegacyOperatorStatusKind.Information;
            CompleteCommand(command, resultId, succeeded: true);
            Touch();
            return CreateSnapshot();
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException or KeyNotFoundException or
            LegacyManualFinancialWorkflowDataException)
        {
            // Keep the GraphE modal open and never append a partial row.
            _dialog = new LegacyDialogState(exception.Message);
            _statusMessage = exception.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            CompleteCommand(command, resultId, succeeded: false);
            Touch();
            return CreateSnapshot();
        }
    }

    public LegacyOperatorSnapshot BeginManualFinancialCreate()
    {
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        _manualFinancialSnapshot = workflow.BeginCreate(snapshot);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchManualFinancialStocksAsync(
        string stockName,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(stockName);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        // GraphE's search button and Enter handler both returned without any
        // state or focus change when the stock-name box was empty.
        if (string.IsNullOrWhiteSpace(stockName))
        {
            return CreateSnapshot();
        }

        _manualFinancialSnapshot = await workflow.SearchStocksAsync(
            snapshot,
            stockName,
            cancellationToken).ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectManualFinancialStock(string resultId)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        if (string.IsNullOrWhiteSpace(resultId))
        {
            return CreateSnapshot();
        }

        try
        {
            _manualFinancialSnapshot = workflow.SelectStock(snapshot, resultId);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> SaveManualFinancialAsync(
        MMoneyCoderSharp.Data.ManualFinancialRecord record,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(record);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        try
        {
            LegacyManualFinancialWriteRequest request = snapshot.SelectedRecord is null
                ? workflow.CreateCreateRequest(snapshot, record)
                : workflow.CreateUpdateRequest(snapshot, record);
            _manualFinancialSnapshot = await workflow.ExecuteAsync(
                snapshot,
                request,
                cancellationToken).ConfigureAwait(false);
            ApplyManualFinancialResultStatus(_manualFinancialSnapshot, "저장");
            ShowManualFinancialSaveDialog(_manualFinancialSnapshot);
            Touch();
            return CreateSnapshot();
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> SaveManualFinancialRawAsync(
        MMoneyCoderSharp.Data.ManualFinancialRawRecord record,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(record);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        try
        {
            var request = workflow.CreateRawUpdateRequest(snapshot, record);
            _manualFinancialSnapshot = await workflow.ExecuteAsync(
                snapshot,
                request,
                cancellationToken).ConfigureAwait(false);
            ApplyManualFinancialResultStatus(_manualFinancialSnapshot, "저장");
            ShowManualFinancialSaveDialog(_manualFinancialSnapshot);
            Touch();
            return CreateSnapshot();
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public Task<LegacyOperatorSnapshot> DeleteManualFinancialAsync(
        bool all,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return Task.FromResult(ReportError("수동 재무 입력 화면을 먼저 여세요."));
        }

        if (_dialog is not null)
        {
            return Task.FromResult(CreateSnapshot());
        }

        // GraphE returned immediately when no row had been selected; it did not
        // show either the Yes/No question or a follow-up error MessageBox.
        if (!all && snapshot.SelectedRowId is null)
        {
            return Task.FromResult(CreateSnapshot());
        }

        try
        {
            // Validate the current selection/write authority before showing the
            // native-compatible question, but do not cross the write boundary.
            _ = all
                ? workflow.CreateDeleteAllRequest(
                    snapshot,
                    snapshot.Definition.DeleteAllConfirmationToken)
                : workflow.CreateDeleteRequest(snapshot);
            _pendingNativeConfirmation = PendingNativeConfirmation.ForManualFinancialDelete(
                all,
                snapshot.Revision);
            _dialog = new LegacyDialogState(
                all ? "모든 종목을 삭제하겠습니까?" : "선택한 종목을 삭제하겠습니까?",
                IsConfirmation: true);
            Touch();
            return Task.FromResult(CreateSnapshot());
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return Task.FromResult(CreateSnapshot());
        }
    }

    public LegacyOperatorSnapshot ActivateManualFinancialAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!TryGetManualFinancial(out var workflow, out var snapshot))
        {
            return ReportError("수동 재무 입력 화면을 먼저 여세요.");
        }

        try
        {
            var draft = workflow.MaterializePlaylistDraft(snapshot, actionId);
            AppendManualFinancialRow(draft);
            _statusMessage = $"{draft.Title} {draft.Detail} 컷을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> OpenManualNetSellAsync(
        MBN_STOCK_WEBVIEW.Core.Playout.Scenes.S5025ManualAudience audience,
        CancellationToken cancellationToken = default)
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "고정 컷 섹션이 요청한 순서대로 현재 수동 입력을 완료하거나 취소하세요.");
        }

        return await OpenManualNetSellCoreAsync(audience, cancellationToken)
            .ConfigureAwait(false);
    }

    private async Task<LegacyOperatorSnapshot> OpenManualNetSellCoreAsync(
        MBN_STOCK_WEBVIEW.Core.Playout.Scenes.S5025ManualAudience audience,
        CancellationToken cancellationToken)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 순매도 입력 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.OpenNetSellAsync(audience, cancellationToken)
            .ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> OpenManualViAsync(
        CancellationToken cancellationToken = default)
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "고정 컷 섹션이 요청한 순서대로 현재 수동 입력을 완료하거나 취소하세요.");
        }

        return await OpenManualViCoreAsync(cancellationToken).ConfigureAwait(false);
    }

    private async Task<LegacyOperatorSnapshot> OpenManualViCoreAsync(
        CancellationToken cancellationToken)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.OpenViAsync(cancellationToken).ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot CloseManualLists()
    {
        _manualListsWorkflow?.Close();
        if (_pendingFixedSectionBatch is { } pending)
        {
            _pendingFixedSectionBatch = null;
            _statusMessage =
                $"{pending.SectionName} 섹션 추가를 취소했습니다. 편성표에는 아무 항목도 추가하지 않았습니다. 이미 확인한 수동 저장 내용은 유지됩니다.";
            _statusKind = LegacyOperatorStatusKind.Warning;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> RefreshManualListsAsync(
        CancellationToken cancellationToken = default)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 목록 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.RefreshAsync(cancellationToken).ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SetManualNetSellCell(
        string rowId,
        LegacyManualNetSellField field,
        string value)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 순매도 입력 기능을 사용할 수 없습니다.");
        }

        _manualListsWorkflow.SetNetSellCell(rowId, field, value);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SaveManualNetSellAsync(
        CancellationToken cancellationToken = default)
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "섹션 수동 입력은 확인 명령으로만 저장하고 다음 단계로 진행할 수 있습니다.");
        }

        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 순매도 입력 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.SaveNetSellAsync(cancellationToken).ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot AddManualNetSellPlaylistRow()
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "섹션 수동 입력 중에는 개별 컷을 편성표에 추가할 수 없습니다.");
        }

        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 순매도 입력 기능을 사용할 수 없습니다.");
        }

        try
        {
            AppendManualListRow(_manualListsWorkflow.CreateNetSellPlaylistDraft());
            _statusMessage = "검증된 수동 순매도 컷을 편성표에 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchManualViAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.SearchViAsync(query, cancellationToken)
            .ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectManualViSearchResult(string resultId)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("The manual VI list feature is unavailable.");
        }

        _manualListsWorkflow.SelectViSearchResult(resultId);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot AddManualViSearchResult(string resultId)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        _manualListsWorkflow.AddViSearchResult(resultId);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot MoveManualViItem(
        string itemId,
        LegacyManualListMoveDirection direction)
    {
        _manualListsWorkflow?.MoveViItem(itemId, direction);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DeleteManualViItem(string itemId)
    {
        _manualListsWorkflow?.DeleteViItem(itemId);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DeleteAllManualViItems()
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        var manualLists = _manualListsWorkflow.Current;
        if (!manualLists.IsOpen || manualLists.Screen != LegacyManualListScreen.Vi)
        {
            return ReportError("VI 수동 목록 화면을 먼저 여세요.");
        }

        if (_dialog is not null)
        {
            return CreateSnapshot();
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForManualViDeleteAll(
            manualLists.ViItems.Select(static item => item.ItemId).ToArray());
        _dialog = new LegacyDialogState("모두 삭제하겠습니까?", IsConfirmation: true);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SaveManualViAsync(
        CancellationToken cancellationToken = default)
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "섹션 수동 입력은 확인 명령으로만 저장하고 다음 단계로 진행할 수 있습니다.");
        }

        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.SaveViAsync(cancellationToken).ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot AddManualViPlaylistRow()
    {
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "섹션 수동 입력 중에는 개별 컷을 편성표에 추가할 수 없습니다.");
        }

        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        try
        {
            AppendManualListRow(_manualListsWorkflow.CreateViPlaylistDraft());
            _statusMessage = "검증된 VI 발동 컷을 편성표에 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
        }

        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Reproduces the original FSell/VI OK button: persist once, verify the fresh
    /// readback, append the corresponding cut, and close the editor as one operator
    /// action. A failed or outcome-unknown save leaves the editor open and never
    /// attempts playlist materialization or an automatic retry.
    /// </summary>
    public async Task<LegacyOperatorSnapshot> ConfirmManualListsAsync(
        CancellationToken cancellationToken = default)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("수동 목록 기능을 사용할 수 없습니다.");
        }

        var screen = _manualListsWorkflow.Current.Screen;
        if (screen is null)
        {
            return ReportError("열려 있는 수동 목록 입력창이 없습니다.");
        }

        if (_pendingFixedSectionBatch is { } pending &&
            (GetActiveFixedMarket() != pending.Market ||
                !pending.IsCurrentManual(
                    screen.Value,
                    _manualListsWorkflow.Current.Audience)))
        {
            return ReportError(
                "고정 컷 섹션의 현재 수동 입력 단계와 열린 입력창이 일치하지 않아 저장하지 않았습니다.");
        }

        if (screen == LegacyManualListScreen.NetSell)
        {
            await _manualListsWorkflow.SaveNetSellAsync(cancellationToken)
                .ConfigureAwait(false);
        }
        else
        {
            await _manualListsWorkflow.SaveViAsync(cancellationToken)
                .ConfigureAwait(false);
        }

        var saved = _manualListsWorkflow.Current;
        if (!saved.CanMaterializePlaylist)
        {
            ApplyManualListsStatus();
            Touch();
            return CreateSnapshot();
        }

        try
        {
            var draft = screen == LegacyManualListScreen.NetSell
                ? _manualListsWorkflow.CreateNetSellPlaylistDraft()
                : _manualListsWorkflow.CreateViPlaylistDraft();

            if (_pendingFixedSectionBatch is not null)
            {
                _pendingFixedSectionBatch.StageCurrentManual(draft);
                _manualListsWorkflow.Close();
                return await ContinuePendingFixedSectionAsync(cancellationToken)
                    .ConfigureAwait(false);
            }

            AppendManualListRow(draft);
            _manualListsWorkflow.Close();
            _statusMessage = screen == LegacyManualListScreen.NetSell
                ? "수동 순매도 목록을 저장하고 편성표에 추가했습니다."
                : "VI 발동 목록을 저장하고 편성표에 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> ImportLegacyManualListsAsync(
        CancellationToken cancellationToken = default)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("원본 수동 데이터 가져오기 기능을 사용할 수 없습니다.");
        }

        await _manualListsWorkflow.ImportAsync(cancellationToken).ConfigureAwait(false);
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ActivateFixedAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "고정 컷 섹션의 수동 입력을 완료하거나 취소한 뒤 다른 항목을 선택하세요.");
        }

        var market = GetActiveFixedMarket();
        if (market is null)
        {
            return ReportError("현재 탭에서는 고정 컷을 선택할 수 없습니다.");
        }

        try
        {
            var materialized = _fixedCatalog.Materialize(
                actionId,
                _timeProvider.GetLocalNow());
            if (materialized.Action.Market != market.Value)
            {
                return ReportError("현재 탭에 속하지 않는 항목입니다.");
            }

            var initialPage = CreateFixedInitialPageFallback(materialized.Action);
            AppendFixedRow(materialized, initialPage.PageText);
            ApplyFixedActionAddedStatus(materialized.Action, initialPage.PageCountUnavailable);
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("등록되지 않은 고정 컷 항목입니다.");
        }
    }

    public async Task<LegacyOperatorSnapshot> ActivateFixedActionAsync(
        string actionId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "고정 컷 섹션의 수동 입력을 완료하거나 취소한 뒤 다른 항목을 선택하세요.");
        }

        var manualTarget = GetFixedManualTarget(actionId);

        if (manualTarget is null)
        {
            var fixedMarket = GetActiveFixedMarket();
            if (fixedMarket is null)
            {
                return ReportError("현재 탭에서는 고정 컷을 선택할 수 없습니다.");
            }

            try
            {
                var materialized = _fixedCatalog.Materialize(
                    actionId,
                    _timeProvider.GetLocalNow());
                if (materialized.Action.Market != fixedMarket.Value)
                {
                    return ReportError("현재 탭에 속하지 않는 항목입니다.");
                }

                var initialPage = await ResolveFixedInitialPageAsync(
                    materialized,
                    cancellationToken).ConfigureAwait(false);
                AppendFixedRow(materialized, initialPage.PageText);
                ApplyFixedActionAddedStatus(
                    materialized.Action,
                    initialPage.PageCountUnavailable);
                Touch();
                return CreateSnapshot();
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (InvalidOperationException exception)
            {
                _dialog = new LegacyDialogState(exception.Message);
                Touch();
                return CreateSnapshot();
            }
            catch (KeyNotFoundException)
            {
                return ReportError("등록되지 않은 고정 컷 항목입니다.");
            }
        }

        var market = GetActiveFixedMarket();
        if (market != LegacyFixedMarket.Index)
        {
            return ReportError("현재 탭에 속하지 않는 수동 입력 항목입니다.");
        }

        return manualTarget.Value.Screen == LegacyManualListScreen.NetSell
            ? await OpenManualNetSellAsync(
                manualTarget.Value.Audience,
                cancellationToken).ConfigureAwait(false)
            : await OpenManualViAsync(cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// Preserves UC1 parent-node direct-child order while closing its manual-child gap:
    /// the original parent path removed FSell rows and returned at VI, because the
    /// ShowDialog calls lived only in the leaf path. Automatic children are materialized
    /// into a native staging area first; FSell/VI children are opened one at a time and
    /// the playlist is committed only after every manual child has been confirmed.
    /// </summary>
    public async Task<LegacyOperatorSnapshot> ActivateFixedSection(
        int sectionIndex,
        CancellationToken cancellationToken = default)
    {
        var market = GetActiveFixedMarket();
        if (market is null)
        {
            return ReportError("현재 탭에서는 고정 컷 섹션을 선택할 수 없습니다.");
        }

        var sections = _fixedCatalog.GetSections(market.Value);
        if (sectionIndex < 0 || sectionIndex >= sections.Count)
        {
            return ReportError("등록되지 않은 고정 컷 섹션입니다.");
        }

        if (_pendingFixedSectionBatch is not null)
        {
            return ReportError(
                "이미 고정 컷 섹션의 수동 입력을 진행하고 있습니다. 먼저 완료하거나 취소하세요.");
        }

        if (_manualListsWorkflow?.Current.IsOpen == true)
        {
            return ReportError("열려 있는 수동 입력창을 먼저 완료하거나 닫으세요.");
        }

        var section = sections[sectionIndex];
        try
        {
            var localNow = _timeProvider.GetLocalNow();
            var items = new List<PendingFixedSectionItem>(section.Actions.Count);
            foreach (var action in section.Actions)
            {
                var manualTarget = GetFixedManualTarget(action.Id);
                if (manualTarget is not null)
                {
                    if (_manualListsWorkflow is null)
                    {
                        return ReportError(
                            "이 섹션에는 수동 입력 항목이 있지만 수동 목록 기능을 사용할 수 없습니다.");
                    }

                    items.Add(new PendingFixedSectionItem(action, manualTarget.Value));
                    continue;
                }

                // Every non-manual child is validated before the first playlist row
                // is appended or the first modal editor is opened.
                var materialized = _fixedCatalog.Materialize(
                    action.Id,
                    localNow);
                var initialPage = await ResolveFixedInitialPageAsync(
                    materialized,
                    cancellationToken).ConfigureAwait(false);
                items.Add(new PendingFixedSectionItem(
                    action,
                    materialized,
                    initialPage));
            }

            var manualCount = items.Count(item => item.ManualTarget is not null);
            if (manualCount == 0)
            {
                CommitFixedSectionBatch(items);
                ApplyFixedSectionAddedStatus(section.Name, items);
                Touch();
                return CreateSnapshot();
            }

            _pendingFixedSectionBatch = new PendingFixedSectionBatch(
                market.Value,
                sectionIndex,
                section.Name,
                items,
                manualCount);
            return await OpenPendingFixedSectionManualAsync(cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (InvalidOperationException)
        {
            _pendingFixedSectionBatch = null;
            return ReportError("섹션의 모든 항목을 검증하지 못해 아무것도 추가하지 않았습니다.");
        }
    }

    public LegacyOperatorSnapshot SelectIndustry(int index, bool doubleClick)
    {
        if (!IsActiveIndustryReady())
        {
            return ReportError("업종 목록을 먼저 불러오세요.");
        }

        if (doubleClick)
        {
            _industryWorkflow!.DoubleClickIndustry(index);
        }
        else
        {
            _industryWorkflow!.SelectIndustry(index);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SwapIndustries()
    {
        if (!IsActiveIndustryReady())
        {
            return ReportError("업종 목록을 먼저 불러오세요.");
        }

        _industryWorkflow!.SwapIndustries();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SetIndustryComparisonText(
        LegacyIndustryComparisonSlot slot,
        string text)
    {
        ArgumentNullException.ThrowIfNull(text);
        if (!IsActiveIndustryReady())
        {
            return ReportError("업종 목록을 먼저 불러오세요.");
        }

        try
        {
            _industryWorkflow!.SetComparisonText(slot, text);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 업종 비교 입력칸입니다.");
        }
        catch (LegacyIndustryWorkflowException exception)
        {
            return ReportError(exception.Message);
        }
    }

    public LegacyOperatorSnapshot ActivateIndustryAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsActiveIndustryReady())
        {
            return ReportError("업종 목록을 먼저 불러오세요.");
        }

        try
        {
            var draft = _industryWorkflow!.MaterializePlaylistDraft(actionId);
            AppendIndustryRow(draft, _industryWorkflow.Current.Industries.Count);
            _statusMessage = $"{draft.Detail} 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (LegacyIndustryWorkflowException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public LegacyOperatorSnapshot ActivateIndustrySection(int sectionIndex)
    {
        if (!IsActiveIndustryReady())
        {
            return ReportError("업종 목록을 먼저 불러오세요.");
        }

        // MainForm constructs both industry UC1 instances with UC3 == null. The
        // original root double-click path dereferences UC3 before its 22-child loop,
        // then its outer catch silently consumes the exception. Preserve that
        // observable no-op; only child double-clicks add industry playlist rows.
        return sectionIndex == 0
            ? CreateSnapshot()
            : ReportError("등록되지 않은 업종 컷 섹션입니다.");
    }

    public LegacyOperatorSnapshot SelectOverseasFixedIndex(string targetId)
    {
        ArgumentNullException.ThrowIfNull(targetId);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        try
        {
            var selected = _overseasWorkflow.SelectFixedIndex(targetId).SelectedFixedIndex!;
            _statusMessage = $"{selected.DisplayName} 지수를 선택했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("등록되지 않은 해외지수입니다.");
        }
    }

    public async Task<LegacyOperatorSnapshot> SearchOverseasIndustriesAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        var overseas = await _overseasWorkflow.SearchIndustriesAsync(
            query,
            cancellationToken: cancellationToken).ConfigureAwait(false);
        _statusMessage = $"해외업종 {overseas.Industry.ResultCount}건을 찾았습니다.";
        _statusKind = LegacyOperatorStatusKind.Information;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchOverseasStocksAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        var overseas = await _overseasWorkflow.SearchStocksAsync(
            query,
            cancellationToken: cancellationToken).ConfigureAwait(false);
        _statusMessage = $"해외종목 {overseas.Stock.ResultCount}건을 찾았습니다.";
        _statusKind = LegacyOperatorStatusKind.Information;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectOverseasIndustry(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        try
        {
            var selected = _overseasWorkflow.SelectIndustry(selectionId).Industry.Selected!;
            _statusMessage = $"{selected.KoreanName} 업종을 선택했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("현재 검색 결과에 없는 해외업종입니다.");
        }
    }

    public LegacyOperatorSnapshot SelectOverseasStock(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        try
        {
            var selected = _overseasWorkflow.SelectStock(selectionId).Stock.Selected!;
            _statusMessage = $"{selected.InputName} 종목을 선택했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("현재 검색 결과에 없는 해외종목입니다.");
        }
    }

    public LegacyOperatorSnapshot ActivateOverseasAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsOverseasActive() || _overseasWorkflow is null)
        {
            return ReportError("해외종목 탭을 먼저 선택하세요.");
        }

        try
        {
            var draft = _overseasWorkflow.MaterializePlaylistDraft(actionId);
            AppendOverseasRow(draft);
            _statusMessage = $"{draft.Title} 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (LegacyOverseasWorkflowException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyOperatorSnapshot> SearchComparisonDomesticAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        await _comparisonWorkflow.SearchDomesticAsync(query, cancellationToken)
            .ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SearchComparisonWorldAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        await _comparisonWorkflow.SearchWorldAsync(query, cancellationToken)
            .ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectComparisonDomesticResult(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("Select the comparison tab first.");
        }

        _comparisonWorkflow.SelectDomesticResult(selectionId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectComparisonWorldResult(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("Select the comparison tab first.");
        }

        _comparisonWorkflow.SelectWorldResult(selectionId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectComparisonFixedTarget(string targetId)
    {
        ArgumentNullException.ThrowIfNull(targetId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("Select the comparison tab first.");
        }

        _comparisonWorkflow.SelectFixedTarget(targetId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ChooseComparisonDomesticResult(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.ChooseDomesticResult(selectionId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ChooseComparisonWorldResult(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.ChooseWorldResult(selectionId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ChooseComparisonFixedTarget(string targetId)
    {
        ArgumentNullException.ThrowIfNull(targetId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.ChooseFixedTarget(targetId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SwapComparisonTargets()
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.SwapDraftTargets();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ClearComparisonTargets()
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.ClearDraftTargets();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> AddComparisonPairAsync(
        CancellationToken cancellationToken = default)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        try
        {
            await _comparisonWorkflow.AddDraftPairAsync(cancellationToken)
                .ConfigureAwait(false);
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public LegacyOperatorSnapshot SelectComparisonPair(string rowId)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        _comparisonWorkflow.SelectSavedPair(rowId);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> MoveComparisonPairAsync(
        LegacyComparisonPairMoveDirection direction,
        CancellationToken cancellationToken = default)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        await _comparisonWorkflow.MoveSelectedPairAsync(direction, cancellationToken)
            .ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> DeleteComparisonPairAsync(
        bool all,
        CancellationToken cancellationToken = default)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        if (all)
        {
            if (_dialog is not null)
            {
                return CreateSnapshot();
            }

            _pendingNativeConfirmation = PendingNativeConfirmation.ForComparisonDeleteAll(
                _comparisonWorkflow.Current.Revision);
            _dialog = new LegacyDialogState(
                "모두 삭제하겠습니까?",
                "MmoneyCoder",
                IsConfirmation: true);
        }
        else
        {
            await _comparisonWorkflow.DeleteSelectedPairAsync(cancellationToken)
                .ConfigureAwait(false);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot RequestLegacyComparisonPairImport()
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 메뉴를 먼저 선택하세요.");
        }

        if (!_comparisonWorkflow.CanImportLegacyPairs)
        {
            _dialog = new LegacyDialogState(
                "원본 비교쌍 가져오기는 현재 데이터베이스 구성에서 사용할 수 없습니다.");
            Touch();
            return CreateSnapshot();
        }

        if (_dialog is not null)
        {
            return CreateSnapshot();
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForComparisonImport(
            _comparisonWorkflow.Current.Revision);
        _dialog = new LegacyDialogState(
            "현재 저장된 비교쌍의 순서는 그대로 유지하고, 원본 프로그램의 비교쌍 중 없는 항목만 뒤에 추가할까요?",
            "MmoneyCoder",
            IsConfirmation: true);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ActivateComparisonAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        try
        {
            var draft = _comparisonWorkflow.MaterializeSelectedAction(actionId);
            AppendComparisonRow(draft);
            _statusMessage = $"{draft.Detail} 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 비교 컷 항목입니다.");
        }
    }

    public LegacyOperatorSnapshot ActivateComparisonSection(int sectionIndex)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null)
        {
            return ReportError("비교 탭을 먼저 선택하세요.");
        }

        var snapshot = _comparisonWorkflow.Current;
        var sections = snapshot.Actions
            .Select(static action => action.Section)
            .Distinct(StringComparer.Ordinal)
            .ToArray();
        if (sectionIndex < 0 || sectionIndex >= sections.Length)
        {
            return ReportError("등록되지 않은 비교 컷 섹션입니다.");
        }

        var section = sections[sectionIndex];
        var selectedPair = snapshot.SavedPairs.FirstOrDefault(static pair => pair.IsSelected);
        if (string.Equals(section, "종목별 수익률 비교", StringComparison.Ordinal) &&
            selectedPair is not null &&
            (selectedPair.First.Kind == LegacyComparisonTargetKind.NxtStock ||
             selectedPair.Second.Kind == LegacyComparisonTargetKind.NxtStock))
        {
            _dialog = new LegacyDialogState(
                "NXT 종목이 포함된 비교쌍에는 종목별 수익률 비교 섹션을 추가할 수 없습니다.");
            Touch();
            return CreateSnapshot();
        }

        var actions = snapshot.Actions
            .Where(action => string.Equals(
                action.Section,
                section,
                StringComparison.Ordinal))
            .ToArray();
        if (actions.Length == 0 || actions.Any(static action => !action.IsAvailable))
        {
            _dialog = new LegacyDialogState(
                "섹션의 모든 항목을 검증하지 못해 아무것도 추가하지 않았습니다.");
            Touch();
            return CreateSnapshot();
        }

        try
        {
            var drafts = actions
                .Select(action => _comparisonWorkflow.MaterializeSelectedAction(action.ActionId))
                .ToArray();
            foreach (var draft in drafts)
            {
                AppendComparisonRow(draft);
            }

            _statusMessage = $"{section} 항목 {drafts.Length}개를 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(
                $"섹션의 모든 항목을 검증하지 못해 아무것도 추가하지 않았습니다. {exception.Message}");
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 비교 컷 항목입니다.");
        }
    }

    public Task<LegacyOperatorSnapshot> SearchThemesAsync(
        string query,
        CancellationToken cancellationToken = default) =>
        SearchThemesCoreAsync(query, CurrentThemeNxtSession(), cancellationToken);

    // Compatibility overload for native callers. UC4 does not allow a WebView
    // payload to select the NXT session; TimeProvider remains authoritative.
    public Task<LegacyOperatorSnapshot> SearchThemesAsync(
        string query,
        MMoneyCoderSharp.Data.ThemeSession ignoredNxtSession,
        CancellationToken cancellationToken = default) =>
        SearchThemesAsync(query, cancellationToken);

    private async Task<LegacyOperatorSnapshot> SearchThemesCoreAsync(
        string query,
        MMoneyCoderSharp.Data.ThemeSession nxtSession,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsThemeActive() || _themeWorkflow is null || _themeSnapshot is null)
        {
            return ReportError("테마 탭을 먼저 선택하세요.");
        }

        _themeSnapshot = await _themeWorkflow.SearchAsync(
            _themeSnapshot,
            query,
            nxtSession,
            cancellationToken).ConfigureAwait(false);
        _themeActivationListLoaded = true;
        _statusMessage = _themeSnapshot.SearchIsTruncated
            ? $"테마 검색 결과가 최대 {LegacyThemeWorkflow.MaximumSearchResults:N0}건으로 제한되었습니다. 검색어를 좁혀 주세요."
            : $"테마 {_themeSnapshot.SearchResults.Count:N0}건을 찾았습니다.";
        _statusKind = _themeSnapshot.SearchIsTruncated
            ? LegacyOperatorStatusKind.Warning
            : LegacyOperatorStatusKind.Information;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SelectThemeAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        if (!IsThemeActive() || _themeWorkflow is null || _themeSnapshot is null)
        {
            return ReportError("테마 탭을 먼저 선택하세요.");
        }

        try
        {
            _themeSnapshot = await _themeWorkflow.SelectAsync(
                _themeSnapshot,
                resultId,
                cancellationToken).ConfigureAwait(false);
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("등록되지 않은 테마 검색 결과입니다.");
        }
    }

    public async Task<LegacyOperatorSnapshot> ActivateThemeResultAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        const string command = "activate-theme-result";
        const string originalDoubleClickAction = "five-row-current";
        if (!IsThemeActive() || _themeWorkflow is null || _themeSnapshot is null)
        {
            CompleteCommand(command, resultId, succeeded: false);
            return ReportError("Select the theme tab before activating a result.");
        }

        try
        {
            // The original row double-click first selected the row, then emitted the
            // fixed five-row current-price cut. Await preview loading so those stages
            // cannot race each other.
            _themeSnapshot = await _themeWorkflow.SelectAsync(
                _themeSnapshot,
                resultId,
                cancellationToken).ConfigureAwait(false);
            var draft = _themeWorkflow.MaterializePlaylistDraft(
                _themeSnapshot,
                originalDoubleClickAction);
            AppendThemeRow(draft);
            _statusMessage = $"{draft.Detail} item was added.";
            _statusKind = LegacyOperatorStatusKind.Information;
            CompleteCommand(command, resultId, succeeded: true);
            Touch();
            return CreateSnapshot();
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException or KeyNotFoundException or
            LegacyThemeWorkflowDataException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            _statusMessage = exception.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
            CompleteCommand(command, resultId, succeeded: false);
            Touch();
            return CreateSnapshot();
        }
    }

    public LegacyOperatorSnapshot SetThemeSort(string sortId)
    {
        ArgumentNullException.ThrowIfNull(sortId);
        if (!IsThemeActive() || _themeWorkflow is null || _themeSnapshot is null)
        {
            return ReportError("테마 탭을 먼저 선택하세요.");
        }

        _themeSnapshot = _themeWorkflow.SelectSort(_themeSnapshot, sortId);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ActivateThemeAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsThemeActive() || _themeWorkflow is null || _themeSnapshot is null)
        {
            return ReportError("테마 탭을 먼저 선택하세요.");
        }

        try
        {
            var draft = _themeWorkflow.MaterializePlaylistDraft(_themeSnapshot, actionId);
            AppendThemeRow(draft);
            _statusMessage = $"{draft.Detail} 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
        catch (KeyNotFoundException)
        {
            return ReportError("등록되지 않은 테마 컷 항목입니다.");
        }
    }

    public async Task<LegacyOperatorSnapshot> SearchExpertsAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsExpertActive() || _expertWorkflow is null)
        {
            return ReportError("전문가 탭을 먼저 선택하세요.");
        }

        await _expertWorkflow.SearchAsync(query, cancellationToken).ConfigureAwait(false);
        _expertActivationListLoaded = true;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SelectExpertAsync(
        string selectionId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsExpertActive() || _expertWorkflow is null)
        {
            return ReportError("전문가 탭을 먼저 선택하세요.");
        }

        try
        {
            await _expertWorkflow.SelectExpertAsync(selectionId, cancellationToken)
                .ConfigureAwait(false);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 전문가 검색 결과입니다.");
        }
    }

    public LegacyOperatorSnapshot ActivateExpertAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsExpertActive() || _expertWorkflow is null)
        {
            return ReportError("전문가 탭을 먼저 선택하세요.");
        }

        try
        {
            var draft = _expertWorkflow.MaterializeSelectedAction(actionId);
            AppendExpertRow(draft);
            _statusMessage = $"{draft.Title} 추천 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 전문가 컷 항목입니다.");
        }
    }

    public async Task<LegacyOperatorSnapshot> SearchTradingHaltsAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsTradingHaltActive() || _tradingHaltWorkflow is null)
        {
            return ReportError("정지 탭을 먼저 선택하세요.");
        }

        await _tradingHaltWorkflow.SearchAsync(query, cancellationToken)
            .ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectTradingHalt(string selectionId)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        if (!IsTradingHaltActive() || _tradingHaltWorkflow is null)
        {
            return ReportError("정지 탭을 먼저 선택하세요.");
        }

        try
        {
            _tradingHaltWorkflow.SelectResult(selectionId);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 거래정지 검색 결과입니다.");
        }
    }

    public LegacyOperatorSnapshot ToggleTradingHaltSort()
    {
        if (!IsTradingHaltActive() || _tradingHaltWorkflow is null)
        {
            return ReportError("정지 탭을 먼저 선택하세요.");
        }

        _tradingHaltWorkflow.ToggleNameSort();
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ActivateTradingHaltAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (!IsTradingHaltActive() || _tradingHaltWorkflow is null)
        {
            return ReportError("정지 탭을 먼저 선택하세요.");
        }

        try
        {
            var draft = _tradingHaltWorkflow.MaterializeSelectedAction(actionId);
            AppendTradingHaltRow(draft);
            _statusMessage = $"{draft.Title} 항목을 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
        catch (ArgumentOutOfRangeException)
        {
            return ReportError("등록되지 않은 거래정지 컷 항목입니다.");
        }
    }

    public LegacyOperatorSnapshot SelectPlaylistRow(
        string rowId,
        bool control,
        bool shift)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        var index = FindPlaylistIndex(rowId);
        if (index < 0)
        {
            return CreateSnapshot();
        }

        _activePlaylistRowId = rowId;

        // MainForm handled Control before Shift. Consequently Ctrl+Shift added only
        // the clicked row, and clicking an already selected row did not remove it.
        if (control)
        {
            _selectedPlaylistRowIds.Add(rowId);
        }
        else
        {
            if (_playlistCursorIndex != index)
            {
                _playlistPriorCursorIndex = _playlistCursorIndex;
                _playlistCursorIndex = index;
            }

            _selectedPlaylistRowIds.Clear();
            if (shift)
            {
                AddPlaylistRange(_playlistPriorCursorIndex, _playlistCursorIndex);
            }
            else
            {
                _selectedPlaylistRowIds.Add(rowId);
            }
        }

        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Moves only MainForm's m_rowidx equivalent. A click in the checkbox column
    /// selected the next F8 candidate before its separate enabled-state handling,
    /// but did not replace the rows accumulated for Ctrl/Shift deletion.
    /// </summary>
    public LegacyOperatorSnapshot ActivatePlaylistRow(string rowId)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        var index = FindPlaylistIndex(rowId);
        if (index < 0 || string.Equals(
                _activePlaylistRowId,
                rowId,
                StringComparison.Ordinal))
        {
            return CreateSnapshot();
        }

        _activePlaylistRowId = rowId;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ToggleActivePlaylistRowEnabled()
    {
        return _activePlaylistRowId is null
            ? CreateSnapshot()
            : TogglePlaylistRowEnabled(_activePlaylistRowId);
    }

    public LegacyOperatorSnapshot SelectPlaylistBoundary(bool last)
    {
        if (_playlist.Count == 0)
        {
            return CreateSnapshot();
        }

        var index = last ? _playlist.Count - 1 : 0;
        var rowId = _playlist[index].RowId;
        _activePlaylistRowId = rowId;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DeleteSelectedPlaylistRows()
    {
        if (_selectedPlaylistRowIds.Count == 0)
        {
            return CreateSnapshot();
        }

        var originalRows = _playlist.ToArray();
        var removedIds = new HashSet<string>(
            _selectedPlaylistRowIds,
            StringComparer.Ordinal);
        var activeIndex = FindOriginalPlaylistIndex(originalRows, _activePlaylistRowId);
        var cursorIndex = Math.Clamp(_playlistCursorIndex, 0, originalRows.Length - 1);
        var priorCursorIndex = Math.Clamp(
            _playlistPriorCursorIndex,
            0,
            originalRows.Length - 1);
        var cursorRowId = originalRows[cursorIndex].RowId;
        var priorCursorRowId = originalRows[priorCursorIndex].RowId;

        _playlist.RemoveAll(row => removedIds.Contains(row.RowId));
        _selectedPlaylistRowIds.Clear();
        if (_playlist.Count == 0)
        {
            ResetPlaylistSelection();
        }
        else
        {
            _activePlaylistRowId = ResolveSurvivingPlaylistRowId(
                originalRows,
                removedIds,
                _activePlaylistRowId,
                activeIndex);
            _playlistCursorIndex = ResolveSurvivingPlaylistIndex(
                originalRows,
                removedIds,
                cursorRowId,
                cursorIndex);
            _playlistPriorCursorIndex = ResolveSurvivingPlaylistIndex(
                originalRows,
                removedIds,
                priorCursorRowId,
                priorCursorIndex);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DeleteAllPlaylistRows()
    {
        if (_dialog is not null)
        {
            return CreateSnapshot();
        }

        _pendingNativeConfirmation = PendingNativeConfirmation.ForPlaylistDeleteAll(
            _playlist.Select(static row => row.RowId).ToArray());
        _dialog = new LegacyDialogState(
            "송출 리스트를 모두 삭제하겠습니까?",
            IsConfirmation: true);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot MoveSelectedPlaylistRows(
        LegacyPlaylistMoveDirection direction)
    {
        var activeIndex = _activePlaylistRowId is null
            ? -1
            : FindPlaylistIndex(_activePlaylistRowId);
        if (activeIndex < 0)
        {
            return CreateSnapshot();
        }

        var targetIndex = direction switch
        {
            LegacyPlaylistMoveDirection.Up => activeIndex - 1,
            LegacyPlaylistMoveDirection.Down => activeIndex + 1,
            _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null)
        };
        if (targetIndex < 0 || targetIndex >= _playlist.Count)
        {
            return CreateSnapshot();
        }

        // Spd_UpDown exchanged all seven cell values for the active row only. It did
        // not move an entire multi-selection as a block.
        (_playlist[activeIndex], _playlist[targetIndex]) =
            (_playlist[targetIndex], _playlist[activeIndex]);

        _selectedPlaylistRowIds.Clear();
        _selectedPlaylistRowIds.Add(_activePlaylistRowId!);
        _playlistPriorCursorIndex = targetIndex;
        _playlistCursorIndex = targetIndex;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ReorderPlaylistRows(
        string sourceRowId,
        string targetRowId,
        LegacyDragDropPosition position)
    {
        ArgumentNullException.ThrowIfNull(sourceRowId);
        ArgumentNullException.ThrowIfNull(targetRowId);

        var sourceIndex = FindPlaylistIndex(sourceRowId);
        var targetIndex = FindPlaylistIndex(targetRowId);
        if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
        {
            return CreateSnapshot();
        }

        // FarPoint's native row move uses the row being dragged as its source.
        // MainForm's manually painted Ctrl/Shift deletion selection was not a
        // native multi-row drag selection, so it must never turn this into a
        // block move.
        var movingRow = _playlist[sourceIndex];
        var remainingRows = _playlist
            .Where(row => !string.Equals(
                row.RowId,
                sourceRowId,
                StringComparison.Ordinal))
            .ToList();
        var remainingTargetIndex = remainingRows.FindIndex(row =>
            string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
        if (remainingTargetIndex < 0)
        {
            return CreateSnapshot();
        }

        var insertionIndex = position switch
        {
            LegacyDragDropPosition.Before => remainingTargetIndex,
            LegacyDragDropPosition.After => remainingTargetIndex + 1,
            _ => throw new ArgumentOutOfRangeException(nameof(position), position, null)
        };
        remainingRows.Insert(insertionIndex, movingRow);

        var orderChanged = !_playlist
            .Select(static row => row.RowId)
            .SequenceEqual(
                remainingRows.Select(static row => row.RowId),
                StringComparer.Ordinal);
        if (!orderChanged)
        {
            return CreateSnapshot();
        }

        _playlist.Clear();
        _playlist.AddRange(remainingRows);
        _selectedPlaylistRowIds.Clear();
        _selectedPlaylistRowIds.Add(sourceRowId);
        _activePlaylistRowId = sourceRowId;
        _playlistCursorIndex = FindPlaylistIndex(sourceRowId);
        _playlistPriorCursorIndex = _playlistCursorIndex;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsAsync(
        CancellationToken cancellationToken = default)
    {
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            var namedPlaylist = await _namedPlaylistWorkflow.RefreshAsync(
                cancellationToken: cancellationToken).ConfigureAwait(false);
            ApplyNamedPlaylistStatus(namedPlaylist);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch
        {
            ApplyNamedPlaylistStatus(
                _namedPlaylistWorkflow.Current,
                "DB 재생목록 목록을 불러오지 못했습니다.",
                LegacyOperatorStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot SelectNamedPlaylist(string definitionId)
    {
        ArgumentNullException.ThrowIfNull(definitionId);
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            ApplyNamedPlaylistStatus(
                _namedPlaylistWorkflow.SelectDefinition(definitionId));
        }
        catch (Exception exception) when (exception is KeyNotFoundException or
            InvalidOperationException or ArgumentException)
        {
            _dialog = new LegacyDialogState(
                "선택한 DB 재생목록이 더 이상 유효하지 않습니다. 목록을 새로 고치세요.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> LoadSelectedNamedPlaylistAsync(
        CancellationToken cancellationToken = default)
    {
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            var namedPlaylist = await _namedPlaylistWorkflow.LoadSelectedAsync(
                cancellationToken).ConfigureAwait(false);
            var loadedRows = namedPlaylist.Rows
                .Select(CreateNamedPlaylistOperatorRow)
                .ToArray();

            _playlist.Clear();
            _playlist.AddRange(loadedRows);
            ResetPlaylistSelection();
            SelectInitialPlaylistCandidate();
            _dialog = null;
            ApplyNamedPlaylistStatus(namedPlaylist);
            var unresolvedCount = loadedRows.Count(row => !row.IsSceneResolved);
            if (unresolvedCount > 0)
            {
                _statusMessage =
                    $"DB 재생목록을 불러왔습니다. 장면 이름을 찾지 못한 {unresolvedCount}개 행은 해당 행을 송출할 때만 차단됩니다.";
                _statusKind = LegacyOperatorStatusKind.Warning;
            }
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (InvalidDataException)
        {
            _dialog = new LegacyDialogState(
                "DB 재생목록의 저장 형식을 읽을 수 없어 기존 송출 목록을 유지했습니다.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Error;
        }
        catch (Exception exception) when (exception is KeyNotFoundException or
            InvalidOperationException or ArgumentException)
        {
            _dialog = new LegacyDialogState("불러올 DB 재생목록을 먼저 선택하세요.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
        }
        catch
        {
            ApplyNamedPlaylistStatus(
                _namedPlaylistWorkflow.Current,
                "선택한 DB 재생목록을 불러오지 못했습니다.",
                LegacyOperatorStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> LoadNamedPlaylistByIdAsync(
        string definitionId,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(definitionId);
        const string command = "load-named-playlist-by-id";
        var selected = SelectNamedPlaylist(definitionId);
        if (!string.Equals(
                selected.NamedPlaylist?.SelectedDefinitionId,
                definitionId,
                StringComparison.Ordinal))
        {
            CompleteCommand(command, definitionId, succeeded: false);
            Touch();
            return CreateSnapshot();
        }

        var loaded = await LoadSelectedNamedPlaylistAsync(cancellationToken)
            .ConfigureAwait(false);
        var succeeded =
            string.Equals(
                loaded.NamedPlaylist?.SelectedDefinitionId,
                definitionId,
                StringComparison.Ordinal) &&
            loaded.NamedPlaylist?.ReadStatus == LegacyNamedPlaylistReadStatus.Ready &&
            loaded.NamedPlaylist?.DocumentFreshness == LegacyNamedPlaylistFreshness.Fresh &&
            (loaded.StatusKind is LegacyOperatorStatusKind.Information or
                LegacyOperatorStatusKind.Warning) &&
            loaded.Dialog is null;
        CompleteCommand(command, definitionId, succeeded);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> CreateNamedPlaylistAsync(
        string title,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(title);
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            var draft = _namedPlaylistWorkflow.CreateProgramDraft(title);
            var result = await _namedPlaylistWorkflow.CreateProgramAsync(
                draft,
                cancellationToken).ConfigureAwait(false);
            ApplyNamedPlaylistStatus(result.Snapshot);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException)
        {
            _dialog = new LegacyDialogState(
                "새 DB 재생목록을 만들 수 없습니다. 목록을 새로 고친 뒤 이름을 확인하세요.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SaveCurrentNamedPlaylistAsync(
        CancellationToken cancellationToken = default)
    {
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            if (_playlist.Count > LegacyNamedPlaylistWorkflowController.MaximumItems)
            {
                throw new InvalidOperationException("The playlist is too large to save.");
            }

            // DC_LIST/PLAY_LIST compatibility remains the original seven fields.
            // Closed native theme rows project their exact DB title into the legacy
            // subject field because ExactSubject is intentionally not an eighth field.
            var rows = _playlist.Select(row =>
            {
                var persistedSelection = CreateNamedPlaylistStoredSelection(row);
                var entry = new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacyPlaylistEntry(
                    row.RowId,
                    row.SceneAlias,
                    row.IsEnabled,
                    row.FadeDuration,
                    persistedSelection);
                return _namedPlaylistWorkflow.CreateRowDraft(
                    entry,
                    ParseNamedPlaylistPage(row.PageText));
            }).ToArray();

            var result = await _namedPlaylistWorkflow.SaveCurrentAsync(
                rows,
                cancellationToken).ConfigureAwait(false);
            ApplyNamedPlaylistStatus(result.Snapshot);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException or FormatException or OverflowException)
        {
            _dialog = new LegacyDialogState(
                "현재 송출 목록을 저장할 수 없습니다. DB 재생목록 선택과 Page 값을 확인하세요.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> SaveCurrentNamedPlaylistToAsync(
        string definitionId,
        bool showSavedAlert = false,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(definitionId);
        const string command = "save-current-named-playlist-to";
        var selected = SelectNamedPlaylist(definitionId);
        if (!string.Equals(
                selected.NamedPlaylist?.SelectedDefinitionId,
                definitionId,
                StringComparison.Ordinal))
        {
            CompleteCommand(command, definitionId, succeeded: false);
            Touch();
            return CreateSnapshot();
        }
        var selectedRevision = selected.NamedPlaylist?.Revision ?? -1;

        var saved = await SaveCurrentNamedPlaylistAsync(cancellationToken)
            .ConfigureAwait(false);
        var succeeded =
            (saved.NamedPlaylist?.Revision ?? -1) > selectedRevision &&
            string.Equals(
                saved.NamedPlaylist?.SelectedDefinitionId,
                definitionId,
                StringComparison.Ordinal) &&
            saved.NamedPlaylist?.LastMutationOutcome is
                LegacyNamedPlaylistMutationOutcome.CommittedFresh or
                LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
        if (succeeded && showSavedAlert)
        {
            // PList displayed this one-button MessageBox after the confirmed save.
            // Keep it in authoritative state so WebView focus/modal behavior is the
            // same as every other MmoneyCoder dialog.
            _dialog = new LegacyDialogState("저장되었습니다");
        }
        CompleteCommand(command, definitionId, succeeded);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> DeleteSelectedNamedPlaylistAsync(
        CancellationToken cancellationToken = default)
    {
        if (_namedPlaylistWorkflow is null)
        {
            return ReportError("DB 재생목록 기능이 구성되지 않았습니다.");
        }

        try
        {
            var result = await _namedPlaylistWorkflow.DeleteSelectedAsync(
                cancellationToken).ConfigureAwait(false);
            ApplyNamedPlaylistStatus(result.Snapshot);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception exception) when (exception is ArgumentException or
            InvalidOperationException)
        {
            _dialog = new LegacyDialogState("삭제할 DB 재생목록을 먼저 선택하세요.");
            _statusMessage = _dialog.Message;
            _statusKind = LegacyOperatorStatusKind.Warning;
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorSnapshot> ConfirmDialogAsync(
        CancellationToken cancellationToken = default)
    {
        var pending = _pendingNativeConfirmation;
        if (_dialog?.IsConfirmation != true || pending is null)
        {
            return CreateSnapshot();
        }

        // Consume the confirmation before crossing a database-write boundary. A
        // timeout or unknown outcome must never leave a replayable Yes action.
        _pendingNativeConfirmation = null;
        _dialog = null;

        return pending.Kind switch
        {
            PendingNativeConfirmationKind.DeleteTheme =>
                await ExecuteConfirmedThemeDeleteAsync(
                    pending.ThemeIdentity!,
                    cancellationToken).ConfigureAwait(false),
            PendingNativeConfirmationKind.DeleteExpert =>
                await ExecuteConfirmedExpertDeleteAsync(
                    pending.ExpertIdentity!,
                    cancellationToken).ConfigureAwait(false),
            PendingNativeConfirmationKind.ClearThemeDraft =>
                ExecuteConfirmedThemeDraftClear(pending.CatalogRevision),
            PendingNativeConfirmationKind.DeleteAllComparisonPairs =>
                await ExecuteConfirmedComparisonDeleteAllAsync(
                    pending.ComparisonRevision,
                    cancellationToken)
                    .ConfigureAwait(false),
            PendingNativeConfirmationKind.ImportLegacyComparisonPairs =>
                await ExecuteConfirmedComparisonImportAsync(
                    pending.ComparisonRevision,
                    cancellationToken)
                    .ConfigureAwait(false),
            PendingNativeConfirmationKind.DeleteAllPlaylistRows =>
                ExecuteConfirmedPlaylistDeleteAll(pending.PlaylistRowIds!),
            PendingNativeConfirmationKind.DeleteManualFinancial =>
                await ExecuteConfirmedManualFinancialDeleteAsync(
                    all: false,
                    pending.ManualFinancialRevision,
                    cancellationToken).ConfigureAwait(false),
            PendingNativeConfirmationKind.DeleteAllManualFinancial =>
                await ExecuteConfirmedManualFinancialDeleteAsync(
                    all: true,
                    pending.ManualFinancialRevision,
                    cancellationToken).ConfigureAwait(false),
            PendingNativeConfirmationKind.DeleteAllManualViItems =>
                ExecuteConfirmedManualViDeleteAll(pending.ManualViItemIds!),
            _ => throw new InvalidOperationException("Unknown native confirmation kind.")
        };
    }

    public LegacyOperatorSnapshot CancelDialog()
    {
        if (_dialog?.IsConfirmation == true)
        {
            _pendingNativeConfirmation = null;
            _dialog = null;
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot DismissDialog()
    {
        if (_dialog is not null && !_dialog.IsConfirmation)
        {
            _dialog = null;
            if (_closeOperatorCatalogAfterDialog)
            {
                _operatorCatalogWorkflow?.Close();
                _closeOperatorCatalogAfterDialog = false;
            }
            ContinuePendingDrop();
            Touch();
        }

        return CreateSnapshot();
    }

    private LegacyOperatorSnapshot ExecuteConfirmedPlaylistDeleteAll(
        IReadOnlyList<string> expectedRowIds)
    {
        var currentRowIds = _playlist.Select(static row => row.RowId).ToArray();
        if (!currentRowIds.SequenceEqual(expectedRowIds, StringComparer.Ordinal))
        {
            return ReportError(
                "송출 리스트가 변경되어 전체 삭제를 시작하지 않았습니다.");
        }

        _playlist.Clear();
        ResetPlaylistSelection();
        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ExecuteConfirmedManualFinancialDeleteAsync(
        bool all,
        long expectedRevision,
        CancellationToken cancellationToken)
    {
        if (!TryGetManualFinancial(out var workflow, out var snapshot) ||
            snapshot.Revision != expectedRevision)
        {
            return ReportError(
                "수동 재무 삭제 대상이 변경되어 DB 삭제를 시작하지 않았습니다.");
        }

        try
        {
            LegacyManualFinancialWriteRequest request = all
                ? workflow.CreateDeleteAllRequest(
                    snapshot,
                    snapshot.Definition.DeleteAllConfirmationToken)
                : workflow.CreateDeleteRequest(snapshot);
            _manualFinancialSnapshot = await workflow.ExecuteAsync(
                snapshot,
                request,
                cancellationToken).ConfigureAwait(false);
            ApplyManualFinancialResultStatus(
                _manualFinancialSnapshot,
                all ? "전체 삭제" : "삭제");
            Touch();
            return CreateSnapshot();
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    private LegacyOperatorSnapshot ExecuteConfirmedManualViDeleteAll(
        IReadOnlyList<string> expectedItemIds)
    {
        if (_manualListsWorkflow is null)
        {
            return ReportError("VI 수동 목록 기능을 사용할 수 없습니다.");
        }

        var current = _manualListsWorkflow.Current;
        var currentItemIds = current.ViItems.Select(static item => item.ItemId).ToArray();
        if (!current.IsOpen || current.Screen != LegacyManualListScreen.Vi ||
            !currentItemIds.SequenceEqual(expectedItemIds, StringComparer.Ordinal))
        {
            return ReportError(
                "VI 목록이 변경되어 전체 삭제를 시작하지 않았습니다.");
        }

        _manualListsWorkflow.DeleteAllViItems();
        ApplyManualListsStatus();
        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ExecuteConfirmedThemeDeleteAsync(
        MMoneyCoderSharp.Data.ThemeSelectionIdentity identity,
        CancellationToken cancellationToken)
    {
        if (_operatorCatalogWorkflow is null || _themeWorkflow is null ||
            _themeSnapshot?.SelectedIdentity != identity)
        {
            return ReportError("삭제 대상 테마가 변경되어 DB 삭제를 시작하지 않았습니다.");
        }

        await _operatorCatalogWorkflow.OpenThemeForEditAsync(identity, cancellationToken)
            .ConfigureAwait(false);
        if (!_operatorCatalogWorkflow.Current.CanDelete)
        {
            ApplyOperatorCatalogStatus();
            _operatorCatalogWorkflow.Close();
            Touch();
            return CreateSnapshot();
        }

        await _operatorCatalogWorkflow.DeleteAsync(cancellationToken).ConfigureAwait(false);
        if (_operatorCatalogWorkflow.Current.Mode == LegacyOperatorCatalogMode.List)
        {
            _themeSnapshot = _themeWorkflow.RemoveSelectedResult(_themeSnapshot);
            _operatorCatalogWorkflow.Close();
            _statusMessage = string.Empty;
            _statusKind = LegacyOperatorStatusKind.Neutral;
        }
        else
        {
            ApplyOperatorCatalogStatus();
        }

        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ExecuteConfirmedExpertDeleteAsync(
        MMoneyCoderSharp.Data.ExpertSelectionIdentity identity,
        CancellationToken cancellationToken)
    {
        var selected = _expertWorkflow?.Current.SelectedExpert;
        if (_operatorCatalogWorkflow is null || selected is null ||
            !string.Equals(selected.ExpertCode, identity.ExpertCode, StringComparison.Ordinal) ||
            !string.Equals(selected.ExpertName, identity.ExpertName, StringComparison.Ordinal))
        {
            return ReportError("삭제 대상 전문가가 변경되어 DB 삭제를 시작하지 않았습니다.");
        }

        await _operatorCatalogWorkflow.OpenExpertForEditAsync(identity, cancellationToken)
            .ConfigureAwait(false);
        if (!_operatorCatalogWorkflow.Current.CanDelete)
        {
            ApplyOperatorCatalogStatus();
            _operatorCatalogWorkflow.Close();
            Touch();
            return CreateSnapshot();
        }

        await _operatorCatalogWorkflow.DeleteAsync(cancellationToken).ConfigureAwait(false);
        if (_operatorCatalogWorkflow.Current.Mode == LegacyOperatorCatalogMode.List)
        {
            _expertWorkflow?.RemoveSelectedResultPreservingPreview();
            _operatorCatalogWorkflow.Close();
            _statusMessage = string.Empty;
            _statusKind = LegacyOperatorStatusKind.Neutral;
        }
        else
        {
            ApplyOperatorCatalogStatus();
        }

        Touch();
        return CreateSnapshot();
    }

    private LegacyOperatorSnapshot ExecuteConfirmedThemeDraftClear(long catalogRevision)
    {
        if (_operatorCatalogWorkflow is null)
        {
            return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
        }

        var catalog = _operatorCatalogWorkflow.Current;
        if (catalog.Revision != catalogRevision || !catalog.IsOpen ||
            catalog.Entity != LegacyOperatorCatalogEntity.Theme ||
            catalog.Mode is not (LegacyOperatorCatalogMode.Create or
                LegacyOperatorCatalogMode.Edit))
        {
            return ReportError("ThemeA 편집 목록이 변경되어 전체 삭제를 실행하지 않았습니다.");
        }

        _operatorCatalogWorkflow.ClearThemeDraftRows();
        ApplyOperatorCatalogStatus();
        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ExecuteConfirmedComparisonDeleteAllAsync(
        long comparisonRevision,
        CancellationToken cancellationToken)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null ||
            _comparisonWorkflow.Current.Revision != comparisonRevision)
        {
            return ReportError(
                "삭제 대상 종목 비교 목록이 변경되어 전체 삭제를 실행하지 않았습니다.");
        }

        await _comparisonWorkflow.DeleteAllPairsAsync(cancellationToken)
            .ConfigureAwait(false);
        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ExecuteConfirmedComparisonImportAsync(
        long comparisonRevision,
        CancellationToken cancellationToken)
    {
        if (!IsComparisonActive() || _comparisonWorkflow is null ||
            _comparisonWorkflow.Current.Revision != comparisonRevision)
        {
            return ReportError(
                "비교쌍 목록이 바뀌어 원본 비교쌍 가져오기를 시작하지 않았습니다.");
        }

        try
        {
            await _comparisonWorkflow.ImportLegacyPairsAsync(cancellationToken)
                .ConfigureAwait(false);
            Touch();
            return CreateSnapshot();
        }
        catch (LegacyComparisonImportException exception)
        {
            _dialog = new LegacyDialogState(exception.Failure switch
            {
                LegacyComparisonImportFailure.SourceUnavailable =>
                    "원본 프로그램의 고정된 Data\\종목비교.dat 파일을 읽을 수 없습니다.",
                LegacyComparisonImportFailure.SourceInvalid =>
                    "원본 종목비교.dat 파일 형식이 올바르지 않아 가져오지 않았습니다.",
                LegacyComparisonImportFailure.IdentityNotFound =>
                    "원본 비교쌍 중 현재 종목 DB에서 정확히 찾을 수 없는 항목이 있어 아무것도 가져오지 않았습니다.",
                LegacyComparisonImportFailure.IdentityAmbiguous =>
                    "원본 비교쌍 중 현재 종목 DB에서 하나로 확정할 수 없는 항목이 있어 아무것도 가져오지 않았습니다.",
                LegacyComparisonImportFailure.DatabaseDataInvalid =>
                    "현재 종목 DB 조회 결과가 안전한 식별 형식과 일치하지 않아 아무것도 가져오지 않았습니다.",
                _ => "원본 비교쌍을 안전하게 가져오지 못했습니다."
            });
            Touch();
            return CreateSnapshot();
        }
        catch (InvalidOperationException exception)
        {
            _dialog = new LegacyDialogState(exception.Message);
            Touch();
            return CreateSnapshot();
        }
    }

    public LegacyOperatorSnapshot ReportError(string message)
    {
        _statusMessage = message ?? string.Empty;
        _statusKind = LegacyOperatorStatusKind.Error;
        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Completes a correlated named-playlist command when the native dispatch
    /// boundary rejects the request or encounters an unexpected exception.
    /// The terminal receipt prevents the WebView modal from waiting indefinitely;
    /// the named-playlist workflow remains the authority for any write quarantine
    /// that was already latched before the exception escaped.
    /// </summary>
    public LegacyOperatorSnapshot ReportNamedPlaylistCommandFailure(
        string command,
        string targetId,
        string message)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(command);
        ArgumentException.ThrowIfNullOrWhiteSpace(targetId);
        if (command is not "refresh-named-playlists" and
            not "select-named-playlist" and
            not "create-named-playlist" and
            not "delete-selected-named-playlist" and
            not "load-named-playlist-by-id" and
            not "save-current-named-playlist-to")
        {
            throw new ArgumentOutOfRangeException(
                nameof(command),
                command,
                "Only correlated named-playlist commands can be completed here.");
        }

        CompleteCommand(command, targetId, succeeded: false);
        return ReportError(message);
    }

    public LegacyOperatorSnapshot CompleteNamedPlaylistDispatch(
        string command,
        string requestId,
        bool succeeded)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(command);
        ArgumentException.ThrowIfNullOrWhiteSpace(requestId);
        if (command is not "refresh-named-playlists" and
            not "select-named-playlist" and
            not "create-named-playlist" and
            not "delete-selected-named-playlist")
        {
            throw new ArgumentOutOfRangeException(
                nameof(command),
                command,
                "The command does not use a named-playlist dispatch id.");
        }

        CompleteCommand(command, requestId, succeeded);
        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Terminates an accepted save command without making its database outcome
    /// retryable. This is intentionally separate from a known pre-dispatch
    /// rejection: an escaped post-dispatch exception permanently quarantines every
    /// later named-playlist write in this process.
    /// </summary>
    public LegacyOperatorSnapshot ReportNamedPlaylistWriteOutcomeUnknown(
        string command,
        string targetId,
        string message)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(command);
        ArgumentException.ThrowIfNullOrWhiteSpace(targetId);
        if (command is not "create-named-playlist" and
            not "delete-selected-named-playlist" and
            not "save-current-named-playlist-to")
        {
            throw new ArgumentOutOfRangeException(
                nameof(command),
                command,
                "The command is not a named-playlist write.");
        }

        _namedPlaylistWorkflow?.QuarantineAmbiguousWriteOutcome();
        CompleteCommand(command, targetId, succeeded: false);
        return ReportError(message);
    }

    public LegacyOperatorSnapshot ReportInformation(string message)
    {
        _statusMessage = message ?? string.Empty;
        _statusKind = LegacyOperatorStatusKind.Information;
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorSnapshot ReportWarning(string message)
    {
        _statusMessage = message ?? string.Empty;
        _statusKind = LegacyOperatorStatusKind.Warning;
        Touch();
        return CreateSnapshot();
    }

    private void ContinuePendingDrop()
    {
        while (_pendingDropCutRows is { Count: > 0 })
        {
            var physicalIndex = _pendingDropCutRows.Dequeue();
            if (!TryAppendCut(physicalIndex))
            {
                return;
            }
        }

        _pendingDropCutRows = null;
    }

    private bool TryAppendCut(int physicalIndex)
    {
        if (_selectedStockIndex is not int stockIndex ||
            stockIndex < 0 || stockIndex >= _searchData.DisplayNames.Count ||
            !IsCutIndex(physicalIndex))
        {
            return true;
        }

        var cut = _cutRows[physicalIndex];
        var stockName = _searchData.DisplayNames[stockIndex];
        var isNxt = stockName.Contains("(NXT)", StringComparison.Ordinal);
        if (isNxt && !string.Equals(
                cut.RawLabel,
                LegacyStockCutCatalog.NxtAllowedLabel,
                StringComparison.Ordinal))
        {
            _dialog = new LegacyDialogState("NXT는 제공하지 않습니다.");
            return false;
        }

        if (cut.IsSeparator)
        {
            return true;
        }

        var identity = _searchData.ResolveLastByDisplayName(stockName);
        var rowId = $"legacy-stock-{++_playlistSequence:D8}";
        AppendPlaylistRow(LegacyStockCutCatalog.CreatePlaylistRow(
            rowId,
            stockName,
            identity,
            cut));
        return true;
    }

    private void ApplyLegacyPointerSelection(int physicalIndex, bool control, bool shift)
    {
        if (control && shift)
        {
            AddLegacyRangeFromAnchor(physicalIndex);
        }
        else if (control)
        {
            if (!_legacyPointerRows.Add(physicalIndex))
            {
                _legacyPointerRows.Remove(physicalIndex);
            }
        }
        else if (shift)
        {
            _legacyPointerRows.Clear();
            AddLegacyRangeFromAnchor(physicalIndex);
        }
        else
        {
            _legacyPointerRows.Clear();
            _legacyPointerRows.Add(physicalIndex);
        }
    }

    private void ApplyNativeListSelection(int physicalIndex, bool control, bool shift)
    {
        var anchor = _nativeSelectionAnchorIndex >= 0
            ? _nativeSelectionAnchorIndex
            : physicalIndex;
        if (control && shift)
        {
            AddInclusiveRange(
                _nativeSelectedCutRows,
                anchor,
                physicalIndex);
        }
        else if (control)
        {
            if (!_nativeSelectedCutRows.Add(physicalIndex))
            {
                _nativeSelectedCutRows.Remove(physicalIndex);
            }
        }
        else if (shift)
        {
            _nativeSelectedCutRows.Clear();
            AddInclusiveRange(
                _nativeSelectedCutRows,
                anchor,
                physicalIndex);
        }
        else
        {
            _nativeSelectedCutRows.Clear();
            _nativeSelectedCutRows.Add(physicalIndex);
        }
    }

    private void ApplyNativeKeyboardNavigationSelection(
        int physicalIndex,
        bool control,
        bool shift)
    {
        var anchor = IsCutIndex(_nativeSelectionAnchorIndex)
            ? _nativeSelectionAnchorIndex
            : physicalIndex;
        if (control && !shift)
        {
            // Ctrl+navigation moves the focus rectangle without changing either
            // selection or the Win32 ListView selection mark.
            return;
        }

        if (control && shift)
        {
            AddInclusiveRange(_nativeSelectedCutRows, anchor, physicalIndex);
            return;
        }

        _nativeSelectedCutRows.Clear();
        if (shift)
        {
            AddInclusiveRange(_nativeSelectedCutRows, anchor, physicalIndex);
            return;
        }

        _nativeSelectedCutRows.Add(physicalIndex);
        _nativeSelectionAnchorIndex = physicalIndex;
    }

    private void ApplyNativeSpaceSelection(int physicalIndex, bool control, bool shift)
    {
        var anchor = IsCutIndex(_nativeSelectionAnchorIndex)
            ? _nativeSelectionAnchorIndex
            : physicalIndex;
        if (control && shift)
        {
            AddInclusiveRange(_nativeSelectedCutRows, anchor, physicalIndex);
            return;
        }

        if (control)
        {
            if (!_nativeSelectedCutRows.Add(physicalIndex))
            {
                _nativeSelectedCutRows.Remove(physicalIndex);
            }

            _nativeSelectionAnchorIndex = physicalIndex;
            return;
        }

        _nativeSelectedCutRows.Clear();
        if (shift)
        {
            AddInclusiveRange(_nativeSelectedCutRows, anchor, physicalIndex);
            return;
        }

        _nativeSelectedCutRows.Add(physicalIndex);
        _nativeSelectionAnchorIndex = physicalIndex;
    }

    private void AddLegacyRangeFromAnchor(int physicalIndex)
    {
        // MainForm used beforeRow = -1 and skipped the range when the previous
        // and current rows were equal. The sentinel also affected its double-click count.
        var anchor = _cutAnchorIndex ?? -1;
        if (anchor == physicalIndex)
        {
            return;
        }

        var first = Math.Min(anchor, physicalIndex);
        var last = Math.Max(anchor, physicalIndex);
        for (var index = first; index <= last; index++)
        {
            _legacyPointerRows.Add(index);
        }
    }

    private static void AddInclusiveRange(
        ISet<int> target,
        int anchor,
        int physicalIndex)
    {
        var first = Math.Min(anchor, physicalIndex);
        var last = Math.Max(anchor, physicalIndex);
        for (var index = first; index <= last; index++)
        {
            target.Add(index);
        }
    }

    private void ApplyNamedPlaylistStatus(
        LegacyNamedPlaylistWorkflowSnapshot snapshot,
        string? fallbackMessage = null,
        LegacyOperatorStatusKind? fallbackKind = null)
    {
        _statusMessage = string.IsNullOrWhiteSpace(snapshot.StatusMessage)
            ? fallbackMessage ?? string.Empty
            : snapshot.StatusMessage;
        _statusKind = fallbackKind ?? snapshot.StatusKind;
    }

    private static MMoneyCoderSharp.Data.NamedPlaylistPageState? ParseNamedPlaylistPage(
        string pageText)
    {
        ArgumentNullException.ThrowIfNull(pageText);
        if (pageText.Length == 0)
        {
            return null;
        }

        if (!string.Equals(pageText, pageText.Trim(), StringComparison.Ordinal))
        {
            throw new FormatException("A named-playlist page must be canonical.");
        }

        var parts = pageText.Split('/');
        if (parts.Length != 2 ||
            !int.TryParse(
                parts[0],
                System.Globalization.NumberStyles.None,
                System.Globalization.CultureInfo.InvariantCulture,
                out var currentPage) ||
            !int.TryParse(
                parts[1],
                System.Globalization.NumberStyles.None,
                System.Globalization.CultureInfo.InvariantCulture,
                out var totalPages))
        {
            throw new FormatException("A named-playlist page must use current/total form.");
        }

        return new MMoneyCoderSharp.Data.NamedPlaylistPageState(currentPage, totalPages);
    }

    private static CoreSceneSelection CreateNamedPlaylistStoredSelection(
        LegacyOperatorPlaylistRow row)
    {
        var native = row.PlayoutSelection;
        if (native?.ExactSubject is { } exactTitle &&
            native.GroupCode is "PAGED_THEME" or "PAGED_NXT_THEME")
        {
            var isNxt = native.GroupCode == "PAGED_NXT_THEME";
            var expectedMarket = isNxt ? "테마_NXT" : "테마";
            if (!string.Equals(row.MarketText, expectedMarket, StringComparison.Ordinal) ||
                !string.Equals(native.DataCode, row.StockCode, StringComparison.Ordinal) ||
                native.DataCode.Length is < 3 or > 8 ||
                native.DataCode.Any(character => !char.IsAsciiDigit(character)) ||
                !IsSafeStoredThemeTitle(exactTitle, 128))
            {
                throw new InvalidOperationException(
                    "The native theme row has an invalid exact stored identity.");
            }

            return new CoreSceneSelection(
                row.MarketText,
                isNxt ? exactTitle + "(NXT)" : exactTitle,
                row.GraphicType,
                row.Subtype,
                row.StockCode);
        }

        return new CoreSceneSelection(
            row.MarketText,
            row.StockName,
            row.GraphicType,
            row.Subtype,
            row.StockCode);
    }

    private LegacyOperatorPlaylistRow CreateNamedPlaylistOperatorRow(
        LegacyNamedPlaylistRowView row)
    {
        var selection = row.Selection;
        var sceneAlias = ResolveNamedPlaylistSceneAlias(selection) ??
            MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacyPlaylistEntry.UnresolvedCutCode;
        var playoutSelection = TryCreateNamedFixedSelection(
                selection,
                out var fixedSelection,
                out _)
            ? fixedSelection
            : TryCreateNamedStockDetailSelection(selection, out var stockDetailSelection)
                ? stockDetailSelection
                : TryCreateNamedLegacyIndustryFixedSelection(
                    selection,
                    out var legacyIndustryFixedSelection)
                    ? legacyIndustryFixedSelection
                : TryCreateNamedIndustrySelection(selection, out var industrySelection)
                    ? industrySelection
                    : TryCreateNamedLegacyComparisonSelection(
                        selection,
                        out var legacyComparisonSelection)
                        ? legacyComparisonSelection
                    : TryCreateNamedComparisonSelection(selection, out var comparisonSelection)
                        ? comparisonSelection
                        : TryCreateNamedThemeSelection(selection, out var themeSelection)
                            ? themeSelection
                            : TryCreateNamedExpertSelection(selection, out var expertSelection)
                                ? expertSelection
                        : TryCreateNamedManualFinancialSelection(
                            selection,
                            out var manualFinancialSelection)
                            ? manualFinancialSelection
            : TryCreateNamedManualNetSellSelection(
                selection,
                out var manualNetSellSelection)
                ? manualNetSellSelection
                : TryCreateNamedVersionedViSelection(selection, out var viSelection)
                    ? viSelection
                    : new CoreSceneSelection(
                        selection.GroupCode,
                        selection.Subject,
                        selection.GraphicType,
                        selection.Subtype,
                        selection.DataCode);
        var rawCutLabel = selection.Subtype.Length == 0
            ? selection.GraphicType
            : $"{selection.GraphicType}_{selection.Subtype}";
        return NormalizeMovingAverageIdentity(new LegacyOperatorPlaylistRow(
            $"legacy-named-{++_playlistSequence:D8}",
            row.IsEnabled,
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            selection.Subtype,
            row.Page?.PageText ?? string.Empty,
            selection.DataCode,
            rawCutLabel,
            6,
            SceneAlias: sceneAlias,
            PlayoutSelection: playoutSelection));
    }

    private string? ResolveNamedPlaylistSceneAlias(
        LegacyNamedPlaylistSelectionView selection)
    {
        var rawLabel = selection.Subtype.Length == 0
            ? selection.GraphicType
            : $"{selection.GraphicType}_{selection.Subtype}";

        // An original fixed-index row can use a stock-market value such as 코스피.
        // Resolve the exact fixed signature before the broader stock-cut label map.
        if (TryCreateNamedFixedSelection(selection, out _, out var fixedAction))
        {
            return fixedAction!.Code;
        }

        // Stock and trading-halt rows retain the original stock-market and cut-label
        // columns, so the closed stock catalog remains the authority for their alias.
        if (selection.GroupCode is "코스피" or "코스닥" or
            "코스피_NXT" or "코스닥_NXT")
        {
            try
            {
                return LegacyStockCutCatalog.ResolveCutAlias(
                    rawLabel,
                    selection.GroupCode.EndsWith("_NXT", StringComparison.Ordinal) ||
                    selection.Subject.Contains("(NXT)", StringComparison.Ordinal));
            }
            catch (InvalidOperationException)
            {
                // Continue through the other closed registries. This also permits
                // manual-financial rows whose market column resembles a stock market.
            }
        }

        var industryMarket = selection.GroupCode switch
        {
            "업종_코스피" => MMoneyCoderSharp.Data.IndustryMarket.Kospi,
            "업종_코스닥" => MMoneyCoderSharp.Data.IndustryMarket.Kosdaq,
            _ => (MMoneyCoderSharp.Data.IndustryMarket?)null
        };
        if (industryMarket is { } industry)
        {
            var actions = LegacyIndustryActionCatalog.GetActions(industry)
                .Where(action => string.Equals(
                    action.Label,
                    rawLabel,
                    StringComparison.Ordinal))
                .Take(2)
                .ToArray();
            if (actions.Length == 1)
            {
                return actions[0].SceneAlias;
            }
        }

        if (selection.GroupCode is "테마" or "테마_NXT")
        {
            var detail = selection.Subtype.Length == 0
                ? selection.GraphicType
                : $"{selection.GraphicType} · {selection.Subtype}";
            var actions = LegacyThemeWorkflow.Actions
                .Where(action => LegacyThemeWorkflow.Sorts.Any(sort => string.Equals(
                    $"{action.Label} · {sort.Label}",
                    detail,
                    StringComparison.Ordinal)))
                .Take(2)
                .ToArray();
            if (actions.Length == 1)
            {
                return actions[0].Code;
            }
        }

        if (selection.GroupCode is "전문가" or "전문가 추천" &&
            string.Equals(selection.GraphicType, "5단 표그래프", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "현재가", StringComparison.Ordinal))
        {
            return "5074";
        }

        // FSell rows are saved with the closed canonical selection emitted by the
        // native manual-list workflow. Older PList rows use the original visible
        // seven-field identity; the scene resolver accepts both shapes and performs
        // the fresh trusted-file read again before any cue can reach Tornado2.
        if (TryCreateNamedManualNetSellSelection(selection, out _))
        {
            return "5025";
        }

        // A versioned VI row carries no path or operator-controlled scene name. The
        // native cue provider reopens the trusted snapshot and requires this exact
        // version at PREPARE, so alias restoration remains fail-closed.
        if (TryCreateNamedVersionedViSelection(selection, out _))
        {
            return "5074";
        }

        if (string.Equals(selection.GroupCode, "비교", StringComparison.Ordinal))
        {
            var alias = selection.GraphicType switch
            {
                "s5032" => "5032",
                "s8018" => "8032",
                "s5029" => "5029",
                "s5026" => "5026",
                "s5087" => "5087",
                _ => null
            };
            if (alias is not null)
            {
                return alias;
            }
        }

        var manualFinancial = LegacyManualFinancialWorkflow.Screens
            .Where(definition =>
                string.Equals(definition.Label, selection.GraphicType, StringComparison.Ordinal) &&
                (selection.GroupCode is "KOSPI" or "KOSDAQ" or
                    "NXT_KOSPI" or "NXT_KOSDAQ"))
            .Take(2)
            .ToArray();
        if (manualFinancial.Length == 1)
        {
            return manualFinancial[0].Code;
        }

        return LegacyCutInfoSceneAliasResolver.TryResolve(
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            selection.Subtype,
            out var legacyAlias)
                ? legacyAlias
                : null;
    }

    private bool TryCreateNamedLegacyIndustryFixedSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        var market = selection.GroupCode switch
        {
            "업종_코스피" when selection.Subject == "코스피" =>
                MMoneyCoderSharp.Data.IndustryMarket.Kospi,
            "업종_코스닥" when selection.Subject == "코스닥" =>
                MMoneyCoderSharp.Data.IndustryMarket.Kosdaq,
            _ => (MMoneyCoderSharp.Data.IndustryMarket?)null
        };
        if (market is not { } exactMarket || selection.Subtype.Length != 0 ||
            selection.DataCode.Length != 0)
        {
            return false;
        }

        var marketGroup = exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
            ? "INDUSTRY_KOSPI"
            : "INDUSTRY_KOSDAQ";
        var marketName = exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
            ? "KOSPI"
            : "KOSDAQ";
        normalized = selection.GraphicType switch
        {
            "5단 표그래프" => new CoreSceneSelection(
                exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
                    ? "PAGED_DOMESTIC_KOSPI"
                    : "PAGED_DOMESTIC_KOSDAQ",
                "INDEX",
                string.Empty,
                "INDUSTRY",
                _timeProvider.GetLocalNow().ToString(
                    "yyyy-MM-dd",
                    System.Globalization.CultureInfo.InvariantCulture)),
            "네모그래프" => new CoreSceneSelection(
                marketGroup,
                marketName,
                "네모그래프",
                string.Empty,
                string.Empty),
            "섹터지수" => new CoreSceneSelection(
                marketGroup,
                marketName,
                "SECTOR_INDEX",
                string.Empty,
                string.Empty),
            _ => null!
        };
        return normalized is not null;
    }

    private static bool TryCreateNamedLegacyComparisonSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (!string.Equals(selection.GroupCode, "지수", StringComparison.Ordinal) ||
            !string.Equals(selection.GraphicType, "2열판", StringComparison.Ordinal) ||
            selection.DataCode.Length != 0)
        {
            return false;
        }

        var actionId = selection.Subtype switch
        {
            "" => "two-column-current",
            "예상체결" => "two-column-expected",
            _ => null
        };
        var names = selection.Subject.Split(',', StringSplitOptions.None);
        if (actionId is null || names.Length != 2)
        {
            return false;
        }

        var first = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(
            new LegacyComparisonStoredTargetReference(names[0], string.Empty));
        var second = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(
            new LegacyComparisonStoredTargetReference(names[1], string.Empty));
        if (first is null || second is null)
        {
            return false;
        }

        try
        {
            normalized = LegacyComparisonWorkflowCatalog.Materialize(
                actionId,
                first,
                second,
                "legacy-named-restore").Selection;
            return true;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    private bool TryCreateNamedThemeSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        var isNxt = string.Equals(selection.GroupCode, "테마_NXT", StringComparison.Ordinal);
        if (!isNxt && !string.Equals(selection.GroupCode, "테마", StringComparison.Ordinal) ||
            selection.DataCode.Length is < 3 or > 8 ||
            selection.DataCode.Any(character => !char.IsAsciiDigit(character)))
        {
            return false;
        }

        var subtype = selection.Subtype switch
        {
            "테마-현재가(입력순)" => (Value: "CURRENT", Sort: "INPUT_ORDER"),
            "테마-현재가(상승률순)" => (Value: "CURRENT", Sort: "GAIN_DESC"),
            "테마-현재가(하락률순)" => (Value: "CURRENT", Sort: "GAIN_ASC"),
            "테마-현재가" => (Value: "CURRENT", Sort: "GAIN_ASC"),
            "테마-예상체결가(입력순)" => (Value: "EXPECTED", Sort: "INPUT_ORDER"),
            "테마-예상체결가(상승률순)" => (Value: "EXPECTED", Sort: "GAIN_DESC"),
            "테마-예상체결가(하락률순)" => (Value: "EXPECTED", Sort: "GAIN_ASC"),
            "테마-예상체결가" => (Value: "EXPECTED", Sort: "GAIN_ASC"),
            _ => default
        };
        if (subtype.Value is null || isNxt && subtype.Value != "CURRENT" ||
            selection.GraphicType switch
            {
                "5단 표그래프" => false,
                "6종목 현재가" or "12종목 현재가" => subtype.Value != "CURRENT",
                _ => true
            })
        {
            return false;
        }

        if (isNxt)
        {
            const string suffix = "(NXT)";
            if (!selection.Subject.EndsWith(suffix, StringComparison.Ordinal) ||
                selection.Subject.Length == suffix.Length)
            {
                return false;
            }

            var exactTitle = selection.Subject[..^suffix.Length];
            if (!IsSafeStoredThemeTitle(exactTitle, 128))
            {
                return false;
            }

            normalized = new CoreSceneSelection(
                "PAGED_NXT_THEME",
                selection.Subject,
                _timeProvider.GetLocalNow().Hour <= 12 ? "PRE_MARKET" : "AFTER_MARKET",
                subtype.Sort,
                selection.DataCode,
                exactTitle);
            return true;
        }

        if (!IsSafeStoredThemeTitle(selection.Subject, 128))
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            "PAGED_THEME",
            selection.Subject,
            subtype.Sort,
            subtype.Value,
            selection.DataCode,
            selection.Subject);
        return true;
    }

    private static bool IsSafeStoredThemeTitle(string value, int maximumLength) =>
        value.Length is >= 1 &&
        value.Length <= maximumLength &&
        !string.IsNullOrWhiteSpace(value) &&
        !value.Contains('^') &&
        !value.Any(character =>
            char.IsControl(character) ||
            char.IsSurrogate(character) ||
            character == '\uFFFD' ||
            System.Globalization.CharUnicodeInfo.GetUnicodeCategory(character) is
                System.Globalization.UnicodeCategory.Format or
                System.Globalization.UnicodeCategory.LineSeparator or
                System.Globalization.UnicodeCategory.ParagraphSeparator);

    private static bool TryCreateNamedExpertSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (selection.GroupCode is not ("전문가" or "전문가 추천") ||
            !string.Equals(selection.GraphicType, "5단 표그래프", StringComparison.Ordinal) ||
            !string.Equals(selection.Subtype, "현재가", StringComparison.Ordinal) ||
            selection.DataCode.Length != 4 ||
            selection.DataCode.Any(character => !char.IsAsciiDigit(character)))
        {
            return false;
        }

        try
        {
            LegacyComparisonValueGuard.RequireText(
                selection.Subject,
                128,
                nameof(selection.Subject));
        }
        catch (ArgumentException)
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            "PAGED_EXPERT",
            selection.Subject,
            "INPUT_ORDER",
            "CURRENT",
            selection.DataCode);
        return true;
    }

    private bool TryCreateNamedFixedSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized,
        out LegacyFixedAction? action)
    {
        normalized = null!;
        action = null;
        var fixedMarket = selection.GroupCode switch
        {
            "해외지수" => LegacyFixedMarket.Overseas,
            "환율" => LegacyFixedMarket.Exchange,
            "주요지수" => LegacyFixedMarket.Index,
            _ when selection.DataCode.Length == 0 &&
                string.Equals(selection.Subject, "지수", StringComparison.Ordinal) =>
                LegacyFixedMarket.Index,
            _ => (LegacyFixedMarket?)null
        };
        if (fixedMarket is not { } market)
        {
            return false;
        }

        var matches = _fixedCatalog.GetActions(market)
            .Where(candidate =>
                MatchesCurrentNamedFixedSelection(candidate, selection) ||
                MatchesLegacyNamedFixedSelection(candidate, selection))
            .Take(2)
            .ToArray();
        if (matches.Length != 1 || !matches[0].Available)
        {
            return false;
        }

        action = matches[0];
        var isCurrentShape = MatchesCurrentNamedFixedSelection(action, selection);
        var isLegacyShape = MatchesLegacyNamedFixedSelection(action, selection);
        var materialized = _fixedCatalog.Materialize(
            action.Id,
            _timeProvider.GetLocalNow()).Selection;
        normalized = new CoreSceneSelection(
            materialized.GroupCode,
            materialized.Subject,
            materialized.GraphicType,
            materialized.Subtype,
            isLegacyShape && !isCurrentShape
                ? materialized.DataCode
                : selection.DataCode);
        return true;
    }

    private static bool MatchesCurrentNamedFixedSelection(
        LegacyFixedAction action,
        LegacyNamedPlaylistSelectionView selection) =>
        string.Equals(action.Label, selection.Subject, StringComparison.Ordinal) &&
        string.Equals(action.Section, selection.GraphicType, StringComparison.Ordinal) &&
        string.Equals(action.Detail, selection.Subtype, StringComparison.Ordinal);

    private static bool MatchesLegacyNamedFixedSelection(
        LegacyFixedAction action,
        LegacyNamedPlaylistSelectionView selection)
    {
        var visible = ProjectLegacyFixedVisibleSelection(action);
        return string.Equals(visible.GroupCode, selection.GroupCode, StringComparison.Ordinal) &&
            string.Equals(visible.Subject, selection.Subject, StringComparison.Ordinal) &&
            string.Equals(visible.GraphicType, selection.GraphicType, StringComparison.Ordinal) &&
            string.Equals(visible.Subtype, selection.Subtype, StringComparison.Ordinal) &&
            string.Equals(visible.DataCode, selection.DataCode, StringComparison.Ordinal);
    }

    private static LegacyFixedSelection ProjectLegacyFixedVisibleSelection(
        LegacyFixedAction action)
    {
        var split = action.Label.IndexOf('_');
        var first = split < 0 ? action.Label : action.Label[..split];
        var remainder = split < 0 ? string.Empty : action.Label[(split + 1)..];
        if (action.Market is LegacyFixedMarket.Overseas or LegacyFixedMarket.Exchange)
        {
            return new LegacyFixedSelection(
                action.Market == LegacyFixedMarket.Overseas ? "해외지수" : "환율",
                first,
                action.Section,
                remainder,
                string.Empty);
        }

        if (action.Section is "5단 표그래프" or "6종목 현재가" or "12종목 현재가")
        {
            return new LegacyFixedSelection(
                LegacyFixedIndexGroup(action.Label),
                "지수",
                action.Section,
                action.Label,
                string.Empty);
        }

        if (string.Equals(action.Section, "매매동향", StringComparison.Ordinal))
        {
            return new LegacyFixedSelection(
                LegacyFixedIndexGroup(action.Label),
                "지수",
                first,
                remainder,
                string.Empty);
        }

        return new LegacyFixedSelection(
            action.Section,
            "지수",
            first,
            remainder,
            string.Empty);
    }

    private static string LegacyFixedIndexGroup(string label) =>
        label.Contains("코스피_NXT", StringComparison.Ordinal) ? "코스피_NXT" :
        label.Contains("코스닥_NXT", StringComparison.Ordinal) ? "코스닥_NXT" :
        label.Contains("코스피", StringComparison.Ordinal) ? "코스피" :
        label.Contains("코스닥", StringComparison.Ordinal) ? "코스닥" :
        "전체";

    private static bool TryCreateNamedStockDetailSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (selection.GroupCode is not ("코스피" or "코스닥") ||
            !string.Equals(selection.GraphicType, "1열판상세", StringComparison.Ordinal))
        {
            return false;
        }

        var branch = selection.Subtype switch
        {
            "액면가" => nameof(S5011DetailBranch.FaceValue),
            "PBR" => nameof(S5011DetailBranch.Valuation),
            "거래량" => nameof(S5011DetailBranch.Volume),
            _ => null
        };
        if (branch is null)
        {
            return false;
        }

        try
        {
            LegacyComparisonValueGuard.RequireText(
                selection.Subject,
                200,
                nameof(selection.Subject));
            LegacyComparisonValueGuard.RequireStockCode(
                selection.DataCode,
                nameof(selection.DataCode));
        }
        catch (ArgumentException)
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            branch,
            selection.DataCode);
        return true;
    }

    private static bool TryCreateNamedIndustrySelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        var market = selection.GroupCode switch
        {
            "업종_코스피" => MMoneyCoderSharp.Data.IndustryMarket.Kospi,
            "업종_코스닥" => MMoneyCoderSharp.Data.IndustryMarket.Kosdaq,
            _ => (MMoneyCoderSharp.Data.IndustryMarket?)null
        };
        if (market is not { } exactMarket)
        {
            return false;
        }

        var rawLabel = selection.Subtype.Length == 0
            ? selection.GraphicType
            : $"{selection.GraphicType}_{selection.Subtype}";
        var actions = LegacyIndustryActionCatalog.GetActions(exactMarket)
            .Where(action => string.Equals(action.Label, rawLabel, StringComparison.Ordinal))
            .Take(2)
            .ToArray();
        if (actions.Length != 1)
        {
            return false;
        }

        try
        {
            LegacyComparisonValueGuard.RequireText(
                selection.Subject,
                256,
                nameof(selection.Subject));
        }
        catch (ArgumentException)
        {
            return false;
        }

        var action = actions[0];
        var industryGroup = exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
            ? "INDUSTRY_KOSPI"
            : "INDUSTRY_KOSDAQ";
        var marketName = exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
            ? "KOSPI"
            : "KOSDAQ";
        normalized = action.Kind switch
        {
            LegacyIndustryActionKind.Single => new CoreSceneSelection(
                industryGroup,
                selection.Subject,
                "1열판",
                "CURRENT",
                string.Empty),
            LegacyIndustryActionKind.Pair => new CoreSceneSelection(
                industryGroup,
                selection.Subject,
                "2열판",
                string.Empty,
                string.Empty),
            LegacyIndustryActionKind.Yield => new CoreSceneSelection(
                industryGroup,
                selection.Subject,
                "YIELD_GRAPH",
                action.YieldPeriod!,
                string.Empty),
            LegacyIndustryActionKind.Candle => new CoreSceneSelection(
                exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
                    ? "KOSPI_INDUSTRY"
                    : "KOSDAQ_INDUSTRY",
                selection.Subject,
                string.Empty,
                action.CandleMode == LegacyIndustryCandleMode.Volume ? "VOLUME" : "PRICE",
                string.Empty),
            LegacyIndustryActionKind.FiveRow when
                DateOnly.TryParseExact(
                    selection.DataCode,
                    "yyyy-MM-dd",
                    System.Globalization.CultureInfo.InvariantCulture,
                    System.Globalization.DateTimeStyles.None,
                    out _) => new CoreSceneSelection(
                        exactMarket == MMoneyCoderSharp.Data.IndustryMarket.Kospi
                            ? "PAGED_DOMESTIC_KOSPI"
                            : "PAGED_DOMESTIC_KOSDAQ",
                        "INDEX",
                        string.Empty,
                        "INDUSTRY",
                        selection.DataCode),
            LegacyIndustryActionKind.Square => new CoreSceneSelection(
                industryGroup,
                marketName,
                "네모그래프",
                string.Empty,
                string.Empty),
            LegacyIndustryActionKind.Sector => new CoreSceneSelection(
                industryGroup,
                marketName,
                "SECTOR_INDEX",
                string.Empty,
                string.Empty),
            _ => null!
        };
        return normalized is not null;
    }

    private static bool TryCreateNamedComparisonSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (!string.Equals(selection.GroupCode, "비교", StringComparison.Ordinal))
        {
            return false;
        }

        var names = selection.Subject.Split(" ↔ ", StringSplitOptions.None);
        if (names.Length != 2 || names.Any(string.IsNullOrWhiteSpace) ||
            names.Any(name => !string.Equals(name, name.Trim(), StringComparison.Ordinal)))
        {
            return false;
        }

        LegacyComparisonTarget first;
        LegacyComparisonTarget second;
        if (selection.DataCode.Length == 0)
        {
            first = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(
                new LegacyComparisonStoredTargetReference(names[0], string.Empty))!;
            second = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(
                new LegacyComparisonStoredTargetReference(names[1], string.Empty))!;
            if (first is null || second is null)
            {
                return false;
            }
        }
        else if (!TryCreateNamedComparisonStocks(
                     names,
                     selection.DataCode,
                     out first,
                     out second))
        {
            return false;
        }

        var actions = LegacyComparisonWorkflowCatalog.Actions
            .Where(action => string.Equals(
                action.Label,
                selection.Subtype,
                StringComparison.Ordinal))
            .Take(2)
            .ToArray();
        if (actions.Length != 1)
        {
            return false;
        }

        try
        {
            var draft = LegacyComparisonWorkflowCatalog.Materialize(
                actions[0].ActionId,
                first,
                second,
                "named-restore");
            if (!string.Equals(draft.BuilderKey, selection.GraphicType, StringComparison.Ordinal) ||
                !string.Equals(draft.Detail, selection.Subtype, StringComparison.Ordinal) ||
                !string.Equals(
                    draft.Selection.DataCode,
                    selection.DataCode,
                    StringComparison.Ordinal))
            {
                return false;
            }

            normalized = draft.Selection;
            return true;
        }
        catch (Exception exception) when (exception is
            ArgumentException or InvalidOperationException)
        {
            return false;
        }
    }

    private static bool TryCreateNamedComparisonStocks(
        IReadOnlyList<string> names,
        string dataCode,
        out LegacyComparisonTarget first,
        out LegacyComparisonTarget second)
    {
        first = null!;
        second = null!;
        var identities = dataCode.Split('|', StringSplitOptions.None);
        if (identities.Length != 2)
        {
            return false;
        }

        try
        {
            var targets = new LegacyComparisonTarget[2];
            for (var index = 0; index < targets.Length; index++)
            {
                var separator = identities[index].IndexOf(':');
                if (separator <= 0 ||
                    separator != identities[index].LastIndexOf(':') ||
                    separator == identities[index].Length - 1)
                {
                    return false;
                }

                var market = identities[index][..separator] switch
                {
                    "KOSPI" => LegacyComparisonDomesticMarket.Kospi,
                    "KOSDAQ" => LegacyComparisonDomesticMarket.Kosdaq,
                    _ => (LegacyComparisonDomesticMarket?)null
                };
                if (market is not { } exactMarket)
                {
                    return false;
                }

                targets[index] = LegacyComparisonTarget.CreateKrxStock(
                    exactMarket,
                    names[index],
                    identities[index][(separator + 1)..]);
            }

            first = targets[0];
            second = targets[1];
            return true;
        }
        catch (ArgumentException)
        {
            return false;
        }
    }

    private static bool TryCreateNamedManualFinancialSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (selection.GroupCode is not
            ("KOSPI" or "KOSDAQ" or "NXT_KOSPI" or "NXT_KOSDAQ"))
        {
            return false;
        }

        var definitions = LegacyManualFinancialWorkflow.Screens
            .Where(definition =>
                string.Equals(definition.Label, selection.GraphicType, StringComparison.Ordinal) &&
                string.Equals(
                    definition.CanonicalGraphicType,
                    selection.Subtype,
                    StringComparison.Ordinal))
            .Take(2)
            .ToArray();
        if (definitions.Length != 1)
        {
            return false;
        }

        try
        {
            LegacyComparisonValueGuard.RequireText(
                selection.Subject,
                200,
                nameof(selection.Subject));
            LegacyComparisonValueGuard.RequireStockCode(
                selection.DataCode,
                nameof(selection.DataCode));
        }
        catch (ArgumentException)
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            selection.GroupCode,
            selection.Subject,
            definitions[0].CanonicalGraphicType,
            string.Empty,
            string.Empty);
        return true;
    }

    private static bool TryCreateNamedManualNetSellSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        if (selection.DataCode.Length != 0)
        {
            return false;
        }

        string? audience = null;
        if (selection.GroupCode is "INDIVIDUAL" or "FOREIGN" or "INSTITUTION" &&
            string.Equals(selection.Subject, "INDEX", StringComparison.Ordinal) &&
            string.Equals(selection.GraphicType, "MANUAL_NET_SELL", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "MANUAL_NET_SELL", StringComparison.Ordinal))
        {
            audience = selection.GroupCode;
        }
        else if (string.Equals(selection.Subject, "지수", StringComparison.Ordinal) &&
            string.Equals(selection.GraphicType, "순매도 상위", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "순매도 상위", StringComparison.Ordinal))
        {
            audience = selection.GroupCode switch
            {
                "개인 순매도 상위(수동)" => "INDIVIDUAL",
                "외국인 순매도 상위(수동)" => "FOREIGN",
                "기관 순매도 상위(수동)" => "INSTITUTION",
                _ => null
            };
        }
        else if (string.Equals(selection.GroupCode, "지수", StringComparison.Ordinal) &&
            string.Equals(selection.GraphicType, "순매도 상위", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "순매도 상위", StringComparison.Ordinal))
        {
            audience = selection.Subject switch
            {
                "개인 순매도 상위(수동)" => "INDIVIDUAL",
                "외국인 순매도 상위(수동)" => "FOREIGN",
                "기관 순매도 상위(수동)" => "INSTITUTION",
                _ => null
            };
        }

        if (audience is null)
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            audience,
            "INDEX",
            "MANUAL_NET_SELL",
            "MANUAL_NET_SELL",
            string.Empty);
        return true;
    }

    private static bool TryCreateNamedVersionedViSelection(
        LegacyNamedPlaylistSelectionView selection,
        out CoreSceneSelection normalized)
    {
        normalized = null!;
        var canonical = string.Equals(selection.GroupCode, "PAGED_VI", StringComparison.Ordinal) &&
            string.Equals(
                selection.Subject,
                MBN_STOCK_WEBVIEW.Core.Playout.Scenes.TrustedViManualReference.SubjectSentinel,
                StringComparison.Ordinal) &&
            string.Equals(selection.GraphicType, "INPUT_ORDER", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "CURRENT", StringComparison.Ordinal);
        var savedVisibleFields = string.Equals(
                selection.GroupCode,
                "지수",
                StringComparison.Ordinal) &&
            selection.Subject.Length > 0 &&
            string.Equals(selection.GraphicType, "5단 표그래프", StringComparison.Ordinal) &&
            string.Equals(selection.Subtype, "VI 발동(수동)", StringComparison.Ordinal);
        if ((!canonical && !savedVisibleFields) ||
            !MBN_STOCK_WEBVIEW.Core.Playout.Scenes.TrustedViManualReference.IsRowVersion(
                selection.DataCode))
        {
            return false;
        }

        normalized = new CoreSceneSelection(
            "PAGED_VI",
            MBN_STOCK_WEBVIEW.Core.Playout.Scenes.TrustedViManualReference.SubjectSentinel,
            "INPUT_ORDER",
            "CURRENT",
            selection.DataCode);
        return true;
    }

    private LegacyOperatorSnapshot CreateSnapshot()
    {
        var searchRows = _searchData.DisplayNames
            .Select((name, index) => new LegacyStockResultRow(index, name))
            .ToArray();
        var cutRows = _cutRows
            .Select(row => new LegacyCutViewRow(
                row.PhysicalIndex,
                row.DisplayOrdinal,
                row.RawLabel,
                row.IsSeparator,
                _nativeSelectedCutRows.Contains(row.PhysicalIndex),
                row.PhysicalIndex == _nativeFocusedCutIndex))
            .ToArray();

        var playlistRows = _playlist
            .Select(row => row with
            {
                IsSelected = _selectedPlaylistRowIds.Contains(row.RowId),
                IsActive = string.Equals(
                    _activePlaylistRowId,
                    row.RowId,
                    StringComparison.Ordinal)
            })
            .ToArray();
        var tabs = _tabOrder
            .Select(tab => new LegacyOperatorTabState(
                tab,
                GetTabLabel(tab),
                tab == _activeTab))
            .ToArray();
        var fixedCatalog = CreateFixedCatalogView();
        var operatorCatalog = _operatorCatalogWorkflow?.Current;

        return new LegacyOperatorSnapshot(
            _revision,
            _searchText,
            Array.AsReadOnly(searchRows),
            _selectedStockIndex,
            Array.AsReadOnly(cutRows),
            Array.AsReadOnly(playlistRows),
            _dialog,
            _statusMessage,
            _statusKind,
            Array.AsReadOnly(tabs),
            _movingAverages,
            fixedCatalog,
            GetActiveIndustrySnapshot(),
            IsThemeActive() ? _themeSnapshot : null,
            IsExpertActive() ? _expertWorkflow?.Current : null,
            IsTradingHaltActive() ? _tradingHaltWorkflow?.Current : null,
            IsComparisonActive() ? _comparisonWorkflow?.Current : null,
            _manualFinancialSnapshot,
            _namedPlaylistWorkflow?.Current,
            IsOverseasActive() ? _overseasWorkflow?.Current : null,
            _manualListsWorkflow?.Current,
            operatorCatalog?.IsOpen == true ? operatorCatalog : null,
            _commandResult,
            _pendingFixedSectionBatch?.CreateView());
    }

    private LegacyFixedCatalogView? CreateFixedCatalogView()
    {
        var market = GetActiveFixedMarket();
        if (market is null)
        {
            return null;
        }

        var sections = _fixedCatalog.GetSections(market.Value)
            .Select((section, index) => new LegacyFixedSectionView(
                index,
                section.Name,
                Array.AsReadOnly(section.Actions
                    .Select(action => new LegacyFixedActionView(
                        action.Id,
                        action.Label,
                        action.Detail,
                        action.Available,
                        action.Prerequisite))
                    .ToArray())))
            .ToArray();
        return new LegacyFixedCatalogView(market.Value, Array.AsReadOnly(sections));
    }

    private LegacyFixedMarket? GetActiveFixedMarket() => _activeTab switch
    {
        LegacyOperatorTabId.Overseas => LegacyFixedMarket.Overseas,
        LegacyOperatorTabId.Exchange => LegacyFixedMarket.Exchange,
        LegacyOperatorTabId.Index => LegacyFixedMarket.Index,
        _ => null
    };

    private MMoneyCoderSharp.Data.IndustryMarket? GetActiveIndustryMarket() => _activeTab switch
    {
        LegacyOperatorTabId.KospiIndustry => MMoneyCoderSharp.Data.IndustryMarket.Kospi,
        LegacyOperatorTabId.KosdaqIndustry => MMoneyCoderSharp.Data.IndustryMarket.Kosdaq,
        _ => null
    };

    private LegacyIndustrySelectionSnapshot? GetActiveIndustrySnapshot() =>
        GetActiveIndustryMarket() is { } market &&
        _industryWorkflow?.Current.Market == market
            ? _industryWorkflow.Current
            : null;

    private bool IsActiveIndustryReady() => GetActiveIndustrySnapshot() is not null;

    private bool IsThemeActive() => _activeTab == LegacyOperatorTabId.Theme;

    private MMoneyCoderSharp.Data.ThemeSession CurrentThemeNxtSession() =>
        _timeProvider.GetLocalNow().Hour <= 12
            ? MMoneyCoderSharp.Data.ThemeSession.PreMarket
            : MMoneyCoderSharp.Data.ThemeSession.AfterMarket;

    private void BeginTabActivation(LegacyOperatorTabId tabId)
    {
        if (tabId == LegacyOperatorTabId.KospiIndustry)
        {
            _industryActivationListLoaded = false;
            _industryWorkflow?.ResetForTabActivation(
                MMoneyCoderSharp.Data.IndustryMarket.Kospi);
        }
        else if (tabId == LegacyOperatorTabId.KosdaqIndustry)
        {
            _industryActivationListLoaded = false;
            _industryWorkflow?.ResetForTabActivation(
                MMoneyCoderSharp.Data.IndustryMarket.Kosdaq);
        }
        else if (tabId == LegacyOperatorTabId.Comparison)
        {
            _comparisonActivationListLoaded = false;
            _comparisonWorkflow?.ResetForTabActivation();
        }
        else if (tabId == LegacyOperatorTabId.Theme)
        {
            _themeActivationListLoaded = false;
            if (_themeWorkflow is not null && _themeSnapshot is not null)
            {
                _themeSnapshot = _themeWorkflow.ResetForTabActivation(
                    _themeSnapshot,
                    CurrentThemeNxtSession());
            }
        }
        else if (tabId == LegacyOperatorTabId.OverseasStocks)
        {
            _overseasWorkflow?.ResetForTabActivation();
        }
        else if (tabId == LegacyOperatorTabId.Expert)
        {
            _expertActivationListLoaded = false;
        }
        else if (tabId == LegacyOperatorTabId.TradingHalt)
        {
            _tradingHaltWorkflow?.ResetForTabActivation();
        }
    }

    private bool IsCurrentTabActivation(
        LegacyOperatorTabId tabId,
        long activationRequest) =>
        _activeTab == tabId &&
        Volatile.Read(ref _tabActivationRequest) == activationRequest;

    private bool IsComparisonActive() => _activeTab == LegacyOperatorTabId.Comparison;

    private bool IsExpertActive() => _activeTab == LegacyOperatorTabId.Expert;

    private bool IsTradingHaltActive() => _activeTab == LegacyOperatorTabId.TradingHalt;

    private bool IsOverseasActive() => _activeTab == LegacyOperatorTabId.OverseasStocks;

    private bool TryGetManualFinancial(
        out LegacyManualFinancialWorkflow workflow,
        out LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        workflow = _manualFinancialWorkflow!;
        snapshot = _manualFinancialSnapshot!;
        return workflow is not null && snapshot is not null;
    }

    private void ApplyManualFinancialListStatus(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        var warnings = new List<string>();
        if (snapshot.ListRejectedItemCount > 0)
        {
            var displayedLegacyRawCount = snapshot.Rows.Count(row =>
                !row.IsPlayoutReady);
            var hiddenUnreadableCount =
                snapshot.ListRejectedItemCount - displayedLegacyRawCount;
            warnings.Add(hiddenUnreadableCount == 0
                ? $"기존 원문 형식 데이터 {displayedLegacyRawCount}행도 " +
                    "목록에 표시되며 선택·편집할 수 있습니다."
                : $"기존 원문 형식 데이터 {displayedLegacyRawCount}행은 표시했고, " +
                    $"이름을 읽을 수 없는 {hiddenUnreadableCount}행은 제외했습니다.");
        }
        if (snapshot.ListIsTruncated)
        {
            warnings.Add(
                $"검색 결과가 {LegacyManualFinancialWorkflow.MaximumListResults}행을 " +
                "초과해 일부만 표시됩니다.");
        }

        _statusMessage = $"{snapshot.Definition.Label} 목록을 불러왔습니다." +
            (warnings.Count == 0 ? string.Empty : " " + string.Join(' ', warnings));
        _statusKind = warnings.Count == 0
            ? LegacyOperatorStatusKind.Information
            : LegacyOperatorStatusKind.Warning;
    }

    private void ApplyManualFinancialResultStatus(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string operation)
    {
        _statusMessage = snapshot.Status switch
        {
            LegacyManualFinancialWorkflowStatus.WriteCommitted =>
                $"수동 재무 {operation} 작업이 완료되었습니다.",
            LegacyManualFinancialWorkflowStatus.WriteRejected =>
                snapshot.LastMessage ?? $"수동 재무 {operation} 작업이 거부되었습니다.",
            LegacyManualFinancialWorkflowStatus.ReadBackRequired =>
                snapshot.LastMessage ?? "저장은 완료됐지만 새로 읽기가 필요합니다.",
            LegacyManualFinancialWorkflowStatus.OutcomeUnknown =>
                snapshot.LastMessage ?? "결과가 불명확하여 수동 재무 쓰기를 차단했습니다.",
            _ => snapshot.LastMessage ?? string.Empty
        };
        _statusKind = snapshot.Status switch
        {
            LegacyManualFinancialWorkflowStatus.WriteCommitted =>
                LegacyOperatorStatusKind.Information,
            LegacyManualFinancialWorkflowStatus.WriteRejected =>
                LegacyOperatorStatusKind.Warning,
            LegacyManualFinancialWorkflowStatus.ReadBackRequired or
                LegacyManualFinancialWorkflowStatus.OutcomeUnknown =>
                LegacyOperatorStatusKind.Error,
            _ => LegacyOperatorStatusKind.Neutral
        };
    }

    private void ShowManualFinancialSaveDialog(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        // GraphE used a one-button MmoneyCoder MessageBox after every explicit
        // save attempt. Only a correlated write plus fresh read-back receives
        // the legacy success wording. Known rollback/read-back/unknown outcomes
        // retain their already-sanitized, specific status instead of being
        // mislabeled as a generic database outage.
        _dialog = snapshot.Status switch
        {
            LegacyManualFinancialWorkflowStatus.WriteCommitted =>
                new LegacyDialogState("저장되었습니다"),
            LegacyManualFinancialWorkflowStatus.WriteRejected or
                LegacyManualFinancialWorkflowStatus.ReadBackRequired or
                LegacyManualFinancialWorkflowStatus.OutcomeUnknown =>
                new LegacyDialogState(_statusMessage),
            _ => null
        };
    }

    private void ApplyManualListsStatus()
    {
        if (_manualListsWorkflow is null)
        {
            return;
        }

        _statusMessage = _manualListsWorkflow.Current.StatusMessage;
        _statusKind = _manualListsWorkflow.Current.StatusKind;
    }

    private async Task<LegacyOperatorSnapshot> OpenPendingFixedSectionManualAsync(
        CancellationToken cancellationToken)
    {
        var pending = _pendingFixedSectionBatch
            ?? throw new InvalidOperationException("A fixed-section batch is not active.");
        var item = pending.CurrentManualItem
            ?? throw new InvalidOperationException("The fixed-section batch has no pending manual item.");
        var target = item.ManualTarget!.Value;

        if (target.Screen == LegacyManualListScreen.NetSell)
        {
            await OpenManualNetSellCoreAsync(target.Audience, cancellationToken)
                .ConfigureAwait(false);
        }
        else
        {
            await OpenManualViCoreAsync(cancellationToken).ConfigureAwait(false);
        }

        var manual = _manualListsWorkflow!.Current;
        var view = pending.CreateView();
        var progress =
            $"{pending.SectionName} 섹션 수동 입력 {view.ManualStepNumber}/{view.ManualStepCount}: {item.Action.Label}. 확인하면 다음 직계 자식으로 계속합니다.";
        _statusMessage = string.IsNullOrWhiteSpace(manual.StatusMessage)
            ? progress
            : $"{progress} {manual.StatusMessage}";
        _statusKind = manual.StatusKind == LegacyOperatorStatusKind.Neutral
            ? LegacyOperatorStatusKind.Information
            : manual.StatusKind;
        Touch();
        return CreateSnapshot();
    }

    private async Task<LegacyOperatorSnapshot> ContinuePendingFixedSectionAsync(
        CancellationToken cancellationToken)
    {
        var pending = _pendingFixedSectionBatch
            ?? throw new InvalidOperationException("A fixed-section batch is not active.");
        if (pending.CurrentManualItem is not null)
        {
            return await OpenPendingFixedSectionManualAsync(cancellationToken)
                .ConfigureAwait(false);
        }

        try
        {
            CommitFixedSectionBatch(pending.Items);
        }
        catch
        {
            _pendingFixedSectionBatch = null;
            throw;
        }

        _pendingFixedSectionBatch = null;
        ApplyFixedSectionAddedStatus(pending.SectionName, pending.Items);
        Touch();
        return CreateSnapshot();
    }

    private void CommitFixedSectionBatch(IReadOnlyList<PendingFixedSectionItem> items)
    {
        var originalCount = _playlist.Count;
        var originalSequence = _playlistSequence;
        var originalActiveRowId = _activePlaylistRowId;
        var originalSelectedRowIds = _selectedPlaylistRowIds.ToArray();
        var originalCursorIndex = _playlistCursorIndex;
        var originalPriorCursorIndex = _playlistPriorCursorIndex;
        try
        {
            foreach (var item in items)
            {
                if (item.Materialized is not null)
                {
                    AppendFixedRow(
                        item.Materialized,
                        item.InitialPage?.PageText ?? throw new InvalidOperationException(
                            "A materialized fixed-section item has no initial page state."));
                }
                else if (item.ManualDraft is not null)
                {
                    AppendManualListRow(item.ManualDraft);
                }
                else
                {
                    throw new InvalidOperationException(
                        "A fixed-section batch item was not materialized.");
                }
            }
        }
        catch
        {
            if (_playlist.Count > originalCount)
            {
                _playlist.RemoveRange(originalCount, _playlist.Count - originalCount);
            }

            _playlistSequence = originalSequence;
            _activePlaylistRowId = originalActiveRowId;
            _selectedPlaylistRowIds.Clear();
            _selectedPlaylistRowIds.UnionWith(originalSelectedRowIds);
            _playlistCursorIndex = originalCursorIndex;
            _playlistPriorCursorIndex = originalPriorCursorIndex;
            throw;
        }
    }

    private static FixedManualTarget? GetFixedManualTarget(string actionId) => actionId switch
    {
        "fixed-245" => new FixedManualTarget(
            LegacyManualListScreen.NetSell,
            S5025ManualAudience.Individual),
        "fixed-246" => new FixedManualTarget(
            LegacyManualListScreen.NetSell,
            S5025ManualAudience.Foreign),
        "fixed-247" => new FixedManualTarget(
            LegacyManualListScreen.NetSell,
            S5025ManualAudience.Institution),
        "fixed-262" or "fixed-293" or "fixed-320" => new FixedManualTarget(
            LegacyManualListScreen.Vi,
            S5025ManualAudience.Individual),
        _ => null
    };

    private async Task<FixedInitialPage> ResolveFixedInitialPageAsync(
        LegacyMaterializedFixedAction materialized,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var pageSize = GetFixedPageSize(materialized.Action);
        if (pageSize is null)
        {
            return FixedInitialPage.SinglePage;
        }

        if (_fixedPagePlanProvider is null)
        {
            return FixedInitialPage.Unavailable;
        }

        var selection = materialized.Selection;
        var entry = new CorePlaylistEntry(
            materialized.Action.Id,
            materialized.Action.Code,
            IsEnabled: true,
            FadeDuration: 6,
            Selection: new CoreSceneSelection(
                selection.GroupCode,
                selection.Subject,
                selection.GraphicType,
                selection.Subtype,
                selection.DataCode));
        try
        {
            var pagePlan = await _fixedPagePlanProvider.CreatePagePlanAsync(
                entry,
                cancellationToken).ConfigureAwait(false);
            ArgumentNullException.ThrowIfNull(pagePlan);
            var expected = CoreScenePaging.CreatePlan(pagePlan.ItemCount, pageSize.Value);
            if (!expected.IsValid ||
                expected.Plan is null ||
                !string.Equals(
                    pagePlan.BuilderKey,
                    materialized.Action.BuilderKey,
                    StringComparison.Ordinal) ||
                pagePlan.PageSize != pageSize.Value ||
                pagePlan.PageCount != expected.Plan.TotalPages ||
                pagePlan.AccessibleItemCount != expected.Plan.AccessibleItemCount ||
                pagePlan.IsTruncated != (expected.Plan.ExcludedItemCount > 0))
            {
                throw new InvalidOperationException(
                    "The fixed-action page preflight returned an invalid paging plan.");
            }

            return new FixedInitialPage($"1/{pagePlan.PageCount}", false);
        }
        catch (OperationCanceledException)
        {
            throw;
        }
        catch (Exception)
        {
            // PageN/Nxt_PageN in UC1 caught data-read failures and still appended
            // the row as 1/0. Keep that exact failure representation; never guess N.
            return FixedInitialPage.Unavailable;
        }
    }

    private static FixedInitialPage CreateFixedInitialPageFallback(
        LegacyFixedAction action) => GetFixedPageSize(action) is null
            ? FixedInitialPage.SinglePage
            : FixedInitialPage.Unavailable;

    private static CoreScenePageSize? GetFixedPageSize(LegacyFixedAction action)
    {
        ArgumentNullException.ThrowIfNull(action);
        var exact = (action.Code, action.BuilderKey) switch
        {
            ("5074", "s5074") => CoreScenePageSize.Five,
            ("5077", "s5077") => CoreScenePageSize.Six,
            ("5088", "s5088") => CoreScenePageSize.Twelve,
            _ => (CoreScenePageSize?)null
        };
        if (exact is not null)
        {
            return exact;
        }

        if (action.Code is "5074" or "5077" or "5088" ||
            action.BuilderKey is "s5074" or "s5077" or "s5088")
        {
            throw new InvalidOperationException(
                "The fixed-action paging alias and builder do not match.");
        }

        return null;
    }

    private void ApplyFixedActionAddedStatus(
        LegacyFixedAction action,
        bool pageCountUnavailable)
    {
        _statusMessage = pageCountUnavailable
            ? $"{action.Label} 항목을 추가했지만 현재 데이터 건수를 읽지 못해 페이지를 1/0으로 표시합니다."
            : $"{action.Label} 항목을 추가했습니다.";
        _statusKind = pageCountUnavailable
            ? LegacyOperatorStatusKind.Warning
            : LegacyOperatorStatusKind.Information;
    }

    private void ApplyFixedSectionAddedStatus(
        string sectionName,
        IReadOnlyList<PendingFixedSectionItem> items)
    {
        var unavailableCount = items.Count(item => item.PageCountUnavailable);
        _statusMessage = unavailableCount == 0
            ? $"{sectionName} 항목 {items.Count}개를 원본 순서로 추가했습니다."
            : $"{sectionName} 항목 {items.Count}개를 원본 순서로 추가했습니다. 데이터 건수를 읽지 못한 {unavailableCount}개 항목은 페이지를 1/0으로 표시합니다.";
        _statusKind = unavailableCount == 0
            ? LegacyOperatorStatusKind.Information
            : LegacyOperatorStatusKind.Warning;
    }

    private void ApplyOperatorCatalogStatus()
    {
        if (_operatorCatalogWorkflow is null)
        {
            return;
        }

        var snapshot = _operatorCatalogWorkflow.Current;
        _statusMessage = snapshot.StatusMessage;
        _statusKind = snapshot.StatusKind switch
        {
            LegacyOperatorCatalogStatusKind.Information => LegacyOperatorStatusKind.Information,
            LegacyOperatorCatalogStatusKind.Warning => LegacyOperatorStatusKind.Warning,
            LegacyOperatorCatalogStatusKind.Error => LegacyOperatorStatusKind.Error,
            _ => LegacyOperatorStatusKind.Neutral
        };
    }

    private void AppendManualListRow(LegacyManualListPlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-manual-list-{++_playlistSequence:D8}",
            true,
            draft.MarketText,
            draft.StockName,
            draft.GraphicType,
            draft.Subtype,
            draft.PageText,
            draft.Selection.DataCode,
            draft.RawCutLabel,
            draft.FadeDuration,
            SceneAlias: draft.SceneAlias,
            PlayoutSelection: draft.Selection));
    }

    private void AppendFixedRow(
        LegacyMaterializedFixedAction materialized,
        string pageText)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(pageText);
        var action = materialized.Action;
        var selection = materialized.Selection;
        var visible = ProjectLegacyFixedVisibleSelection(action);
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-fixed-{++_playlistSequence:D8}",
            true,
            visible.GroupCode,
            visible.Subject,
            visible.GraphicType,
            visible.Subtype,
            pageText,
            visible.DataCode,
            action.Label,
            6,
            SceneAlias: action.Code,
            PlayoutSelection: new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
                selection.GroupCode,
                selection.Subject,
                selection.GraphicType,
                selection.Subtype,
                selection.DataCode)));
    }

    private void AppendIndustryRow(LegacyIndustryPlaylistDraft draft, int itemCount)
    {
        var split = draft.Detail.IndexOf('_');
        var graphicType = split > 0 ? draft.Detail[..split] : draft.Detail;
        var subtype = split > 0 && split < draft.Detail.Length - 1
            ? draft.Detail[(split + 1)..]
            : string.Empty;
        var pageCount = draft.PageSize > 0
            ? Math.Clamp(
                (int)Math.Ceiling(itemCount / (double)draft.PageSize),
                1,
                20)
            : 1;
        var marketText = draft.Market == MMoneyCoderSharp.Data.IndustryMarket.Kospi
            ? "업종_코스피"
            : "업종_코스닥";
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-industry-{++_playlistSequence:D8}",
            draft.IsEnabled,
            marketText,
            draft.Title,
            graphicType,
            subtype,
            $"1/{pageCount}",
            draft.Selection.DataCode,
            draft.Detail,
            draft.FadeDuration,
            SceneAlias: draft.SceneAlias,
            PlayoutSelection: new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
                draft.Selection.GroupCode,
                draft.Selection.Subject,
                draft.Selection.GraphicType,
                draft.Selection.Subtype,
                draft.Selection.DataCode)));
    }

    private void AppendOverseasRow(LegacyOverseasPlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-overseas-{++_playlistSequence:D8}",
            draft.IsEnabled,
            draft.VisibleFields.Market,
            draft.VisibleFields.StockName,
            draft.VisibleFields.GraphicType,
            draft.VisibleFields.Subtype,
            draft.VisibleFields.PageText,
            draft.Selection.DataCode,
            draft.Detail,
            draft.FadeDuration,
            SceneAlias: draft.SceneAlias,
            PlayoutSelection: new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
                draft.Selection.GroupCode,
                draft.Selection.Subject,
                draft.Selection.GraphicType,
                draft.Selection.Subtype,
                draft.Selection.DataCode)));
    }

    private void AppendThemeRow(LegacyThemePlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-theme-{++_playlistSequence:D8}",
            true,
            draft.Selection.GroupCode == "PAGED_THEME" ? "테마" : "테마_NXT",
            draft.Title,
            draft.PlaylistGraphicType,
            draft.PlaylistSubtype,
            draft.PagePlan.InitialPageText,
            draft.Selection.DataCode,
            draft.Detail,
            6,
            SceneAlias: draft.Code,
            PlayoutSelection: new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
                draft.Selection.GroupCode,
                draft.Selection.Subject,
                draft.Selection.GraphicType,
                draft.Selection.Subtype,
                draft.Selection.DataCode,
                draft.Selection.ExactSubject)));
    }

    private void AppendComparisonRow(LegacyGenericPlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-comparison-{++_playlistSequence:D8}",
            draft.IsEnabled,
            "비교",
            draft.Title,
            draft.BuilderKey,
            draft.Detail,
            "1/1",
            draft.Selection.DataCode,
            draft.Source,
            draft.FadeDuration,
            SceneAlias: draft.CutCode,
            PlayoutSelection: draft.Selection));
    }

    private void AppendExpertRow(LegacyExpertPlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-expert-{++_playlistSequence:D8}",
            draft.IsEnabled,
            "전문가",
            draft.Title,
            "5단 표그래프",
            "현재가",
            draft.PageText,
            draft.ExpertCode,
            draft.Detail,
            draft.FadeDuration,
            SceneAlias: draft.CutCode,
            PlayoutSelection: draft.Selection));
    }

    private void AppendTradingHaltRow(LegacyTradingHaltPlaylistDraft draft)
    {
        var isCandle = draft.CandlePeriodDays.HasValue;
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-halt-{++_playlistSequence:D8}",
            draft.IsEnabled,
            draft.Market == "kospi" ? "코스피" : "코스닥",
            draft.Title,
            isCandle ? "캔들그래프" : "1열판기본",
            isCandle ? "거래정지_120일" : "현재가",
            draft.PageText,
            draft.StockCode,
            draft.Detail,
            draft.FadeDuration,
            SceneAlias: draft.CutCode,
            PlayoutSelection: draft.Selection));
    }

    private void AppendManualFinancialRow(LegacyManualFinancialPlaylistDraft draft)
    {
        AppendPlaylistRow(new LegacyOperatorPlaylistRow(
            $"legacy-manual-financial-{++_playlistSequence:D8}",
            true,
            draft.Market,
            draft.Title,
            draft.Detail,
            draft.CutSelection.Selection.GraphicType,
            "1/1",
            draft.StockCode,
            draft.Detail,
            6,
            SceneAlias: draft.Code,
            PlayoutSelection: draft.CutSelection.Selection));
    }

    private static string GetTabLabel(LegacyOperatorTabId tabId) => tabId switch
    {
        LegacyOperatorTabId.Overseas => "해외",
        LegacyOperatorTabId.Exchange => "환율",
        LegacyOperatorTabId.Index => "지수",
        LegacyOperatorTabId.KospiIndustry => "코스피",
        LegacyOperatorTabId.KosdaqIndustry => "코스닥",
        LegacyOperatorTabId.Comparison => "비교",
        LegacyOperatorTabId.Theme => "테마",
        LegacyOperatorTabId.OverseasStocks => "해외시장",
        LegacyOperatorTabId.Expert => "전문가",
        LegacyOperatorTabId.TradingHalt => "거래정지",
        _ => throw new ArgumentOutOfRangeException(nameof(tabId), tabId, null)
    };

    private bool IsCutIndex(int physicalIndex) =>
        physicalIndex >= 0 && physicalIndex < _cutRows.Count;

    private static IReadOnlyList<LegacyCutCatalogRow> CreateValidatedCutRows(
        IReadOnlyList<LegacyCutCatalogRow>? cutRows)
    {
        var source = cutRows ?? LegacyStockCutCatalog.Rows;
        if (source.Count is < 1 or > LegacyRuntimeStockCutMenuLoader.MaximumRows)
        {
            throw new ArgumentException(
                "The stock cut menu must contain from 1 through 50 physical rows.",
                nameof(cutRows));
        }

        var copiedRows = new LegacyCutCatalogRow[source.Count];
        var displayOrdinal = 0;
        for (var index = 0; index < source.Count; index++)
        {
            var row = source[index] ?? throw new ArgumentException(
                "The stock cut menu cannot contain a null row.",
                nameof(cutRows));
            if (row.PhysicalIndex != index)
            {
                throw new ArgumentException(
                    "Stock cut physical indices must be contiguous and zero based.",
                    nameof(cutRows));
            }

            if (row.IsSeparator)
            {
                if (row.DisplayOrdinal is not null ||
                    !string.IsNullOrEmpty(row.RawLabel))
                {
                    throw new ArgumentException(
                        "A stock cut separator must have no display ordinal or label.",
                        nameof(cutRows));
                }
            }
            else
            {
                displayOrdinal++;
                if (row.DisplayOrdinal != displayOrdinal)
                {
                    throw new ArgumentException(
                        "Stock cut display ordinals must be contiguous across active rows.",
                        nameof(cutRows));
                }

                var isKnown = LegacyStockCutCatalog.Rows.Any(candidate =>
                    !candidate.IsSeparator && string.Equals(
                        candidate.RawLabel,
                        row.RawLabel,
                        StringComparison.Ordinal));
                if (!isKnown)
                {
                    throw new ArgumentException(
                        "The stock cut menu contains an unknown cut label.",
                        nameof(cutRows));
                }
            }

            copiedRows[index] = row;
        }

        if (displayOrdinal == 0)
        {
            throw new ArgumentException(
                "The stock cut menu must contain at least one active cut.",
                nameof(cutRows));
        }

        return Array.AsReadOnly(copiedRows);
    }

    private int FindPlaylistIndex(string rowId) => _playlist.FindIndex(row =>
        string.Equals(row.RowId, rowId, StringComparison.Ordinal));

    private static int FindOriginalPlaylistIndex(
        IReadOnlyList<LegacyOperatorPlaylistRow> rows,
        string? rowId)
    {
        if (rowId is null)
        {
            return -1;
        }

        for (var index = 0; index < rows.Count; index++)
        {
            if (string.Equals(rows[index].RowId, rowId, StringComparison.Ordinal))
            {
                return index;
            }
        }

        return -1;
    }

    private void AppendPlaylistRow(LegacyOperatorPlaylistRow row)
    {
        ArgumentNullException.ThrowIfNull(row);
        row = NormalizeMovingAverageIdentity(row);
        var wasEmpty = _playlist.Count == 0;
        _playlist.Add(row);
        if (wasEmpty)
        {
            SelectInitialPlaylistCandidate();
        }
    }

    private static LegacyOperatorPlaylistRow NormalizeMovingAverageIdentity(
        LegacyOperatorPlaylistRow row)
    {
        var cutCode = row.SceneAlias.Length > 0
            ? row.SceneAlias
            : LegacyStockCutCatalog.ResolveCutAlias(
                row.RawCutLabel,
                row.StockName.Contains("(NXT)", StringComparison.Ordinal));
        if (!MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneMovingAverageOverlay
                .Supports(cutCode))
        {
            return row;
        }

        var graphicType = RemoveCapturedMovingAverageFlags(row.GraphicType);
        var selection = row.PlayoutSelection;
        var normalizedSelection = selection is null
            ? null
            : selection with
            {
                GraphicType = RemoveCapturedMovingAverageFlags(selection.GraphicType)
            };
        return string.Equals(graphicType, row.GraphicType, StringComparison.Ordinal) &&
            Equals(normalizedSelection, selection)
                ? row
                : row with
                {
                    GraphicType = graphicType,
                    PlayoutSelection = normalizedSelection
                };
    }

    private static string RemoveCapturedMovingAverageFlags(string graphicType) =>
        graphicType is "MA5" or "MA20" or "MA5,MA20"
            ? string.Empty
            : graphicType;

    private void SelectInitialPlaylistCandidate()
    {
        if (_playlist.Count == 0)
        {
            return;
        }

        _activePlaylistRowId = _playlist[0].RowId;
        // MainForm corrected m_rowidx to zero for the first F8 candidate, but it
        // did not populate bufdel until an actual playlist row click occurred.
        _selectedPlaylistRowIds.Clear();
        _playlistCursorIndex = 0;
        _playlistPriorCursorIndex = 0;
    }

    private string ResolveSurvivingPlaylistRowId(
        IReadOnlyList<LegacyOperatorPlaylistRow> originalRows,
        ISet<string> removedIds,
        string? preferredRowId,
        int preferredIndex)
    {
        if (preferredRowId is not null && FindPlaylistIndex(preferredRowId) >= 0)
        {
            return preferredRowId;
        }

        var resolvedOriginalIndex = FindNextThenPreviousSurvivor(
            originalRows,
            removedIds,
            preferredIndex);
        return originalRows[resolvedOriginalIndex].RowId;
    }

    private int ResolveSurvivingPlaylistIndex(
        IReadOnlyList<LegacyOperatorPlaylistRow> originalRows,
        ISet<string> removedIds,
        string preferredRowId,
        int preferredIndex)
    {
        var survivingIndex = FindPlaylistIndex(preferredRowId);
        if (survivingIndex >= 0)
        {
            return survivingIndex;
        }

        var resolvedOriginalIndex = FindNextThenPreviousSurvivor(
            originalRows,
            removedIds,
            preferredIndex);
        return FindPlaylistIndex(originalRows[resolvedOriginalIndex].RowId);
    }

    private static int FindNextThenPreviousSurvivor(
        IReadOnlyList<LegacyOperatorPlaylistRow> originalRows,
        ISet<string> removedIds,
        int preferredIndex)
    {
        if (preferredIndex < 0)
        {
            for (var index = 0; index < originalRows.Count; index++)
            {
                if (!removedIds.Contains(originalRows[index].RowId))
                {
                    return index;
                }
            }
        }

        var start = Math.Clamp(preferredIndex, 0, originalRows.Count - 1);
        for (var index = start + 1; index < originalRows.Count; index++)
        {
            if (!removedIds.Contains(originalRows[index].RowId))
            {
                return index;
            }
        }

        for (var index = start - 1; index >= 0; index--)
        {
            if (!removedIds.Contains(originalRows[index].RowId))
            {
                return index;
            }
        }

        throw new InvalidOperationException("삭제 후 남은 플레이리스트 행을 찾지 못했습니다.");
    }

    private void AddPlaylistRange(int firstIndex, int lastIndex)
    {
        var first = Math.Max(0, Math.Min(firstIndex, lastIndex));
        var last = Math.Min(_playlist.Count - 1, Math.Max(firstIndex, lastIndex));
        for (var index = first; index <= last; index++)
        {
            _selectedPlaylistRowIds.Add(_playlist[index].RowId);
        }
    }

    private void ResetPlaylistSelection()
    {
        _selectedPlaylistRowIds.Clear();
        _activePlaylistRowId = null;
        _playlistCursorIndex = 0;
        _playlistPriorCursorIndex = 0;
    }

    private void CompleteCommand(string command, string targetId, bool succeeded)
    {
        _commandResult = new LegacyOperatorCommandResult(
            checked(++_commandSequence),
            command,
            targetId,
            succeeded);
    }

    private readonly record struct FixedManualTarget(
        LegacyManualListScreen Screen,
        S5025ManualAudience Audience);

    private readonly record struct FixedInitialPage(
        string PageText,
        bool PageCountUnavailable)
    {
        internal static FixedInitialPage SinglePage { get; } = new("1/1", false);

        internal static FixedInitialPage Unavailable { get; } = new("1/0", true);
    }

    private sealed class PendingFixedSectionItem
    {
        internal PendingFixedSectionItem(
            LegacyFixedAction action,
            LegacyMaterializedFixedAction materialized,
            FixedInitialPage initialPage)
        {
            Action = action;
            Materialized = materialized;
            InitialPage = initialPage;
        }

        internal PendingFixedSectionItem(
            LegacyFixedAction action,
            FixedManualTarget manualTarget)
        {
            Action = action;
            ManualTarget = manualTarget;
        }

        internal LegacyFixedAction Action { get; }

        internal LegacyMaterializedFixedAction? Materialized { get; }

        internal FixedInitialPage? InitialPage { get; }

        internal FixedManualTarget? ManualTarget { get; }

        internal LegacyManualListPlaylistDraft? ManualDraft { get; private set; }

        internal bool PageCountUnavailable =>
            InitialPage?.PageCountUnavailable == true;

        internal bool IsStaged => Materialized is not null || ManualDraft is not null;

        internal void StageManual(LegacyManualListPlaylistDraft draft)
        {
            ArgumentNullException.ThrowIfNull(draft);
            if (ManualTarget is null || ManualDraft is not null)
            {
                throw new InvalidOperationException(
                    "The fixed-section manual item cannot be staged in its current state.");
            }

            ManualDraft = draft;
        }
    }

    private sealed class PendingFixedSectionBatch
    {
        internal PendingFixedSectionBatch(
            LegacyFixedMarket market,
            int sectionIndex,
            string sectionName,
            IReadOnlyList<PendingFixedSectionItem> items,
            int manualCount)
        {
            Market = market;
            SectionIndex = sectionIndex;
            SectionName = sectionName;
            Items = items;
            ManualCount = manualCount;
        }

        internal LegacyFixedMarket Market { get; }

        internal int SectionIndex { get; }

        internal string SectionName { get; }

        internal IReadOnlyList<PendingFixedSectionItem> Items { get; }

        internal int ManualCount { get; }

        internal PendingFixedSectionItem? CurrentManualItem => Items.FirstOrDefault(item =>
            item.ManualTarget is not null && item.ManualDraft is null);

        internal bool IsCurrentManual(
            LegacyManualListScreen screen,
            S5025ManualAudience audience)
        {
            var target = CurrentManualItem?.ManualTarget;
            return target is not null && target.Value.Screen == screen &&
                (screen == LegacyManualListScreen.Vi || target.Value.Audience == audience);
        }

        internal void StageCurrentManual(LegacyManualListPlaylistDraft draft)
        {
            var current = CurrentManualItem
                ?? throw new InvalidOperationException(
                    "The fixed-section batch has no pending manual item.");
            current.StageManual(draft);
        }

        internal LegacyFixedSectionBatchView CreateView()
        {
            var current = CurrentManualItem
                ?? throw new InvalidOperationException(
                    "A completed fixed-section batch cannot be projected as pending.");
            var manualStep = Items
                .Where(item => item.ManualTarget is not null)
                .TakeWhile(item => !ReferenceEquals(item, current))
                .Count() + 1;
            return new LegacyFixedSectionBatchView(
                SectionIndex,
                SectionName,
                Items.Count,
                Items.Count(item => item.IsStaged),
                manualStep,
                ManualCount,
                current.Action.Id,
                current.Action.Label);
        }
    }

    private enum PendingNativeConfirmationKind
    {
        DeleteTheme,
        DeleteExpert,
        ClearThemeDraft,
        DeleteAllComparisonPairs,
        ImportLegacyComparisonPairs,
        DeleteAllPlaylistRows,
        DeleteManualFinancial,
        DeleteAllManualFinancial,
        DeleteAllManualViItems
    }

    private sealed record PendingNativeConfirmation(PendingNativeConfirmationKind Kind)
    {
        internal MMoneyCoderSharp.Data.ThemeSelectionIdentity? ThemeIdentity { get; init; }

        internal MMoneyCoderSharp.Data.ExpertSelectionIdentity? ExpertIdentity { get; init; }

        internal long CatalogRevision { get; init; }

        internal long ComparisonRevision { get; init; }

        internal IReadOnlyList<string>? PlaylistRowIds { get; init; }

        internal long ManualFinancialRevision { get; init; }

        internal IReadOnlyList<string>? ManualViItemIds { get; init; }

        internal static PendingNativeConfirmation ForTheme(
            MMoneyCoderSharp.Data.ThemeSelectionIdentity identity) =>
            new(PendingNativeConfirmationKind.DeleteTheme)
            {
                ThemeIdentity = identity
            };

        internal static PendingNativeConfirmation ForExpert(
            MMoneyCoderSharp.Data.ExpertSelectionIdentity identity) =>
            new(PendingNativeConfirmationKind.DeleteExpert)
            {
                ExpertIdentity = identity
            };

        internal static PendingNativeConfirmation ForThemeDraftClear(long revision) =>
            new(PendingNativeConfirmationKind.ClearThemeDraft)
            {
                CatalogRevision = revision
            };

        internal static PendingNativeConfirmation ForComparisonDeleteAll(long revision) =>
            new(PendingNativeConfirmationKind.DeleteAllComparisonPairs)
            {
                ComparisonRevision = revision
            };

        internal static PendingNativeConfirmation ForComparisonImport(long revision) =>
            new(PendingNativeConfirmationKind.ImportLegacyComparisonPairs)
            {
                ComparisonRevision = revision
            };

        internal static PendingNativeConfirmation ForPlaylistDeleteAll(
            IReadOnlyList<string> rowIds) =>
            new(PendingNativeConfirmationKind.DeleteAllPlaylistRows)
            {
                PlaylistRowIds = rowIds
            };

        internal static PendingNativeConfirmation ForManualFinancialDelete(
            bool all,
            long revision) =>
            new(all
                ? PendingNativeConfirmationKind.DeleteAllManualFinancial
                : PendingNativeConfirmationKind.DeleteManualFinancial)
            {
                ManualFinancialRevision = revision
            };

        internal static PendingNativeConfirmation ForManualViDeleteAll(
            IReadOnlyList<string> itemIds) =>
            new(PendingNativeConfirmationKind.DeleteAllManualViItems)
            {
                ManualViItemIds = itemIds
            };
    }

    private void Touch() => _revision++;
}
