using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyBridge;

public abstract record LegacyUiIntent;

public sealed record LegacyReadyIntent : LegacyUiIntent;

public sealed record LegacySearchStocksIntent(string Text, LegacySearchTrigger Trigger)
    : LegacyUiIntent;

public sealed record LegacySelectStockIntent(int ResultIndex) : LegacyUiIntent;

public sealed record LegacyCutPointerDownIntent(
    int PhysicalIndex,
    bool Control,
    bool Shift,
    int X,
    int Y,
    long TimestampMilliseconds,
    bool AllowAppend) : LegacyUiIntent;

public sealed record LegacyCutKeyDownIntent(
    LegacyCutKeyboardKey Key,
    bool Control,
    bool Shift) : LegacyUiIntent;

public sealed record LegacyClearCutSelectionIntent : LegacyUiIntent;

public sealed record LegacyDropSelectedCutsIntent : LegacyUiIntent;

public sealed record LegacySetPlaylistEnabledIntent(string RowId, bool Enabled)
    : LegacyUiIntent;

public sealed record LegacySetAllPlaylistEnabledIntent(bool Enabled)
    : LegacyUiIntent;

public sealed record LegacySelectPlaylistRowIntent(
    string RowId,
    bool Control,
    bool Shift) : LegacyUiIntent;

public sealed record LegacyActivatePlaylistRowIntent(string RowId) : LegacyUiIntent;

public sealed record LegacyMoveSelectedPlaylistRowsIntent(
    LegacyPlaylistMoveDirection Direction) : LegacyUiIntent;

public sealed record LegacyReorderPlaylistRowsIntent(
    string SourceRowId,
    string TargetRowId,
    LegacyDragDropPosition Position) : LegacyUiIntent;

public sealed record LegacyDeleteSelectedPlaylistRowsIntent : LegacyUiIntent;

public sealed record LegacyDeleteAllPlaylistRowsIntent : LegacyUiIntent;

public sealed record LegacyToggleActivePlaylistEnabledIntent : LegacyUiIntent;

public sealed record LegacySelectPlaylistBoundaryIntent(bool Last) : LegacyUiIntent;

public sealed record LegacySelectTabIntent(LegacyOperatorTabId TabId) : LegacyUiIntent;

public sealed record LegacySwapTabsIntent(
    LegacyOperatorTabId Source,
    LegacyOperatorTabId Target) : LegacyUiIntent;

public sealed record LegacyHoverSwapTabsIntent(
    LegacyOperatorTabId Source,
    LegacyOperatorTabId Target,
    int ExpectedSourceIndex,
    int ExpectedTargetIndex) : LegacyUiIntent;

public sealed record LegacySetMovingAveragesIntent(bool Ma5, bool Ma20) : LegacyUiIntent;

public sealed record LegacyActivateFixedActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacyActivateFixedSectionIntent(int SectionIndex) : LegacyUiIntent;

public sealed record LegacySelectIndustryIntent(int Index, bool DoubleClick) : LegacyUiIntent;

public sealed record LegacySetIndustryComparisonTextIntent(
    LegacyIndustryComparisonSlot Slot,
    string Value) : LegacyUiIntent;

public sealed record LegacySwapIndustriesIntent : LegacyUiIntent;

public sealed record LegacyActivateIndustryActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacyActivateIndustrySectionIntent(int SectionIndex) : LegacyUiIntent;

public sealed record LegacySelectOverseasFixedIndexIntent(string TargetId) : LegacyUiIntent;

public sealed record LegacySearchOverseasIndustriesIntent(string Query) : LegacyUiIntent;

public sealed record LegacySearchOverseasStocksIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectOverseasIndustryIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacySelectOverseasStockIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacyActivateOverseasActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacySearchComparisonDomesticIntent(string Query) : LegacyUiIntent;

public sealed record LegacySearchComparisonWorldIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectComparisonDomesticIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacySelectComparisonWorldIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacySelectComparisonFixedIntent(string TargetId) : LegacyUiIntent;

public sealed record LegacyChooseComparisonDomesticIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacyChooseComparisonWorldIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacyChooseComparisonFixedIntent(string TargetId) : LegacyUiIntent;

public sealed record LegacySwapComparisonTargetsIntent : LegacyUiIntent;

public sealed record LegacyClearComparisonTargetsIntent : LegacyUiIntent;

public sealed record LegacyAddComparisonPairIntent : LegacyUiIntent;

public sealed record LegacyImportComparisonPairsIntent : LegacyUiIntent;

public sealed record LegacySelectComparisonPairIntent(string RowId) : LegacyUiIntent;

public sealed record LegacyMoveComparisonPairIntent(LegacyComparisonPairMoveDirection Direction)
    : LegacyUiIntent;

public sealed record LegacyDeleteSelectedComparisonPairIntent : LegacyUiIntent;

public sealed record LegacyDeleteAllComparisonPairsIntent : LegacyUiIntent;

public sealed record LegacyActivateComparisonActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacyActivateComparisonSectionIntent(int SectionIndex) : LegacyUiIntent;

// UC4 has no NXT-session control. The native controller chooses it from its
// local clock, so the WebView can supply only the search text.
public sealed record LegacySearchThemesIntent(string Query)
    : LegacyUiIntent;

public sealed record LegacySelectThemeIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyActivateThemeResultIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacySetThemeSortIntent(string SortId) : LegacyUiIntent;

public sealed record LegacyActivateThemeActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacySearchExpertsIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectExpertIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacyActivateExpertActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacySearchTradingHaltsIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectTradingHaltIntent(string SelectionId) : LegacyUiIntent;

public sealed record LegacyToggleTradingHaltSortIntent : LegacyUiIntent;

public sealed record LegacyActivateTradingHaltActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacyOpenOperatorCatalogIntent(LegacyOperatorCatalogEntity Entity)
    : LegacyUiIntent;

public sealed record LegacyBeginUc4ThemeCatalogIntent : LegacyUiIntent;

public sealed record LegacyEditUc4SelectedThemeIntent : LegacyUiIntent;

public sealed record LegacyDeleteUc4SelectedThemeIntent : LegacyUiIntent;

public sealed record LegacyBeginUc6ExpertCatalogIntent : LegacyUiIntent;

public sealed record LegacyEditUc6SelectedExpertIntent : LegacyUiIntent;

public sealed record LegacyDeleteUc6SelectedExpertIntent : LegacyUiIntent;

public sealed record LegacyCloseOperatorCatalogIntent : LegacyUiIntent;

public sealed record LegacySearchOperatorCatalogIntent(string Query, ThemeSession NxtSession)
    : LegacyUiIntent;

public sealed record LegacySelectOperatorCatalogResultIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyBeginCreateThemeCatalogIntent(ThemeMarket Market) : LegacyUiIntent;

public sealed record LegacyChangeCreateThemeMarketIntent(
    ThemeMarket Market,
    string EditorName) : LegacyUiIntent;

public sealed record LegacyBeginCreateExpertCatalogIntent : LegacyUiIntent;

public sealed record LegacySearchOperatorCatalogStocksIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectOperatorCatalogStockIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyAddOperatorCatalogStockIntent(decimal? BuyAmount) : LegacyUiIntent;

public sealed record LegacyActivateOperatorCatalogStockIntent(
    string ResultId,
    decimal? BuyAmount) : LegacyUiIntent;

public sealed record LegacyMoveOperatorCatalogDraftRowIntent(
    string RowId,
    LegacyOperatorCatalogMoveDirection Direction) : LegacyUiIntent;

public sealed record LegacyReorderOperatorCatalogDraftRowIntent(
    string SourceRowId,
    string TargetRowId,
    LegacyDragDropPosition Position) : LegacyUiIntent;

public sealed record LegacyRemoveOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent;

public sealed record LegacySelectOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent;

public sealed record LegacyRemoveActiveOperatorCatalogDraftRowIntent : LegacyUiIntent;

public sealed record LegacyClearOperatorCatalogDraftRowsIntent : LegacyUiIntent;

public sealed record LegacySetOperatorCatalogDraftBuyAmountIntent(
    string RowId,
    decimal? BuyAmount) : LegacyUiIntent;

public sealed record LegacyCheckOperatorCatalogThemeNameIntent(string Name) : LegacyUiIntent;

public sealed record LegacySaveOperatorCatalogIntent(string Name) : LegacyUiIntent;

public sealed record LegacyConfirmNativeDialogIntent : LegacyUiIntent;

public sealed record LegacyCancelNativeDialogIntent : LegacyUiIntent;

public sealed record LegacyOpenManualFinancialIntent(ManualFinancialScreenKind Screen)
    : LegacyUiIntent;

public sealed record LegacyCloseManualFinancialIntent : LegacyUiIntent;

public sealed record LegacySearchManualFinancialIntent(string Query) : LegacyUiIntent;

public sealed record LegacyFindManualFinancialIntent(
    string Query,
    LegacyManualFinancialFindDirection Direction,
    long Generation) : LegacyUiIntent;

public sealed record LegacyToggleManualFinancialNameSortIntent(long Generation) : LegacyUiIntent;

public sealed record LegacyLoadManualFinancialIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyActivateManualFinancialResultIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyBeginManualFinancialCreateIntent : LegacyUiIntent;

public sealed record LegacySearchManualFinancialStocksIntent(string StockName) : LegacyUiIntent;

public sealed record LegacySelectManualFinancialStockIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacySaveManualFinancialIntent(ManualFinancialRecord Record)
    : LegacyUiIntent;

public sealed record LegacySaveManualFinancialRawIntent(ManualFinancialRawRecord Record)
    : LegacyUiIntent;

public sealed record LegacyDeleteManualFinancialIntent : LegacyUiIntent;

public sealed record LegacyDeleteAllManualFinancialIntent : LegacyUiIntent;

public sealed record LegacyActivateManualFinancialActionIntent(string ActionId) : LegacyUiIntent;

public sealed record LegacyOpenManualListIntent(
    LegacyManualListScreen Screen,
    S5025ManualAudience Audience) : LegacyUiIntent;

public sealed record LegacyCloseManualListIntent : LegacyUiIntent;

public sealed record LegacyRefreshManualListIntent : LegacyUiIntent;

public sealed record LegacySetManualNetSellCellIntent(
    string RowId,
    LegacyManualNetSellField Field,
    string Value) : LegacyUiIntent;

public sealed record LegacySaveManualNetSellIntent : LegacyUiIntent;

public sealed record LegacyAddManualNetSellCutIntent : LegacyUiIntent;

public sealed record LegacySearchManualViIntent(string Query) : LegacyUiIntent;

public sealed record LegacySelectManualViResultIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyAddManualViResultIntent(string ResultId) : LegacyUiIntent;

public sealed record LegacyMoveManualViItemIntent(
    string ItemId,
    LegacyManualListMoveDirection Direction) : LegacyUiIntent;

public sealed record LegacyDeleteManualViItemIntent(string ItemId) : LegacyUiIntent;

public sealed record LegacyDeleteAllManualViItemsIntent : LegacyUiIntent;

public sealed record LegacySaveManualViIntent : LegacyUiIntent;

public sealed record LegacyAddManualViCutIntent : LegacyUiIntent;

public sealed record LegacyConfirmManualListIntent : LegacyUiIntent;

public sealed record LegacyImportManualListsIntent : LegacyUiIntent;

public sealed record LegacyRefreshNamedPlaylistsIntent(string RequestId) : LegacyUiIntent;

public sealed record LegacySelectNamedPlaylistIntent(
    string DefinitionId,
    string RequestId) : LegacyUiIntent;

public sealed record LegacyLoadSelectedNamedPlaylistIntent : LegacyUiIntent;

public sealed record LegacyLoadNamedPlaylistByIdIntent(string DefinitionId) : LegacyUiIntent;

public sealed record LegacyCreateNamedPlaylistIntent(string Title, string RequestId) : LegacyUiIntent;

public sealed record LegacySaveCurrentNamedPlaylistIntent : LegacyUiIntent;

public sealed record LegacySaveCurrentNamedPlaylistToIntent(
    string DefinitionId,
    bool ShowSavedAlert = false) : LegacyUiIntent;

public sealed record LegacyDeleteSelectedNamedPlaylistIntent(string RequestId) : LegacyUiIntent;

public sealed record LegacyDismissDialogIntent : LegacyUiIntent;

public sealed record LegacyInteractionMetrics(
    int CutDragWidthDips,
    int CutDragHeightDips,
    uint SourceDpi = 96,
    double HostRasterizationScale = 1d,
    int DoubleClickTimeMilliseconds = 500)
{
    public const uint CssPixelsPerInch = 96;
    public const int FallbackCutDragWidth = 4;
    public const int FallbackCutDragHeight = 4;
    public const int FallbackDoubleClickTimeMilliseconds = 500;
    public const int MaximumDoubleClickTimeMilliseconds = 5_000;

    public static LegacyInteractionMetrics Default { get; } = new(
        FallbackCutDragWidth,
        FallbackCutDragHeight,
        CssPixelsPerInch,
        1d,
        FallbackDoubleClickTimeMilliseconds);

    public static LegacyInteractionMetrics FromPixelsAtDpi(
        int cutDragWidthPixels,
        int cutDragHeightPixels,
        uint sourceDpi,
        double hostRasterizationScale = 1d,
        int doubleClickTimeMilliseconds = FallbackDoubleClickTimeMilliseconds) =>
        sourceDpi == 0
            ? Default with
            {
                HostRasterizationScale = NormalizeHostScale(hostRasterizationScale),
                DoubleClickTimeMilliseconds = NormalizeDoubleClickTime(
                    doubleClickTimeMilliseconds)
            }
            : new(
                ToDips(
                    cutDragWidthPixels,
                    sourceDpi,
                    FallbackCutDragWidth),
                ToDips(
                    cutDragHeightPixels,
                    sourceDpi,
                    FallbackCutDragHeight),
                sourceDpi,
                NormalizeHostScale(hostRasterizationScale),
                NormalizeDoubleClickTime(doubleClickTimeMilliseconds));

    internal static LegacyInteractionMetrics Normalize(LegacyInteractionMetrics? metrics) =>
        metrics is null
            ? Default
            : new(
                metrics.CutDragWidthDips > 0
                    ? metrics.CutDragWidthDips
                    : FallbackCutDragWidth,
                metrics.CutDragHeightDips > 0
                    ? metrics.CutDragHeightDips
                    : FallbackCutDragHeight,
                metrics.SourceDpi > 0 ? metrics.SourceDpi : CssPixelsPerInch,
                NormalizeHostScale(metrics.HostRasterizationScale),
                NormalizeDoubleClickTime(metrics.DoubleClickTimeMilliseconds));

    private static double NormalizeHostScale(double hostRasterizationScale) =>
        double.IsFinite(hostRasterizationScale) && hostRasterizationScale > 0d
            ? hostRasterizationScale
            : 1d;

    private static int NormalizeDoubleClickTime(int milliseconds) =>
        milliseconds is > 0 and <= MaximumDoubleClickTimeMilliseconds
            ? milliseconds
            : FallbackDoubleClickTimeMilliseconds;

    private static int ToDips(int pixelsAtSourceDpi, uint sourceDpi, int fallback) =>
        pixelsAtSourceDpi <= 0
            ? fallback
            : Math.Max(
                1,
                (int)Math.Round(
                    pixelsAtSourceDpi * (double)CssPixelsPerInch / sourceDpi,
                    MidpointRounding.AwayFromZero));
}

public static class LegacyBridgeProtocol
{
    public const string TrustedHost = "legacy-parity.mbn.local";

    private static readonly JsonSerializerOptions SerializerOptions = CreateSerializerOptions();

    public static bool IsTrustedSource(string? source)
    {
        if (!Uri.TryCreate(source, UriKind.Absolute, out var uri))
        {
            return false;
        }

        return string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(uri.Host, TrustedHost, StringComparison.OrdinalIgnoreCase) &&
            uri.IsDefaultPort;
    }

    public static bool TryParseIntent(
        string json,
        out LegacyUiIntent? intent,
        out string? error)
    {
        intent = null;
        error = null;

        try
        {
            using var document = JsonDocument.Parse(json, new JsonDocumentOptions
            {
                AllowTrailingCommas = false,
                CommentHandling = JsonCommentHandling.Disallow,
                MaxDepth = 8
            });
            var root = document.RootElement;
            if (root.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(root, "type", "payload") ||
                !TryString(root, "type", 64, out var type) ||
                !root.TryGetProperty("payload", out var payload) ||
                payload.ValueKind != JsonValueKind.Object)
            {
                error = "올바르지 않은 UI 요청입니다.";
                return false;
            }

            intent = type switch
            {
                "ready" => ParseReady(payload),
                "search-stocks" => ParseSearch(payload),
                "select-stock" => ParseStockSelection(payload),
                "cut-pointer-down" => ParseCutPointer(payload),
                "cut-key-down" => ParseCutKeyDown(payload),
                "clear-cut-selection" => ParseClearCutSelection(payload),
                "drop-selected-cuts" => ParseDrop(payload),
                "set-playlist-enabled" => ParseEnabled(payload),
                "set-all-playlist-enabled" => ParseAllEnabled(payload),
                "select-playlist-row" => ParsePlaylistSelection(payload),
                "activate-playlist-row" => ParsePlaylistActivation(payload),
                "move-selected-playlist-rows" => ParsePlaylistMove(payload),
                "reorder-playlist-rows" => ParsePlaylistReorder(payload),
                "delete-selected-playlist-rows" => ParseDeleteSelected(payload),
                "delete-all-playlist-rows" => ParseDeleteAll(payload),
                "toggle-active-playlist-enabled" => ParseToggleActiveEnabled(payload),
                "select-playlist-boundary" => ParsePlaylistBoundary(payload),
                "select-tab" => ParseTabSelection(payload),
                "swap-tabs" => ParseTabSwap(payload),
                "hover-swap-tabs" => ParseTabHoverSwap(payload),
                "set-moving-averages" => ParseMovingAverages(payload),
                "activate-fixed-action" => ParseFixedAction(payload),
                "activate-fixed-section" => ParseFixedSection(payload),
                "select-industry" => ParseIndustrySelection(payload, false),
                "double-click-industry" => ParseIndustrySelection(payload, true),
                "set-industry-comparison-text" => ParseIndustryComparisonText(payload),
                "swap-industries" => ParseSwapIndustries(payload),
                "activate-industry-action" => ParseIndustryAction(payload),
                "activate-industry-section" => ParseSectionIndex(
                    payload,
                    static index => new LegacyActivateIndustrySectionIntent(index)),
                "select-overseas-fixed-index" => ParseOverseasFixedIndex(payload),
                "search-overseas-industries" => ParseOverseasSearch(payload, false),
                "search-overseas-stocks" => ParseOverseasSearch(payload, true),
                "select-overseas-industry" => ParseOverseasSelection(payload, false),
                "select-overseas-stock" => ParseOverseasSelection(payload, true),
                "activate-overseas-action" => ParseOverseasAction(payload),
                "search-comparison-domestic" => ParseComparisonSearch(payload, false),
                "search-comparison-world" => ParseComparisonSearch(payload, true),
                "select-comparison-domestic" => ParseComparisonSelection(payload, false),
                "select-comparison-world" => ParseComparisonSelection(payload, true),
                "select-comparison-fixed" => ParseComparisonFixedSelection(payload),
                "choose-comparison-domestic" => ParseComparisonChoice(payload, false),
                "choose-comparison-world" => ParseComparisonChoice(payload, true),
                "choose-comparison-fixed" => ParseComparisonFixed(payload),
                "swap-comparison-targets" => ParseComparisonSwap(payload),
                "clear-comparison-targets" => ParseComparisonClear(payload),
                "add-comparison-pair" => ParseComparisonAdd(payload),
                "import-legacy-comparison-pairs" => ParseComparisonImport(payload),
                "select-comparison-pair" => ParseComparisonPairSelection(payload),
                "move-comparison-pair" => ParseComparisonPairMove(payload),
                "delete-selected-comparison-pair" => ParseComparisonDeleteSelected(payload),
                "delete-all-comparison-pairs" => ParseComparisonDeleteAll(payload),
                "activate-comparison-action" => ParseComparisonAction(payload),
                "activate-comparison-section" => ParseSectionIndex(
                    payload,
                    static index => new LegacyActivateComparisonSectionIntent(index)),
                "search-themes" => ParseThemeSearch(payload),
                "select-theme" => ParseThemeSelection(payload),
                "activate-theme-result" => ParseThemeResultActivation(payload),
                "set-theme-sort" => ParseThemeSort(payload),
                "activate-theme-action" => ParseThemeAction(payload),
                "search-experts" => ParseExpertSearch(payload),
                "select-expert" => ParseExpertSelection(payload),
                "activate-expert-action" => ParseExpertAction(payload),
                "search-trading-halts" => ParseTradingHaltSearch(payload),
                "select-trading-halt" => ParseTradingHaltSelection(payload),
                "toggle-trading-halt-sort" => ParseTradingHaltSort(payload),
                "activate-trading-halt-action" => ParseTradingHaltAction(payload),
                "open-operator-catalog" => ParseOperatorCatalogOpen(payload),
                "begin-uc4-theme-catalog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyBeginUc4ThemeCatalogIntent()),
                "edit-uc4-selected-theme" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyEditUc4SelectedThemeIntent()),
                "delete-uc4-selected-theme" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyDeleteUc4SelectedThemeIntent()),
                "begin-uc6-expert-catalog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyBeginUc6ExpertCatalogIntent()),
                "edit-uc6-selected-expert" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyEditUc6SelectedExpertIntent()),
                "delete-uc6-selected-expert" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyDeleteUc6SelectedExpertIntent()),
                "close-operator-catalog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyCloseOperatorCatalogIntent()),
                "search-operator-catalog" => ParseOperatorCatalogSearch(payload),
                "select-operator-catalog-result" => ParseOperatorCatalogResult(payload),
                "begin-create-theme-catalog" => ParseOperatorCatalogThemeCreate(payload),
                "change-create-theme-market" =>
                    ParseOperatorCatalogThemeMarketChange(payload),
                "begin-create-expert-catalog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyBeginCreateExpertCatalogIntent()),
                "search-operator-catalog-stocks" => ParseOperatorCatalogStockSearch(payload),
                "select-operator-catalog-stock" => ParseOperatorCatalogStockSelection(payload),
                "add-operator-catalog-stock" => ParseOperatorCatalogStockAdd(payload),
                "activate-operator-catalog-stock" =>
                    ParseOperatorCatalogStockActivation(payload),
                "move-operator-catalog-draft-row" => ParseOperatorCatalogDraftMove(payload),
                "reorder-operator-catalog-draft-row" =>
                    ParseOperatorCatalogDraftReorder(payload),
                "remove-operator-catalog-draft-row" => ParseOperatorCatalogDraftRemove(payload),
                "select-operator-catalog-draft-row" =>
                    ParseOperatorCatalogDraftSelection(payload),
                "remove-active-operator-catalog-draft-row" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyRemoveActiveOperatorCatalogDraftRowIntent()),
                "clear-operator-catalog-draft-rows" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyClearOperatorCatalogDraftRowsIntent()),
                "set-operator-catalog-draft-buy-amount" =>
                    ParseOperatorCatalogDraftBuyAmount(payload),
                "check-operator-catalog-theme-name" =>
                    ParseOperatorCatalogThemeNameCheck(payload),
                "save-operator-catalog" => ParseOperatorCatalogSave(payload),
                "confirm-native-dialog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyConfirmNativeDialogIntent()),
                "cancel-native-dialog" => ParseOperatorCatalogEmpty(
                    payload,
                    static () => new LegacyCancelNativeDialogIntent()),
                "refresh-named-playlists" => ParseNamedPlaylistRefresh(payload),
                "select-named-playlist" => ParseNamedPlaylistSelection(payload),
                "load-named-playlist-by-id" => ParseNamedPlaylistLoadById(payload),
                "load-selected-named-playlist" => ParseNamedPlaylistEmpty(
                    payload,
                    static () => new LegacyLoadSelectedNamedPlaylistIntent()),
                "create-named-playlist" => ParseNamedPlaylistCreate(payload),
                "save-current-named-playlist" => ParseNamedPlaylistEmpty(
                    payload,
                    static () => new LegacySaveCurrentNamedPlaylistIntent()),
                "save-current-named-playlist-to" => ParseNamedPlaylistSaveTo(payload),
                "delete-selected-named-playlist" =>
                    ParseNamedPlaylistDelete(payload),
                "open-manual-financial" => ParseManualFinancialOpen(payload),
                "close-manual-financial" => ParseManualFinancialEmpty(
                    payload,
                    static () => new LegacyCloseManualFinancialIntent()),
                "search-manual-financial" => ParseManualFinancialSearch(payload),
                "find-manual-financial" => ParseManualFinancialFind(payload),
                "toggle-manual-financial-name-sort" =>
                    ParseManualFinancialNameSort(payload),
                "load-manual-financial" => ParseManualFinancialLoad(payload),
                "activate-manual-financial-result" =>
                    ParseManualFinancialResultActivation(payload),
                "begin-manual-financial-create" => ParseManualFinancialEmpty(
                    payload,
                    static () => new LegacyBeginManualFinancialCreateIntent()),
                "search-manual-financial-stocks" => ParseManualFinancialStockSearch(payload),
                "select-manual-financial-stock" => ParseManualFinancialStockSelection(payload),
                "save-manual-financial" => ParseManualFinancialSave(payload),
                "save-manual-financial-raw" => ParseManualFinancialRawSave(payload),
                "delete-manual-financial" => ParseManualFinancialEmpty(
                    payload,
                    static () => new LegacyDeleteManualFinancialIntent()),
                "delete-all-manual-financial" => ParseManualFinancialEmpty(
                    payload,
                    static () => new LegacyDeleteAllManualFinancialIntent()),
                "activate-manual-financial-action" => ParseManualFinancialAction(payload),
                "open-manual-list" => ParseManualListOpen(payload),
                "close-manual-list" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyCloseManualListIntent()),
                "refresh-manual-list" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyRefreshManualListIntent()),
                "set-manual-net-sell-cell" => ParseManualNetSellCell(payload),
                "save-manual-net-sell" => ParseManualListEmpty(
                    payload,
                    static () => new LegacySaveManualNetSellIntent()),
                "add-manual-net-sell-cut" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyAddManualNetSellCutIntent()),
                "search-manual-vi" => ParseManualViSearch(payload),
                "select-manual-vi-result" => ParseManualViSelection(payload),
                "add-manual-vi-result" => ParseManualViResult(payload),
                "move-manual-vi-item" => ParseManualViMove(payload),
                "delete-manual-vi-item" => ParseManualViDelete(payload),
                "delete-all-manual-vi-items" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyDeleteAllManualViItemsIntent()),
                "save-manual-vi" => ParseManualListEmpty(
                    payload,
                    static () => new LegacySaveManualViIntent()),
                "add-manual-vi-cut" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyAddManualViCutIntent()),
                "confirm-manual-list" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyConfirmManualListIntent()),
                "import-manual-lists" => ParseManualListEmpty(
                    payload,
                    static () => new LegacyImportManualListsIntent()),
                "dismiss-dialog" => ParseDismiss(payload),
                _ => LegacyOperatorSettingsUiIntentParser.Parse(type, payload) ??
                    LegacyPlayoutUiIntentParser.Parse(type, payload)
            };

            if (intent is null)
            {
                error = "지원하지 않는 UI 요청입니다.";
                return false;
            }

            return true;
        }
        catch (JsonException)
        {
            error = "UI 요청 JSON을 읽을 수 없습니다.";
            return false;
        }
    }

    public static string SerializeState(
        LegacyOperatorSnapshot snapshot,
        bool isBusy = false,
        LegacyOperatorPlayoutSnapshot? playout = null,
        LegacyInteractionMetrics? interactionMetrics = null,
        LegacyOperatorSettingsSnapshot? operatorSettings = null)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        var normalizedInteractionMetrics = LegacyInteractionMetrics.Normalize(
            interactionMetrics);
        var payload = new
        {
            snapshot.Revision,
            snapshot.SearchText,
            snapshot.SearchResults,
            snapshot.SelectedStockIndex,
            snapshot.CutRows,
            playlist = snapshot.Playlist.Select(row => new
            {
                row.RowId,
                row.IsEnabled,
                row.IsSelected,
                row.IsActive,
                row.IsSceneResolved,
                row.MarketText,
                row.StockName,
                row.GraphicType,
                row.Subtype,
                row.PageText
            }),
            snapshot.Dialog,
            snapshot.StatusMessage,
            snapshot.StatusKind,
            commandResult = snapshot.CommandResult is null ? null : new
            {
                snapshot.CommandResult.Sequence,
                snapshot.CommandResult.Command,
                snapshot.CommandResult.TargetId,
                snapshot.CommandResult.Succeeded
            },
            tabs = snapshot.Tabs ?? [],
            snapshot.MovingAverages,
            snapshot.FixedCatalog,
            snapshot.FixedSectionBatch,
            industry = snapshot.Industry is null ? null : new
            {
                snapshot.Industry.Market,
                industries = snapshot.Industry.Industries.Select((row, index) => new
                {
                    index,
                    name = row.IndustryName
                }),
                snapshot.Industry.SelectedIndex,
                selectedName = snapshot.Industry.SelectedIndustry?.IndustryName,
                firstName = snapshot.Industry.FirstText,
                secondName = snapshot.Industry.SecondText,
                actions = snapshot.Industry.Actions.Select(action => new
                {
                    actionId = action.ActionId,
                    action.Label,
                    action.Section,
                    action.Category
                })
            },
            overseas = snapshot.Overseas is null ? null : new
            {
                snapshot.Overseas.Revision,
                fixedIndexTargets = snapshot.Overseas.FixedIndexTargets.Select(target => new
                {
                    target.TargetId,
                    target.DisplayName
                }),
                selectedFixedIndexId = snapshot.Overseas.SelectedFixedIndex?.TargetId,
                selectedFixedIndexName = snapshot.Overseas.SelectedFixedIndex?.DisplayName,
                industry = new
                {
                    snapshot.Overseas.Industry.Status,
                    snapshot.Overseas.Industry.Query,
                    snapshot.Overseas.Industry.ResultCount,
                    snapshot.Overseas.Industry.IsTruncated,
                    snapshot.Overseas.Industry.SelectedId,
                    selectedName = snapshot.Overseas.Industry.Selected?.KoreanName,
                    snapshot.Overseas.Industry.Error,
                    results = snapshot.Overseas.Industry.Results.Select(row => new
                    {
                        row.SelectionId,
                        displayName = row.KoreanName
                    })
                },
                stock = new
                {
                    snapshot.Overseas.Stock.Status,
                    snapshot.Overseas.Stock.Query,
                    snapshot.Overseas.Stock.ResultCount,
                    snapshot.Overseas.Stock.IsTruncated,
                    snapshot.Overseas.Stock.IsolatedInvalidRowCount,
                    snapshot.Overseas.Stock.SelectedId,
                    selectedName = snapshot.Overseas.Stock.Selected?.InputName,
                    snapshot.Overseas.Stock.Error,
                    results = snapshot.Overseas.Stock.Results.Select(row => new
                    {
                        row.SelectionId,
                        displayName = row.InputName
                    })
                },
                actions = snapshot.Overseas.Actions.Select(action =>
                {
                    var availability = GetOverseasActionAvailability(snapshot.Overseas, action);
                    return new
                    {
                        action.ActionId,
                        action.Label,
                        action.Source,
                        action.Kind,
                        action.PeriodDays,
                        availability.IsAvailable,
                        availability.Reason
                    };
                })
            },
            comparison = snapshot.Comparison is null ? null : new
            {
                snapshot.Comparison.IsBusy,
                domesticSearch = new
                {
                    snapshot.Comparison.DomesticSearch.Query,
                    snapshot.Comparison.DomesticSearch.Status,
                    results = snapshot.Comparison.DomesticSearch.Results.Select(row => new
                    {
                        row.SelectionId,
                        row.DisplayName,
                        row.Metadata,
                        row.Kind,
                        row.CanSelect,
                        row.UnavailableReason
                    }),
                    snapshot.Comparison.DomesticSearch.SelectedResultId,
                    snapshot.Comparison.DomesticSearch.TotalRowCount,
                    snapshot.Comparison.DomesticSearch.IsTruncated,
                    snapshot.Comparison.DomesticSearch.IsolatedInvalidRowCount,
                    snapshot.Comparison.DomesticSearch.DeferredUnsafeRowCount,
                    snapshot.Comparison.DomesticSearch.ErrorMessage
                },
                worldSearch = new
                {
                    snapshot.Comparison.WorldSearch.Query,
                    snapshot.Comparison.WorldSearch.Status,
                    results = snapshot.Comparison.WorldSearch.Results.Select(row => new
                    {
                        row.SelectionId,
                        row.DisplayName,
                        row.Metadata,
                        row.Kind,
                        row.CanSelect,
                        row.UnavailableReason
                    }),
                    snapshot.Comparison.WorldSearch.SelectedResultId,
                    snapshot.Comparison.WorldSearch.TotalRowCount,
                    snapshot.Comparison.WorldSearch.IsTruncated,
                    snapshot.Comparison.WorldSearch.IsolatedInvalidRowCount,
                    snapshot.Comparison.WorldSearch.DeferredUnsafeRowCount,
                    snapshot.Comparison.WorldSearch.ErrorMessage
                },
                fixedTargets = snapshot.Comparison.FixedTargets.Select(row => new
                {
                    targetId = row.SelectionId,
                    row.DisplayName,
                    row.Metadata,
                    row.Kind
                }),
                snapshot.Comparison.SelectedFixedTargetId,
                firstTarget = snapshot.Comparison.FirstTarget is null ? null : new
                {
                    snapshot.Comparison.FirstTarget.SelectionId,
                    snapshot.Comparison.FirstTarget.DisplayName,
                    snapshot.Comparison.FirstTarget.Metadata,
                    snapshot.Comparison.FirstTarget.Kind
                },
                secondTarget = snapshot.Comparison.SecondTarget is null ? null : new
                {
                    snapshot.Comparison.SecondTarget.SelectionId,
                    snapshot.Comparison.SecondTarget.DisplayName,
                    snapshot.Comparison.SecondTarget.Metadata,
                    snapshot.Comparison.SecondTarget.Kind
                },
                savedPairs = snapshot.Comparison.SavedPairs.Select(row => new
                {
                    row.RowId,
                    row.RowNumber,
                    row.DisplayLabel,
                    row.IsSelected
                }),
                snapshot.Comparison.SelectedPairId,
                snapshot.Comparison.Actions,
                snapshot.Comparison.StatusMessage,
                snapshot.Comparison.StatusKind,
                snapshot.Comparison.CanImportLegacyPairs,
                lastImport = snapshot.Comparison.LastImport is null ? null : new
                {
                    snapshot.Comparison.LastImport.SourceSha256,
                    snapshot.Comparison.LastImport.SourceRowCount,
                    snapshot.Comparison.LastImport.AddedPairCount,
                    snapshot.Comparison.LastImport.SkippedDuplicateCount,
                    snapshot.Comparison.LastImport.TotalPairCount,
                    snapshot.Comparison.LastImport.ImportedAt
                }
            },
            theme = snapshot.Theme is null ? null : new
            {
                snapshot.Theme.Query,
                snapshot.Theme.NxtSession,
                snapshot.Theme.SortId,
                searchResults = snapshot.Theme.SearchResults.Select(row => new
                {
                    row.ResultId,
                    row.DisplayTitle,
                    row.Market,
                    row.Session
                }),
                snapshot.Theme.SearchIsTruncated,
                snapshot.Theme.SelectedResultId,
                snapshot.Theme.SelectedTheme,
                previewItems = snapshot.Theme.PreviewItems.Select(row => new
                {
                    row.InputIndex,
                    row.ItemName
                }),
                snapshot.Theme.PreviewIsTruncated,
                sorts = LegacyThemeWorkflow.Sorts.Select(sort => new
                {
                    sort.Id,
                    sort.Label
                }),
                actions = LegacyThemeWorkflow.Actions.Select(action =>
                {
                    var state = snapshot.Theme.ActionStates.Single(item =>
                        string.Equals(item.ActionId, action.Id, StringComparison.Ordinal));
                    return new
                    {
                        actionId = action.Id,
                        action.Label,
                        state.IsAvailable,
                        state.UnavailableReason
                    };
                })
            },
            expert = snapshot.Expert is null ? null : new
            {
                search = new
                {
                    snapshot.Expert.Search.Query,
                    snapshot.Expert.Search.Status,
                    results = snapshot.Expert.Search.Results.Select(row => new
                    {
                        row.SelectionId,
                        row.ExpertName
                    }),
                    snapshot.Expert.Search.IsTruncated,
                    snapshot.Expert.Search.ErrorMessage
                },
                selectedExpert = snapshot.Expert.SelectedExpert is null ? null : new
                {
                    snapshot.Expert.SelectedExpert.SelectionId,
                    snapshot.Expert.SelectedExpert.ExpertName
                },
                preview = new
                {
                    snapshot.Expert.Preview.Status,
                    items = snapshot.Expert.Preview.Items.Select(row => new
                    {
                        row.PlayIndex,
                        row.StockName,
                        row.BuyAmount
                    }),
                    snapshot.Expert.Preview.IsTruncated,
                    snapshot.Expert.Preview.PageCount,
                    snapshot.Expert.Preview.MaximumPageCount,
                    snapshot.Expert.Preview.ErrorMessage
                },
                snapshot.Expert.Actions,
                snapshot.Expert.StatusMessage,
                snapshot.Expert.StatusKind
            },
            tradingHalt = snapshot.TradingHalt is null ? null : new
            {
                snapshot.TradingHalt.Query,
                snapshot.TradingHalt.Status,
                results = snapshot.TradingHalt.Results.Select(row => new
                {
                    row.SelectionId,
                    row.Market,
                    row.DisplayName,
                    row.IsSelected
                }),
                selected = snapshot.TradingHalt.Selected is null ? null : new
                {
                    snapshot.TradingHalt.Selected.SelectionId,
                    snapshot.TradingHalt.Selected.Market,
                    snapshot.TradingHalt.Selected.DisplayName
                },
                snapshot.TradingHalt.IsTruncated,
                snapshot.TradingHalt.NameSort,
                snapshot.TradingHalt.Ma5,
                snapshot.TradingHalt.Ma20,
                snapshot.TradingHalt.Actions,
                snapshot.TradingHalt.ErrorMessage,
                snapshot.TradingHalt.StatusMessage,
                snapshot.TradingHalt.StatusKind
            },
            operatorCatalog = snapshot.OperatorCatalog is null ? null : new
            {
                snapshot.OperatorCatalog.Revision,
                snapshot.OperatorCatalog.IsOpen,
                snapshot.OperatorCatalog.Entity,
                snapshot.OperatorCatalog.Mode,
                snapshot.OperatorCatalog.Query,
                snapshot.OperatorCatalog.NxtSession,
                snapshot.OperatorCatalog.NewThemeMarket,
                results = snapshot.OperatorCatalog.Results.Select(row => new
                {
                    row.ResultId,
                    row.CodeLabel,
                    row.DisplayName,
                    row.Scope,
                    row.IsSelected
                }),
                snapshot.OperatorCatalog.ResultsAreTruncated,
                snapshot.OperatorCatalog.SelectedResultId,
                snapshot.OperatorCatalog.CodeLabel,
                snapshot.OperatorCatalog.EditorName,
                snapshot.OperatorCatalog.ThemeNameAvailability,
                snapshot.OperatorCatalog.CheckedThemeName,
                draftRows = snapshot.OperatorCatalog.DraftRows.Select(row => new
                {
                    row.RowId,
                    row.DisplayName,
                    row.Market,
                    row.BuyAmount,
                    row.Position
                }),
                snapshot.OperatorCatalog.ActiveDraftRowId,
                snapshot.OperatorCatalog.StockQuery,
                stockResults = snapshot.OperatorCatalog.StockResults.Select(row => new
                {
                    row.ResultId,
                    row.DisplayName,
                    row.Market,
                    row.IsCompatible,
                    row.IsSelected
                }),
                snapshot.OperatorCatalog.StockResultsAreTruncated,
                snapshot.OperatorCatalog.SelectedStockResultId,
                snapshot.OperatorCatalog.SchemaValidated,
                snapshot.OperatorCatalog.WritesQuarantined,
                snapshot.OperatorCatalog.CanBeginCreate,
                snapshot.OperatorCatalog.CanCheckThemeName,
                snapshot.OperatorCatalog.CanSave,
                snapshot.OperatorCatalog.CanDelete,
                snapshot.OperatorCatalog.CanAddStock,
                snapshot.OperatorCatalog.StatusMessage,
                snapshot.OperatorCatalog.StatusKind
            },
            manualFinancial = snapshot.ManualFinancial is null ? null : new
            {
                screen = ManualFinancialScreenId(snapshot.ManualFinancial.Screen),
                snapshot.ManualFinancial.Definition.Label,
                generation = snapshot.ManualFinancial.Revision,
                snapshot.ManualFinancial.Query,
                snapshot.ManualFinancial.FindQuery,
                nameSortDirection = snapshot.ManualFinancial.NameSortDirection ==
                    LegacyManualFinancialNameSortDirection.Ascending
                        ? "ascending"
                        : "descending",
                rows = snapshot.ManualFinancial.Rows.Select(row => new
                {
                    row.ResultId,
                    row.StockName,
                    row.PreviewValues,
                    row.IsDataReadable,
                    row.IsPlayoutReady
                }),
                snapshot.ManualFinancial.ListIsTruncated,
                snapshot.ManualFinancial.ListRejectedItemCount,
                snapshot.ManualFinancial.SelectedRowId,
                selectedRecord = ProjectManualFinancialRecord(
                    snapshot.ManualFinancial.SelectedRecord),
                selectedRawRecord = ProjectManualFinancialRawRecord(
                    snapshot.ManualFinancial.SelectedRawRecord),
                snapshot.ManualFinancial.IsDetailFresh,
                snapshot.ManualFinancial.IsRawDetailFresh,
                snapshot.ManualFinancial.IsPlayoutReady,
                snapshot.ManualFinancial.StockQuery,
                stockCandidates = snapshot.ManualFinancial.StockCandidates.Select(row => new
                {
                    row.ResultId,
                    row.Market,
                    row.Name
                }),
                snapshot.ManualFinancial.StockSearchIsTruncated,
                snapshot.ManualFinancial.SelectedStockResultId,
                verifiedStock = snapshot.ManualFinancial.VerifiedStock is null ? null : new
                {
                    snapshot.ManualFinancial.VerifiedStock.Market,
                    snapshot.ManualFinancial.VerifiedStock.Name
                },
                snapshot.ManualFinancial.Status,
                snapshot.ManualFinancial.WritesQuarantined,
                snapshot.ManualFinancial.LastMessage,
                snapshot.ManualFinancial.CanEdit,
                snapshot.ManualFinancial.CanSave,
                snapshot.ManualFinancial.CanDelete,
                snapshot.ManualFinancial.CanMaterializePlaylist,
                action = new
                {
                    snapshot.ManualFinancial.Definition.ActionId,
                    snapshot.ManualFinancial.Definition.Label
                }
            },
            manualLists = snapshot.ManualLists is null ? null : new
            {
                snapshot.ManualLists.IsOpen,
                screen = snapshot.ManualLists.Screen switch
                {
                    LegacyManualListScreen.NetSell => "net-sell",
                    LegacyManualListScreen.Vi => "vi",
                    _ => null
                },
                audience = snapshot.ManualLists.Audience switch
                {
                    S5025ManualAudience.Individual => "individual",
                    S5025ManualAudience.Foreign => "foreign",
                    S5025ManualAudience.Institution => "institution",
                    _ => throw new ArgumentOutOfRangeException()
                },
                snapshot.ManualLists.IsAvailable,
                snapshot.ManualLists.AvailabilityCode,
                snapshot.ManualLists.AvailabilityMessage,
                snapshot.ManualLists.WritesQuarantined,
                netRows = snapshot.ManualLists.NetRows.Select(row => new
                {
                    row.RowId,
                    row.LeftName,
                    row.LeftAmount,
                    row.RightName,
                    row.RightAmount
                }),
                snapshot.ManualLists.NetRowsAreFresh,
                viItems = snapshot.ManualLists.ViItems.Select(row => new
                {
                    row.ItemId,
                    row.Index,
                    row.Name
                }),
                snapshot.ManualLists.ViPageCount,
                snapshot.ManualLists.ViItemsAreFresh,
                snapshot.ManualLists.SearchQuery,
                searchResults = snapshot.ManualLists.SearchResults.Select(row => new
                {
                    row.ResultId,
                    row.DisplayName,
                    row.MarketText
                }),
                snapshot.ManualLists.SelectedSearchResultId,
                snapshot.ManualLists.SearchIsTruncated,
                snapshot.ManualLists.StatusMessage,
                snapshot.ManualLists.StatusKind,
                snapshot.ManualLists.CanSave,
                snapshot.ManualLists.CanMaterializePlaylist
            },
            namedPlaylist = snapshot.NamedPlaylist is null ? null : new
            {
                snapshot.NamedPlaylist.Revision,
                snapshot.NamedPlaylist.ReadStatus,
                definitions = snapshot.NamedPlaylist.Definitions.Select(definition => new
                {
                    definition.DefinitionId,
                    definition.Title,
                    definition.IsSelected
                }),
                snapshot.NamedPlaylist.IsListTruncated,
                snapshot.NamedPlaylist.SelectedDefinitionId,
                snapshot.NamedPlaylist.SelectedTitle,
                rowCount = snapshot.NamedPlaylist.Rows.Count,
                snapshot.NamedPlaylist.ListFreshness,
                snapshot.NamedPlaylist.DocumentFreshness,
                snapshot.NamedPlaylist.IsWriteInProgress,
                snapshot.NamedPlaylist.IsWriteQuarantined,
                snapshot.NamedPlaylist.CanMutate,
                snapshot.NamedPlaylist.CanCreate,
                snapshot.NamedPlaylist.CanLoad,
                snapshot.NamedPlaylist.CanSave,
                snapshot.NamedPlaylist.CanDelete,
                snapshot.NamedPlaylist.LastMutationOutcome,
                snapshot.NamedPlaylist.StatusMessage,
                snapshot.NamedPlaylist.StatusKind
            },
            playout = playout is null ? null : new
            {
                playout.Mode,
                playout.ConnectionState,
                playout.Phase,
                playout.IsProcessRunning,
                playout.IsConnected,
                playout.IsCommandAvailable,
                playout.LiveTakeInAllowed,
                playout.IsPlayCompletionPending,
                playout.IsTakeOutCompletionPending,
                playout.OutcomeUnknown,
                playout.PreparedCode,
                playout.OnAirCode,
                playout.CurrentCueIndexZeroBased,
                playout.CurrentEntryId,
                playout.OnAirCueIndexZeroBased,
                playout.OnAirEntryId,
                playout.PageIndexZeroBased,
                playout.PageCount,
                playout.PageSize,
                playout.ItemCount,
                playout.CurrentPageItemCount,
                playout.IsLastPage,
                playout.NextKind,
                playout.IsBusy,
                playout.RefreshActive,
                playout.RefreshCompletedCount,
                playout.RefreshMaximumCount,
                playout.RefreshLimitReached,
                playout.RefreshNextAtUtc,
                playout.RefreshLastSuccessAtUtc,
                playout.RefreshFaultCode,
                playout.FadeDuration,
                playout.BackgroundEnabled,
                playout.BackgroundFileName,
                playout.CanChangeBackground,
                playout.Message
            },
            interactionMetrics = new
            {
                cutDragWidthDips = normalizedInteractionMetrics.CutDragWidthDips,
                cutDragHeightDips = normalizedInteractionMetrics.CutDragHeightDips,
                coordinateSpace = "host-dip",
                sourceDpi = normalizedInteractionMetrics.SourceDpi,
                hostRasterizationScale =
                    normalizedInteractionMetrics.HostRasterizationScale,
                doubleClickTimeMilliseconds =
                    normalizedInteractionMetrics.DoubleClickTimeMilliseconds
            },
            operatorSettings = operatorSettings is null ? null : new
            {
                operatorSettings.Revision,
                design = new
                {
                    operatorSettings.Design.DisplayName,
                    operatorSettings.Design.IsCustom,
                    operatorSettings.Design.IsValid,
                    operatorSettings.Design.Status
                },
                resource = new
                {
                    operatorSettings.Resource.DisplayName,
                    operatorSettings.Resource.IsCustom,
                    operatorSettings.Resource.IsValid,
                    operatorSettings.Resource.Status
                },
                background = new
                {
                    operatorSettings.Background.DisplayName,
                    operatorSettings.Background.IsCustom,
                    operatorSettings.Background.IsValid,
                    operatorSettings.Background.Status
                },
                operatorSettings.NavigationExpanded,
                operatorSettings.ColorTheme,
                operatorSettings.ViewMode,
                operatorSettings.StartWorkspace,
                operatorSettings.RestartRequired,
                operatorSettings.Message,
                operatorSettings.MessageKind,
                operatorSettings.AppVersion,
                operatorSettings.OracleState,
                operatorSettings.MariaState,
                operatorSettings.DatabasePollingSeconds
            },
            isBusy
        };

        return JsonSerializer.Serialize(new { type = "state", payload }, SerializerOptions);
    }

    private static LegacyOverseasActionAvailability GetOverseasActionAvailability(
        LegacyOverseasWorkflowSnapshot snapshot,
        LegacyOverseasActionDefinition action) =>
        action.Source switch
        {
            LegacyOverseasActionSource.FixedIndex when snapshot.SelectedFixedIndex is null =>
                new(false, "해외지수를 선택하십시오."),
            LegacyOverseasActionSource.Industry when snapshot.Industry.Selected is null =>
                new(false, "해외업종을 선택하십시오."),
            LegacyOverseasActionSource.Stock when snapshot.Stock.Selected is null =>
                new(false, "해외종목을 선택하십시오."),
            LegacyOverseasActionSource.Stock when snapshot.Stock.Selected!.NationCode.Length != 2 =>
                new(false, "선택한 해외종목의 국가 코드는 두 글자여야 합니다."),
            _ => new(true, null)
        };

    private static LegacyUiIntent? ParseReady(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyReadyIntent() : null;

    private static LegacyUiIntent? ParseSearch(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "text", "trigger") ||
            !TryString(
                payload,
                "text",
                LegacyParityStockSearchService.LegacyTextBoxMaximumLength,
                out var text) ||
            !TryString(payload, "trigger", 16, out var triggerText))
        {
            return null;
        }

        var trigger = triggerText switch
        {
            "button" => LegacySearchTrigger.Button,
            "enter" => LegacySearchTrigger.Enter,
            _ => (LegacySearchTrigger?)null
        };
        return trigger.HasValue ? new LegacySearchStocksIntent(text, trigger.Value) : null;
    }

    private static LegacyUiIntent? ParseStockSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "resultIndex") &&
        TryInt32(payload, "resultIndex", 0, int.MaxValue, out var resultIndex)
            ? new LegacySelectStockIntent(resultIndex)
            : null;

    private static LegacyUiIntent? ParseCutPointer(JsonElement payload)
    {
        if (!HasOnlyProperties(
                payload,
                "physicalIndex",
                "control",
                "shift",
                "x",
                "y",
                "timestampMilliseconds",
                "allowAppend") ||
            !TryInt32(
                payload,
                "physicalIndex",
                0,
                LegacyRuntimeStockCutMenuLoader.MaximumRows - 1,
                out var physicalIndex) ||
            !TryBoolean(payload, "control", out var control) ||
            !TryBoolean(payload, "shift", out var shift) ||
            !TryInt32(payload, "x", 0, 16_384, out var x) ||
            !TryInt32(payload, "y", 0, 16_384, out var y) ||
            !TryInt64(payload, "timestampMilliseconds", 0, long.MaxValue, out var timestamp) ||
            !TryBoolean(payload, "allowAppend", out var allowAppend))
        {
            return null;
        }

        return new LegacyCutPointerDownIntent(
            physicalIndex,
            control,
            shift,
            x,
            y,
            timestamp,
            allowAppend);
    }

    private static LegacyUiIntent? ParseDrop(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDropSelectedCutsIntent() : null;

    private static LegacyUiIntent? ParseCutKeyDown(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "key", "control", "shift") ||
            !TryString(payload, "key", 16, out var keyText) ||
            !TryBoolean(payload, "control", out var control) ||
            !TryBoolean(payload, "shift", out var shift))
        {
            return null;
        }

        var key = keyText switch
        {
            "arrow-up" => LegacyCutKeyboardKey.ArrowUp,
            "arrow-down" => LegacyCutKeyboardKey.ArrowDown,
            "home" => LegacyCutKeyboardKey.Home,
            "end" => LegacyCutKeyboardKey.End,
            "space" => LegacyCutKeyboardKey.Space,
            _ => (LegacyCutKeyboardKey?)null
        };
        return key.HasValue
            ? new LegacyCutKeyDownIntent(key.Value, control, shift)
            : null;
    }

    private static LegacyUiIntent? ParseClearCutSelection(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyClearCutSelectionIntent() : null;

    private static LegacyUiIntent? ParseEnabled(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "rowId", "enabled") ||
            !TryString(payload, "rowId", 128, out var rowId) ||
            !IsSafeIdentifier(rowId) ||
            !TryBoolean(payload, "enabled", out var enabled))
        {
            return null;
        }

        return new LegacySetPlaylistEnabledIntent(rowId, enabled);
    }

    private static LegacyUiIntent? ParseAllEnabled(JsonElement payload) =>
        HasOnlyProperties(payload, "enabled") &&
        TryBoolean(payload, "enabled", out var enabled)
            ? new LegacySetAllPlaylistEnabledIntent(enabled)
            : null;

    private static LegacyUiIntent? ParsePlaylistSelection(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "rowId", "control", "shift") ||
            !TryString(payload, "rowId", 128, out var rowId) ||
            !IsSafeIdentifier(rowId) ||
            !TryBoolean(payload, "control", out var control) ||
            !TryBoolean(payload, "shift", out var shift))
        {
            return null;
        }

        return new LegacySelectPlaylistRowIntent(rowId, control, shift);
    }

    private static LegacyUiIntent? ParsePlaylistActivation(JsonElement payload) =>
        HasOnlyProperties(payload, "rowId") &&
        TryString(payload, "rowId", 128, out var rowId) &&
        IsSafeIdentifier(rowId)
            ? new LegacyActivatePlaylistRowIntent(rowId)
            : null;

    private static LegacyUiIntent? ParsePlaylistMove(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "direction") ||
            !TryString(payload, "direction", 8, out var directionText))
        {
            return null;
        }

        var direction = directionText switch
        {
            "up" => LegacyPlaylistMoveDirection.Up,
            "down" => LegacyPlaylistMoveDirection.Down,
            _ => (LegacyPlaylistMoveDirection?)null
        };
        return direction.HasValue
            ? new LegacyMoveSelectedPlaylistRowsIntent(direction.Value)
            : null;
    }

    private static LegacyUiIntent? ParsePlaylistReorder(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") ||
            !TryString(payload, "sourceRowId", 128, out var sourceRowId) ||
            !TryString(payload, "targetRowId", 128, out var targetRowId) ||
            !IsSafeIdentifier(sourceRowId) ||
            !IsSafeIdentifier(targetRowId) ||
            string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) ||
            !TryString(payload, "position", 8, out var positionText))
        {
            return null;
        }

        var position = ParseDragDropPosition(positionText);
        return position.HasValue
            ? new LegacyReorderPlaylistRowsIntent(sourceRowId, targetRowId, position.Value)
            : null;
    }

    private static LegacyUiIntent? ParseDeleteSelected(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDeleteSelectedPlaylistRowsIntent() : null;

    private static LegacyUiIntent? ParseDeleteAll(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDeleteAllPlaylistRowsIntent() : null;

    private static LegacyUiIntent? ParseToggleActiveEnabled(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyToggleActivePlaylistEnabledIntent() : null;

    private static LegacyUiIntent? ParsePlaylistBoundary(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "boundary") ||
            !TryString(payload, "boundary", 8, out var boundary))
        {
            return null;
        }

        return boundary switch
        {
            "first" => new LegacySelectPlaylistBoundaryIntent(false),
            "last" => new LegacySelectPlaylistBoundaryIntent(true),
            _ => null
        };
    }

    private static LegacyUiIntent? ParseTabSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "tabId") &&
        TryTabId(payload, "tabId", out var tabId)
            ? new LegacySelectTabIntent(tabId)
            : null;

    private static LegacyUiIntent? ParseTabSwap(JsonElement payload) =>
        HasOnlyProperties(payload, "source", "target") &&
        TryTabId(payload, "source", out var source) &&
        TryTabId(payload, "target", out var target)
            ? new LegacySwapTabsIntent(source, target)
            : null;

    private static LegacyUiIntent? ParseTabHoverSwap(JsonElement payload)
    {
        if (!HasOnlyProperties(
                payload,
                "source",
                "target",
                "expectedSourceIndex",
                "expectedTargetIndex") ||
            !TryTabId(payload, "source", out var source) ||
            !TryTabId(payload, "target", out var target) ||
            source == target ||
            !TryInt32(payload, "expectedSourceIndex", 0, 9, out var sourceIndex) ||
            !TryInt32(payload, "expectedTargetIndex", 0, 9, out var targetIndex) ||
            sourceIndex == targetIndex)
        {
            return null;
        }

        return new LegacyHoverSwapTabsIntent(source, target, sourceIndex, targetIndex);
    }

    private static LegacyUiIntent? ParseMovingAverages(JsonElement payload) =>
        HasOnlyProperties(payload, "ma5", "ma20") &&
        TryBoolean(payload, "ma5", out var ma5) &&
        TryBoolean(payload, "ma20", out var ma20)
            ? new LegacySetMovingAveragesIntent(ma5, ma20)
            : null;

    private static LegacyUiIntent? ParseFixedAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        IsSafeIdentifier(actionId)
            ? new LegacyActivateFixedActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseFixedSection(JsonElement payload) =>
        HasOnlyProperties(payload, "sectionIndex") &&
        TryInt32(payload, "sectionIndex", 0, 255, out var sectionIndex)
            ? new LegacyActivateFixedSectionIntent(sectionIndex)
            : null;

    private static LegacyUiIntent? ParseIndustrySelection(
        JsonElement payload,
        bool doubleClick) =>
        HasOnlyProperties(payload, "index") &&
        TryInt32(payload, "index", 0, 499, out var index)
            ? new LegacySelectIndustryIntent(index, doubleClick)
            : null;

    private static LegacyUiIntent? ParseIndustryComparisonText(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "slot", "value") ||
            !TryString(payload, "slot", 16, out var slotText) ||
            !TrySafeString(
                payload,
                "value",
                LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength,
                allowEmpty: true,
                allowUnderscore: true,
                out var value))
        {
            return null;
        }

        var slot = slotText switch
        {
            "first" => LegacyIndustryComparisonSlot.First,
            "second" => LegacyIndustryComparisonSlot.Second,
            _ => (LegacyIndustryComparisonSlot?)null
        };
        return slot.HasValue
            ? new LegacySetIndustryComparisonTextIntent(slot.Value, value)
            : null;
    }

    private static LegacyUiIntent? ParseSwapIndustries(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacySwapIndustriesIntent() : null;

    private static LegacyUiIntent? ParseIndustryAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        IsSafeOpaqueActionId(actionId)
            ? new LegacyActivateIndustryActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseOverseasFixedIndex(JsonElement payload) =>
        HasOnlyProperties(payload, "targetId") &&
        TryString(payload, "targetId", 64, out var targetId) &&
        IsSafeIdentifier(targetId) &&
        LegacyOverseasActionCatalog.FindFixedIndexTarget(targetId) is not null
            ? new LegacySelectOverseasFixedIndexIntent(targetId)
            : null;

    private static LegacyUiIntent? ParseOverseasSearch(
        JsonElement payload,
        bool stocks) =>
        HasOnlyProperties(payload, "query") &&
        TrySafeString(
            payload,
            "query",
            LegacyOverseasSelectionWorkflow.MaximumQueryLength,
            allowEmpty: true,
            allowUnderscore: true,
            out var query)
            ? stocks
                ? new LegacySearchOverseasStocksIntent(query)
                : new LegacySearchOverseasIndustriesIntent(query)
            : null;

    private static LegacyUiIntent? ParseOverseasSelection(
        JsonElement payload,
        bool stocks) =>
        HasOnlyProperties(payload, "selectionId") &&
        TryString(payload, "selectionId", 128, out var selectionId) &&
        IsSafeIdentifier(selectionId)
            ? stocks
                ? new LegacySelectOverseasStockIntent(selectionId)
                : new LegacySelectOverseasIndustryIntent(selectionId)
            : null;

    private static LegacyUiIntent? ParseOverseasAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        IsSafeIdentifier(actionId) &&
        LegacyOverseasActionCatalog.FindAction(actionId) is not null
            ? new LegacyActivateOverseasActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseComparisonSearch(
        JsonElement payload,
        bool world) =>
        HasOnlyProperties(payload, "query") &&
        TryString(payload, "query", 64, out var query)
            ? world
                ? new LegacySearchComparisonWorldIntent(query)
                : new LegacySearchComparisonDomesticIntent(query)
            : null;

    private static LegacyUiIntent? ParseComparisonChoice(
        JsonElement payload,
        bool world) =>
        HasOnlyProperties(payload, "selectionId") &&
        TryString(payload, "selectionId", 128, out var selectionId) &&
        IsSafeIdentifier(selectionId)
            ? world
                ? new LegacyChooseComparisonWorldIntent(selectionId)
                : new LegacyChooseComparisonDomesticIntent(selectionId)
            : null;

    private static LegacyUiIntent? ParseComparisonSelection(
        JsonElement payload,
        bool world) =>
        HasOnlyProperties(payload, "selectionId") &&
        TryString(payload, "selectionId", 128, out var selectionId) &&
        IsSafeIdentifier(selectionId)
            ? world
                ? new LegacySelectComparisonWorldIntent(selectionId)
                : new LegacySelectComparisonDomesticIntent(selectionId)
            : null;

    private static LegacyUiIntent? ParseComparisonFixedSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "targetId") &&
        TryString(payload, "targetId", 64, out var targetId) &&
        IsSafeIdentifier(targetId)
            ? new LegacySelectComparisonFixedIntent(targetId)
            : null;

    private static LegacyUiIntent? ParseComparisonFixed(JsonElement payload) =>
        HasOnlyProperties(payload, "targetId") &&
        TryString(payload, "targetId", 64, out var targetId) &&
        IsSafeIdentifier(targetId)
            ? new LegacyChooseComparisonFixedIntent(targetId)
            : null;

    private static LegacyUiIntent? ParseComparisonSwap(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacySwapComparisonTargetsIntent() : null;

    private static LegacyUiIntent? ParseComparisonClear(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyClearComparisonTargetsIntent() : null;

    private static LegacyUiIntent? ParseComparisonAdd(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyAddComparisonPairIntent() : null;

    private static LegacyUiIntent? ParseComparisonImport(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyImportComparisonPairsIntent() : null;

    private static LegacyUiIntent? ParseComparisonPairSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "rowId") &&
        TryString(payload, "rowId", 128, out var rowId) &&
        IsSafeIdentifier(rowId)
            ? new LegacySelectComparisonPairIntent(rowId)
            : null;

    private static LegacyUiIntent? ParseComparisonPairMove(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "direction") ||
            !TryString(payload, "direction", 8, out var direction))
        {
            return null;
        }

        return direction switch
        {
            "up" => new LegacyMoveComparisonPairIntent(
                LegacyComparisonPairMoveDirection.Up),
            "down" => new LegacyMoveComparisonPairIntent(
                LegacyComparisonPairMoveDirection.Down),
            _ => null
        };
    }

    private static LegacyUiIntent? ParseComparisonDeleteSelected(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDeleteSelectedComparisonPairIntent() : null;

    private static LegacyUiIntent? ParseComparisonDeleteAll(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDeleteAllComparisonPairsIntent() : null;

    private static LegacyUiIntent? ParseComparisonAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        IsSafeIdentifier(actionId)
            ? new LegacyActivateComparisonActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseSectionIndex(
        JsonElement payload,
        Func<int, LegacyUiIntent> factory) =>
        HasOnlyProperties(payload, "sectionIndex") &&
        TryInt32(payload, "sectionIndex", 0, 63, out var sectionIndex)
            ? factory(sectionIndex)
            : null;

    private static LegacyUiIntent? ParseThemeSearch(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "query") ||
            !TryString(
                payload,
                "query",
                LegacyThemeSelectionService.MaximumQueryLength,
                out var query))
        {
            return null;
        }

        return new LegacySearchThemesIntent(query);
    }

    private static LegacyUiIntent? ParseThemeSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 64, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacySelectThemeIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseThemeResultActivation(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 64, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacyActivateThemeResultIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseThemeSort(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "sortId") ||
            !TryString(payload, "sortId", 24, out var sortId))
        {
            return null;
        }

        return sortId is "INPUT_ORDER" or "GAIN_DESC" or "GAIN_ASC"
            ? new LegacySetThemeSortIntent(sortId)
            : null;
    }

    private static LegacyUiIntent? ParseThemeAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        IsSafeIdentifier(actionId)
            ? new LegacyActivateThemeActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseExpertSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "query") &&
        TryString(
            payload,
            "query",
            LegacyExpertSelectionService.MaximumQueryLength,
            out var query)
            ? new LegacySearchExpertsIntent(query)
            : null;

    private static LegacyUiIntent? ParseExpertSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "selectionId") &&
        TryString(payload, "selectionId", 128, out var selectionId) &&
        IsSafeIdentifier(selectionId)
            ? new LegacySelectExpertIntent(selectionId)
            : null;

    private static LegacyUiIntent? ParseExpertAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        string.Equals(actionId, LegacyExpertWorkflowController.ActionId, StringComparison.Ordinal)
            ? new LegacyActivateExpertActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseTradingHaltSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "query") &&
        TryString(
            payload,
            "query",
            LegacyTradingHaltSelectionService.MaximumQueryLength,
            out var query)
            ? new LegacySearchTradingHaltsIntent(query)
            : null;

    private static LegacyUiIntent? ParseTradingHaltSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "selectionId") &&
        TryString(payload, "selectionId", 128, out var selectionId) &&
        IsSafeIdentifier(selectionId)
            ? new LegacySelectTradingHaltIntent(selectionId)
            : null;

    private static LegacyUiIntent? ParseTradingHaltSort(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyToggleTradingHaltSortIntent() : null;

    private static LegacyUiIntent? ParseTradingHaltAction(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "actionId") ||
            !TryString(payload, "actionId", 64, out var actionId))
        {
            return null;
        }

        return actionId is LegacyTradingHaltWorkflowController.CurrentActionId or
            LegacyTradingHaltWorkflowController.CandleActionId
            ? new LegacyActivateTradingHaltActionIntent(actionId)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogOpen(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "entity") ||
            !TryString(payload, "entity", 16, out var entityText))
        {
            return null;
        }

        var entity = entityText switch
        {
            "theme" => LegacyOperatorCatalogEntity.Theme,
            "expert" => LegacyOperatorCatalogEntity.Expert,
            _ => (LegacyOperatorCatalogEntity?)null
        };
        return entity.HasValue ? new LegacyOpenOperatorCatalogIntent(entity.Value) : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogEmpty(
        JsonElement payload,
        Func<LegacyUiIntent> factory) =>
        HasOnlyProperties(payload) ? factory() : null;

    private static LegacyUiIntent? ParseOperatorCatalogSearch(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "query", "nxtSession") ||
            !TrySafeString(
                payload,
                "query",
                64,
                allowEmpty: true,
                allowUnderscore: true,
                out var query) ||
            !TryString(payload, "nxtSession", 16, out var sessionText))
        {
            return null;
        }

        var session = sessionText switch
        {
            "preMarket" => ThemeSession.PreMarket,
            "afterMarket" => ThemeSession.AfterMarket,
            _ => (ThemeSession?)null
        };
        return session.HasValue
            ? new LegacySearchOperatorCatalogIntent(query, session.Value)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogResult(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 128, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacySelectOperatorCatalogResultIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogThemeCreate(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "market") ||
            !TryString(payload, "market", 8, out var marketText))
        {
            return null;
        }

        var market = marketText switch
        {
            "krx" => ThemeMarket.Krx,
            "nxt" => ThemeMarket.Nxt,
            _ => (ThemeMarket?)null
        };
        return market.HasValue
            ? new LegacyBeginCreateThemeCatalogIntent(market.Value)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogThemeMarketChange(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "market", "editorName") ||
            !TryString(payload, "market", 8, out var marketText) ||
            !TrySafeString(
                payload,
                "editorName",
                128,
                allowEmpty: true,
                allowUnderscore: true,
                out var editorName) ||
            !string.Equals(editorName, editorName.Trim(), StringComparison.Ordinal))
        {
            return null;
        }

        var market = marketText switch
        {
            "krx" => ThemeMarket.Krx,
            "nxt" => ThemeMarket.Nxt,
            _ => (ThemeMarket?)null
        };
        return market.HasValue
            ? new LegacyChangeCreateThemeMarketIntent(market.Value, editorName)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogStockSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "query") &&
        TrySafeString(
            payload,
            "query",
            LegacyStockSearchService.MaximumQueryLength,
            allowEmpty: true,
            allowUnderscore: true,
            out var query)
            ? new LegacySearchOperatorCatalogStocksIntent(query)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogStockSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 128, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacySelectOperatorCatalogStockIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogStockAdd(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "buyAmount") ||
            !payload.TryGetProperty("buyAmount", out var amountElement))
        {
            return null;
        }

        if (amountElement.ValueKind == JsonValueKind.Null)
        {
            return new LegacyAddOperatorCatalogStockIntent(null);
        }

        return amountElement.ValueKind == JsonValueKind.Number &&
            amountElement.TryGetDecimal(out var amount) &&
            decimal.Truncate(amount) == amount &&
            amount is > 0 and <= 9_007_199_254_740_991m
                ? new LegacyAddOperatorCatalogStockIntent(amount)
                : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogStockActivation(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "resultId", "buyAmount") ||
            !TryString(payload, "resultId", 128, out var resultId) ||
            !IsSafeIdentifier(resultId) ||
            !payload.TryGetProperty("buyAmount", out var amountElement))
        {
            return null;
        }

        if (amountElement.ValueKind == JsonValueKind.Null)
        {
            return new LegacyActivateOperatorCatalogStockIntent(resultId, null);
        }

        return amountElement.ValueKind == JsonValueKind.Number &&
            amountElement.TryGetDecimal(out var amount) &&
            decimal.Truncate(amount) == amount &&
            amount is > 0 and <= 9_007_199_254_740_991m
                ? new LegacyActivateOperatorCatalogStockIntent(resultId, amount)
                : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogDraftMove(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "rowId", "direction") ||
            !TryString(payload, "rowId", 128, out var rowId) ||
            !IsSafeIdentifier(rowId) ||
            !TryString(payload, "direction", 8, out var directionText))
        {
            return null;
        }

        var direction = directionText switch
        {
            "up" => LegacyOperatorCatalogMoveDirection.Up,
            "down" => LegacyOperatorCatalogMoveDirection.Down,
            _ => (LegacyOperatorCatalogMoveDirection?)null
        };
        return direction.HasValue
            ? new LegacyMoveOperatorCatalogDraftRowIntent(rowId, direction.Value)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogDraftReorder(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") ||
            !TryString(payload, "sourceRowId", 128, out var sourceRowId) ||
            !TryString(payload, "targetRowId", 128, out var targetRowId) ||
            !IsSafeIdentifier(sourceRowId) ||
            !IsSafeIdentifier(targetRowId) ||
            string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) ||
            !TryString(payload, "position", 8, out var positionText))
        {
            return null;
        }

        var position = ParseDragDropPosition(positionText);
        return position.HasValue
            ? new LegacyReorderOperatorCatalogDraftRowIntent(
                sourceRowId,
                targetRowId,
                position.Value)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogDraftRemove(JsonElement payload) =>
        HasOnlyProperties(payload, "rowId") &&
        TryString(payload, "rowId", 128, out var rowId) &&
        IsSafeIdentifier(rowId)
            ? new LegacyRemoveOperatorCatalogDraftRowIntent(rowId)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogDraftSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "rowId") &&
        TryString(payload, "rowId", 128, out var rowId) &&
        IsSafeIdentifier(rowId)
            ? new LegacySelectOperatorCatalogDraftRowIntent(rowId)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogDraftBuyAmount(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "rowId", "buyAmount") ||
            !TryString(payload, "rowId", 128, out var rowId) ||
            !IsSafeIdentifier(rowId) ||
            !payload.TryGetProperty("buyAmount", out var amountElement))
        {
            return null;
        }

        if (amountElement.ValueKind == JsonValueKind.Null)
        {
            return new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, null);
        }

        return amountElement.ValueKind == JsonValueKind.Number &&
               amountElement.TryGetDecimal(out var amount) &&
               decimal.Truncate(amount) == amount &&
               amount is > 0 and <= 9_007_199_254_740_991m
            ? new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, amount)
            : null;
    }

    private static LegacyUiIntent? ParseOperatorCatalogSave(JsonElement payload) =>
        HasOnlyProperties(payload, "name") &&
        TrySafeString(
            payload,
            "name",
            128,
            allowEmpty: true,
            allowUnderscore: true,
            out var name) &&
        string.Equals(name, name.Trim(), StringComparison.Ordinal)
            ? new LegacySaveOperatorCatalogIntent(name)
            : null;

    private static LegacyUiIntent? ParseOperatorCatalogThemeNameCheck(JsonElement payload) =>
        HasOnlyProperties(payload, "name") &&
        TrySafeString(
            payload,
            "name",
            128,
            allowEmpty: false,
            allowUnderscore: true,
            out var name) &&
        string.Equals(name, name.Trim(), StringComparison.Ordinal)
            ? new LegacyCheckOperatorCatalogThemeNameIntent(name)
            : null;

    private static LegacyUiIntent? ParseNamedPlaylistEmpty(
        JsonElement payload,
        Func<LegacyUiIntent> factory) =>
        HasOnlyProperties(payload) ? factory() : null;

    private static LegacyUiIntent? ParseNamedPlaylistSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "definitionId", "requestId") &&
        TryString(payload, "definitionId", 128, out var definitionId) &&
        IsSafeIdentifier(definitionId) &&
        TryString(payload, "requestId", 128, out var requestId) &&
        IsSafeIdentifier(requestId)
            ? new LegacySelectNamedPlaylistIntent(definitionId, requestId)
            : null;

    private static LegacyUiIntent? ParseNamedPlaylistRefresh(JsonElement payload) =>
        HasOnlyProperties(payload, "requestId") &&
        TryString(payload, "requestId", 128, out var requestId) &&
        IsSafeIdentifier(requestId)
            ? new LegacyRefreshNamedPlaylistsIntent(requestId)
            : null;

    private static LegacyUiIntent? ParseNamedPlaylistLoadById(JsonElement payload) =>
        HasOnlyProperties(payload, "definitionId") &&
        TryString(payload, "definitionId", 128, out var definitionId) &&
        IsSafeIdentifier(definitionId)
            ? new LegacyLoadNamedPlaylistByIdIntent(definitionId)
            : null;

    private static LegacyUiIntent? ParseNamedPlaylistSaveTo(JsonElement payload)
    {
        var hasSavedAlert = payload.TryGetProperty("showSavedAlert", out var alertProperty);
        if (!(hasSavedAlert
                ? HasOnlyProperties(payload, "definitionId", "showSavedAlert")
                : HasOnlyProperties(payload, "definitionId")) ||
            !TryString(payload, "definitionId", 128, out var definitionId) ||
            !IsSafeIdentifier(definitionId))
        {
            return null;
        }

        var showSavedAlert = false;
        if (hasSavedAlert)
        {
            if (alertProperty.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
            {
                return null;
            }

            showSavedAlert = alertProperty.GetBoolean();
        }

        return new LegacySaveCurrentNamedPlaylistToIntent(definitionId, showSavedAlert);
    }

    private static LegacyUiIntent? ParseNamedPlaylistCreate(JsonElement payload) =>
        HasOnlyProperties(payload, "title", "requestId") &&
        TrySafeString(
            payload,
            "title",
            128,
            allowEmpty: false,
            allowUnderscore: true,
            out var title) &&
        TryString(payload, "requestId", 128, out var requestId) &&
        IsSafeIdentifier(requestId) &&
        string.Equals(title, title.Trim(), StringComparison.Ordinal)
            ? new LegacyCreateNamedPlaylistIntent(title, requestId)
            : null;

    private static LegacyUiIntent? ParseNamedPlaylistDelete(JsonElement payload) =>
        HasOnlyProperties(payload, "requestId") &&
        TryString(payload, "requestId", 128, out var requestId) &&
        IsSafeIdentifier(requestId)
            ? new LegacyDeleteSelectedNamedPlaylistIntent(requestId)
            : null;

    private static LegacyUiIntent? ParseManualFinancialOpen(JsonElement payload) =>
        HasOnlyProperties(payload, "screen") &&
        TryString(payload, "screen", 32, out var screenText) &&
        TryManualFinancialScreen(screenText, out var screen)
            ? new LegacyOpenManualFinancialIntent(screen)
            : null;

    private static LegacyUiIntent? ParseManualFinancialEmpty(
        JsonElement payload,
        Func<LegacyUiIntent> factory) =>
        HasOnlyProperties(payload) ? factory() : null;

    private static LegacyUiIntent? ParseManualFinancialSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "query") &&
        TrySafeString(payload, "query", 64, allowEmpty: true, allowUnderscore: true, out var query)
            ? new LegacySearchManualFinancialIntent(query)
            : null;

    private static LegacyUiIntent? ParseManualFinancialFind(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "query", "direction", "generation") ||
            !TrySafeString(
                payload,
                "query",
                64,
                allowEmpty: false,
                allowUnderscore: true,
                out var query) ||
            !TryString(payload, "direction", 8, out var directionText) ||
            !TryInt64(payload, "generation", 0, long.MaxValue, out var generation))
        {
            return null;
        }

        var direction = directionText switch
        {
            "up" => LegacyManualFinancialFindDirection.Up,
            "down" => LegacyManualFinancialFindDirection.Down,
            _ => (LegacyManualFinancialFindDirection?)null
        };
        return direction.HasValue
            ? new LegacyFindManualFinancialIntent(query, direction.Value, generation)
            : null;
    }

    private static LegacyUiIntent? ParseManualFinancialNameSort(JsonElement payload) =>
        HasOnlyProperties(payload, "generation") &&
        TryInt64(payload, "generation", 0, long.MaxValue, out var generation)
            ? new LegacyToggleManualFinancialNameSortIntent(generation)
            : null;

    private static LegacyUiIntent? ParseManualFinancialLoad(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 128, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacyLoadManualFinancialIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseManualFinancialResultActivation(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 128, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacyActivateManualFinancialResultIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseManualFinancialStockSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "stockName") &&
        TrySafeString(
            payload,
            "stockName",
            64,
            allowEmpty: true,
            allowUnderscore: true,
            out var stockName)
            ? new LegacySearchManualFinancialStocksIntent(stockName)
            : null;

    private static LegacyUiIntent? ParseManualFinancialStockSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 128, out var resultId) &&
        (resultId.Length == 0 || IsSafeIdentifier(resultId))
            ? new LegacySelectManualFinancialStockIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseManualFinancialSave(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "record") ||
            !payload.TryGetProperty("record", out var recordElement) ||
            recordElement.ValueKind != JsonValueKind.Object)
        {
            return null;
        }

        var record = ParseManualFinancialRecord(recordElement);
        return record is null ? null : new LegacySaveManualFinancialIntent(record);
    }

    private static LegacyUiIntent? ParseManualFinancialRawSave(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "record") ||
            !payload.TryGetProperty("record", out var recordElement) ||
            recordElement.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(
                recordElement,
                "screen",
                "stockName",
                "storageValues") ||
            !TryString(recordElement, "screen", 32, out var screenText) ||
            !TryManualFinancialScreen(screenText, out var screen) ||
            !TrySafeString(
                recordElement,
                "stockName",
                200,
                allowEmpty: false,
                allowUnderscore: true,
                out var stockName) ||
            stockName != stockName.Trim() || stockName.Contains('^') ||
            !recordElement.TryGetProperty("storageValues", out var storageElement) ||
            storageElement.ValueKind != JsonValueKind.Array)
        {
            return null;
        }

        var expectedCount = ManualFinancialCutContracts.Get(screen).StorageColumns.Count - 1;
        if (storageElement.GetArrayLength() != expectedCount)
        {
            return null;
        }

        var values = new string[expectedCount];
        var index = 0;
        foreach (var item in storageElement.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.String)
            {
                return null;
            }

            var value = item.GetString() ?? string.Empty;
            if (value.Length > 200 ||
                value.Any(static character =>
                    char.IsSurrogate(character) || character == '\uFFFD'))
            {
                return null;
            }

            values[index++] = value;
        }

        return new LegacySaveManualFinancialRawIntent(
            new ManualFinancialRawRecord(
                new ManualFinancialIdentity(screen, stockName),
                Array.AsReadOnly(values)));
    }

    private static LegacyUiIntent? ParseManualFinancialAction(JsonElement payload) =>
        HasOnlyProperties(payload, "actionId") &&
        TryString(payload, "actionId", 64, out var actionId) &&
        LegacyManualFinancialWorkflow.Screens.Any(definition =>
            string.Equals(definition.ActionId, actionId, StringComparison.Ordinal))
            ? new LegacyActivateManualFinancialActionIntent(actionId)
            : null;

    private static LegacyUiIntent? ParseManualListOpen(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "screen") ||
            !TryString(payload, "screen", 24, out var screen))
        {
            return null;
        }

        return screen switch
        {
            "individual" => new LegacyOpenManualListIntent(
                LegacyManualListScreen.NetSell,
                S5025ManualAudience.Individual),
            "foreign" => new LegacyOpenManualListIntent(
                LegacyManualListScreen.NetSell,
                S5025ManualAudience.Foreign),
            "institution" => new LegacyOpenManualListIntent(
                LegacyManualListScreen.NetSell,
                S5025ManualAudience.Institution),
            "vi" => new LegacyOpenManualListIntent(
                LegacyManualListScreen.Vi,
                S5025ManualAudience.Individual),
            _ => null
        };
    }

    private static LegacyUiIntent? ParseManualListEmpty(
        JsonElement payload,
        Func<LegacyUiIntent> factory) =>
        HasOnlyProperties(payload) ? factory() : null;

    private static LegacyUiIntent? ParseManualNetSellCell(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "rowId", "field", "value") ||
            !TryString(payload, "rowId", 64, out var rowId) ||
            !IsSafeIdentifier(rowId) ||
            !TryString(payload, "field", 24, out var fieldText) ||
            !TrySafeString(
                payload,
                "value",
                LegacyManualListsWorkflow.MaximumNetSellValueLength,
                allowEmpty: true,
                allowUnderscore: true,
                out var value) ||
            value.Contains('^'))
        {
            return null;
        }

        var field = fieldText switch
        {
            "leftName" => LegacyManualNetSellField.LeftName,
            "leftAmount" => LegacyManualNetSellField.LeftAmount,
            "rightName" => LegacyManualNetSellField.RightName,
            "rightAmount" => LegacyManualNetSellField.RightAmount,
            _ => (LegacyManualNetSellField?)null
        };
        return field.HasValue
            ? new LegacySetManualNetSellCellIntent(rowId, field.Value, value)
            : null;
    }

    private static LegacyUiIntent? ParseManualViSearch(JsonElement payload) =>
        HasOnlyProperties(payload, "query") &&
        TrySafeString(
            payload,
            "query",
            LegacyManualListsWorkflow.MaximumSearchLength,
            allowEmpty: true,
            allowUnderscore: true,
            out var query)
            ? new LegacySearchManualViIntent(query)
            : null;

    private static LegacyUiIntent? ParseManualViResult(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 64, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacyAddManualViResultIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseManualViSelection(JsonElement payload) =>
        HasOnlyProperties(payload, "resultId") &&
        TryString(payload, "resultId", 64, out var resultId) &&
        IsSafeIdentifier(resultId)
            ? new LegacySelectManualViResultIntent(resultId)
            : null;

    private static LegacyUiIntent? ParseManualViMove(JsonElement payload)
    {
        if (!HasOnlyProperties(payload, "itemId", "direction") ||
            !TryString(payload, "itemId", 64, out var itemId) ||
            !IsSafeIdentifier(itemId) ||
            !TryString(payload, "direction", 8, out var directionText))
        {
            return null;
        }

        var direction = directionText switch
        {
            "up" => LegacyManualListMoveDirection.Up,
            "down" => LegacyManualListMoveDirection.Down,
            _ => (LegacyManualListMoveDirection?)null
        };
        return direction.HasValue
            ? new LegacyMoveManualViItemIntent(itemId, direction.Value)
            : null;
    }

    private static LegacyUiIntent? ParseManualViDelete(JsonElement payload) =>
        HasOnlyProperties(payload, "itemId") &&
        TryString(payload, "itemId", 64, out var itemId) &&
        IsSafeIdentifier(itemId)
            ? new LegacyDeleteManualViItemIntent(itemId)
            : null;

    private static ManualFinancialRecord? ParseManualFinancialRecord(JsonElement element)
    {
        if (!element.TryGetProperty("screen", out var screenElement) ||
            screenElement.ValueKind != JsonValueKind.String ||
            !TryManualFinancialScreen(screenElement.GetString(), out var screen) ||
            !TrySafeString(
                element,
                "stockName",
                200,
                allowEmpty: false,
                allowUnderscore: true,
                out var stockName))
        {
            return null;
        }

        var identity = new ManualFinancialIdentity(screen, stockName);
        return screen switch
        {
            ManualFinancialScreenKind.RevenueComposition =>
                ParseManualRevenue(element, identity),
            ManualFinancialScreenKind.GrowthMetrics =>
                ParseManualGrowth(element, identity),
            ManualFinancialScreenKind.Sales =>
                ParseManualQuarters(element, identity, operatingProfit: false),
            ManualFinancialScreenKind.OperatingProfit =>
                ParseManualQuarters(element, identity, operatingProfit: true),
            _ => null
        };
    }

    private static ManualRevenueCompositionRecord? ParseManualRevenue(
        JsonElement element,
        ManualFinancialIdentity identity)
    {
        if (!HasOnlyProperties(element, "screen", "stockName", "baseDate", "slices") ||
            !TrySafeString(
                element,
                "baseDate",
                200,
                allowEmpty: true,
                allowUnderscore: true,
                out var baseDate) ||
            !element.TryGetProperty("slices", out var slicesElement) ||
            slicesElement.ValueKind != JsonValueKind.Array ||
            slicesElement.GetArrayLength() != 5)
        {
            return null;
        }

        var slices = new ManualRevenueSlice?[5];
        var index = 0;
        foreach (var item in slicesElement.EnumerateArray())
        {
            if (item.ValueKind == JsonValueKind.Null)
            {
                slices[index++] = null;
                continue;
            }

            if (item.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(item, "label", "percentageText") ||
                !TrySafeString(
                    item,
                    "label",
                    200,
                    allowEmpty: false,
                    allowUnderscore: false,
                    out var label) ||
                !TrySafeString(
                    item,
                    "percentageText",
                    64,
                    allowEmpty: false,
                    allowUnderscore: false,
                    out var percentageText) ||
                !TryManualDoubleText(percentageText, out var percentage))
            {
                return null;
            }

            slices[index++] = new ManualRevenueSlice(label, percentageText, percentage);
        }

        return new ManualRevenueCompositionRecord(identity, baseDate, slices);
    }

    private static ManualGrowthMetricsRecord? ParseManualGrowth(
        JsonElement element,
        ManualFinancialIdentity identity)
    {
        if (!HasOnlyProperties(
                element,
                "screen",
                "stockName",
                "salesGrowth",
                "operatingProfitGrowth",
                "netAssetGrowth",
                "netIncomeGrowth",
                "periods") ||
            !TryGrowthSeries(element, "salesGrowth", out var salesGrowth) ||
            !TryGrowthSeries(
                element,
                "operatingProfitGrowth",
                out var operatingProfitGrowth) ||
            !TryGrowthSeries(element, "netAssetGrowth", out var netAssetGrowth) ||
            !TryGrowthSeries(element, "netIncomeGrowth", out var netIncomeGrowth) ||
            !TryStringArray(
                element,
                "periods",
                4,
                allowEmpty: true,
                allowUnderscore: false,
                out var periods))
        {
            return null;
        }

        return new ManualGrowthMetricsRecord(
            identity,
            salesGrowth,
            operatingProfitGrowth,
            netAssetGrowth,
            netIncomeGrowth,
            new ManualGrowthPeriodLabels(periods[0], periods[1], periods[2], periods[3]));
    }

    private static ManualFinancialRecord? ParseManualQuarters(
        JsonElement element,
        ManualFinancialIdentity identity,
        bool operatingProfit)
    {
        if (!HasOnlyProperties(element, "screen", "stockName", "quarters") ||
            !element.TryGetProperty("quarters", out var quartersElement) ||
            quartersElement.ValueKind != JsonValueKind.Array ||
            quartersElement.GetArrayLength() != 6)
        {
            return null;
        }

        var quarters = new ManualQuarterValue[6];
        var index = 0;
        foreach (var item in quartersElement.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(item, "quarter", "value") ||
                !TrySafeString(
                    item,
                    "quarter",
                    200,
                    allowEmpty: false,
                    allowUnderscore: false,
                    out var quarter) ||
                !item.TryGetProperty("value", out var valueElement) ||
                valueElement.ValueKind != JsonValueKind.Number ||
                !valueElement.TryGetInt32(out var value))
            {
                return null;
            }

            quarters[index++] = new ManualQuarterValue(quarter, value);
        }

        return operatingProfit
            ? new ManualOperatingProfitRecord(identity, quarters)
            : new ManualSalesRecord(identity, quarters);
    }

    private static bool TryGrowthSeries(
        JsonElement element,
        string propertyName,
        out ManualGrowthSeriesValues values)
    {
        values = new ManualGrowthSeriesValues(null, null, null, null);
        if (!element.TryGetProperty(propertyName, out var array) ||
            array.ValueKind != JsonValueKind.Array || array.GetArrayLength() != 4)
        {
            return false;
        }

        var result = new double?[4];
        var index = 0;
        foreach (var item in array.EnumerateArray())
        {
            if (item.ValueKind == JsonValueKind.Null)
            {
                result[index++] = null;
                continue;
            }

            if (item.ValueKind != JsonValueKind.Number ||
                !item.TryGetDouble(out var number) || !double.IsFinite(number) ||
                Math.Abs(number) > 1_000_000_000_000d)
            {
                return false;
            }

            result[index++] = number;
        }

        values = new ManualGrowthSeriesValues(result[0], result[1], result[2], result[3]);
        return true;
    }

    private static bool TryStringArray(
        JsonElement element,
        string propertyName,
        int expectedCount,
        bool allowEmpty,
        bool allowUnderscore,
        out string[] values)
    {
        values = [];
        if (!element.TryGetProperty(propertyName, out var array) ||
            array.ValueKind != JsonValueKind.Array ||
            array.GetArrayLength() != expectedCount)
        {
            return false;
        }

        var result = new string[expectedCount];
        var index = 0;
        foreach (var item in array.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.String ||
                !IsSafeBridgeText(
                    item.GetString() ?? string.Empty,
                    200,
                    allowEmpty,
                    allowUnderscore))
            {
                return false;
            }

            result[index++] = item.GetString()!;
        }

        values = result;
        return true;
    }

    private static bool TryManualDoubleText(string value, out double number)
    {
        if (string.Equals(value, "-", StringComparison.Ordinal))
        {
            number = 0;
            return true;
        }

        return double.TryParse(
                value,
                NumberStyles.Float,
                CultureInfo.InvariantCulture,
                out number) &&
            double.IsFinite(number) && Math.Abs(number) <= 1_000_000_000_000d;
    }

    private static bool TryManualFinancialScreen(
        string? value,
        out ManualFinancialScreenKind screen)
    {
        var parsed = value switch
        {
            "revenue-composition" => ManualFinancialScreenKind.RevenueComposition,
            "growth-metrics" => ManualFinancialScreenKind.GrowthMetrics,
            "sales" => ManualFinancialScreenKind.Sales,
            "operating-profit" => ManualFinancialScreenKind.OperatingProfit,
            _ => (ManualFinancialScreenKind?)null
        };
        screen = parsed.GetValueOrDefault();
        return parsed.HasValue;
    }

    private static string ManualFinancialScreenId(ManualFinancialScreenKind screen) => screen switch
    {
        ManualFinancialScreenKind.RevenueComposition => "revenue-composition",
        ManualFinancialScreenKind.GrowthMetrics => "growth-metrics",
        ManualFinancialScreenKind.Sales => "sales",
        ManualFinancialScreenKind.OperatingProfit => "operating-profit",
        _ => throw new ArgumentOutOfRangeException(nameof(screen))
    };

    private static object? ProjectManualFinancialRecord(ManualFinancialRecord? record) =>
        record switch
        {
            ManualRevenueCompositionRecord revenue => new
            {
                screen = ManualFinancialScreenId(revenue.Identity.Screen),
                stockName = revenue.Identity.StockName,
                revenue.BaseDate,
                slices = revenue.Slices.Select(slice => slice is null ? null : new
                {
                    slice.Label,
                    slice.PercentageText
                })
            },
            ManualGrowthMetricsRecord growth => new
            {
                screen = ManualFinancialScreenId(growth.Identity.Screen),
                stockName = growth.Identity.StockName,
                salesGrowth = growth.SalesGrowth.ToArray(),
                operatingProfitGrowth = growth.OperatingProfitGrowth.ToArray(),
                netAssetGrowth = growth.NetAssetGrowth.ToArray(),
                netIncomeGrowth = growth.NetIncomeGrowth.ToArray(),
                periods = growth.Periods.ToArray()
            },
            ManualSalesRecord sales => new
            {
                screen = ManualFinancialScreenId(sales.Identity.Screen),
                stockName = sales.Identity.StockName,
                sales.Quarters
            },
            ManualOperatingProfitRecord profit => new
            {
                screen = ManualFinancialScreenId(profit.Identity.Screen),
                stockName = profit.Identity.StockName,
                profit.Quarters
            },
            null => null,
            _ => throw new ArgumentOutOfRangeException(nameof(record))
        };

    private static object? ProjectManualFinancialRawRecord(
        ManualFinancialRawRecord? record) => record is null
            ? null
            : new
            {
                screen = ManualFinancialScreenId(record.Identity.Screen),
                stockName = record.Identity.StockName,
                storageValues = record.StorageValues
            };

    private static bool TrySafeString(
        JsonElement element,
        string propertyName,
        int maximumLength,
        bool allowEmpty,
        bool allowUnderscore,
        out string value)
    {
        value = string.Empty;
        return TryString(element, propertyName, maximumLength, out value) &&
            IsSafeBridgeText(value, maximumLength, allowEmpty, allowUnderscore);
    }

    private static bool IsSafeBridgeText(
        string value,
        int maximumLength,
        bool allowEmpty,
        bool allowUnderscore) =>
        value.Length <= maximumLength &&
        (allowEmpty || value.Length > 0) &&
        (allowUnderscore || !value.Contains('_')) &&
        value.All(static character =>
            !char.IsControl(character) && !char.IsSurrogate(character) &&
            character != '\uFFFD');

    private static LegacyUiIntent? ParseDismiss(JsonElement payload) =>
        HasOnlyProperties(payload) ? new LegacyDismissDialogIntent() : null;

    private static bool HasOnlyProperties(JsonElement element, params string[] names)
    {
        var expected = new HashSet<string>(names, StringComparer.Ordinal);
        var count = 0;
        foreach (var property in element.EnumerateObject())
        {
            count++;
            if (!expected.Contains(property.Name))
            {
                return false;
            }
        }

        return count == expected.Count;
    }

    private static bool TryString(
        JsonElement element,
        string propertyName,
        int maximumLength,
        out string value)
    {
        value = string.Empty;
        return element.TryGetProperty(propertyName, out var property) &&
            property.ValueKind == JsonValueKind.String &&
            (value = property.GetString() ?? string.Empty).Length <= maximumLength;
    }

    private static bool TryBoolean(JsonElement element, string name, out bool value)
    {
        value = false;
        if (!element.TryGetProperty(name, out var property) ||
            (property.ValueKind != JsonValueKind.True && property.ValueKind != JsonValueKind.False))
        {
            return false;
        }

        value = property.GetBoolean();
        return true;
    }

    private static bool TryTabId(
        JsonElement element,
        string name,
        out LegacyOperatorTabId tabId)
    {
        tabId = default;
        if (!TryString(element, name, 32, out var value))
        {
            return false;
        }

        var parsed = value switch
        {
            "overseas" => LegacyOperatorTabId.Overseas,
            "exchange" => LegacyOperatorTabId.Exchange,
            "index" => LegacyOperatorTabId.Index,
            "kospiIndustry" => LegacyOperatorTabId.KospiIndustry,
            "kosdaqIndustry" => LegacyOperatorTabId.KosdaqIndustry,
            "comparison" => LegacyOperatorTabId.Comparison,
            "theme" => LegacyOperatorTabId.Theme,
            "overseasStocks" => LegacyOperatorTabId.OverseasStocks,
            "expert" => LegacyOperatorTabId.Expert,
            "tradingHalt" => LegacyOperatorTabId.TradingHalt,
            _ => (LegacyOperatorTabId?)null
        };
        if (!parsed.HasValue)
        {
            return false;
        }

        tabId = parsed.Value;
        return true;
    }

    private static bool TryInt32(
        JsonElement element,
        string name,
        int minimum,
        int maximum,
        out int value)
    {
        value = 0;
        return element.TryGetProperty(name, out var property) &&
            property.ValueKind == JsonValueKind.Number &&
            property.TryGetInt32(out value) &&
            value >= minimum && value <= maximum;
    }

    private static LegacyDragDropPosition? ParseDragDropPosition(string value) => value switch
    {
        "before" => LegacyDragDropPosition.Before,
        "after" => LegacyDragDropPosition.After,
        _ => null
    };

    private static bool TryInt64(
        JsonElement element,
        string name,
        long minimum,
        long maximum,
        out long value)
    {
        value = 0;
        return element.TryGetProperty(name, out var property) &&
            property.ValueKind == JsonValueKind.Number &&
            property.TryGetInt64(out value) &&
            value >= minimum && value <= maximum;
    }

    private static bool IsSafeIdentifier(string value)
    {
        if (value.Length == 0)
        {
            return false;
        }

        return value.All(character =>
            char.IsAsciiLetterOrDigit(character) || character is '-' or '_');
    }

    private static bool IsSafeOpaqueActionId(string value) =>
        value.Length is > 0 and <= 64 && value.All(character =>
            char.IsLetterOrDigit(character) || character is '-' or '_');

    private static JsonSerializerOptions CreateSerializerOptions()
    {
        var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
        options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
        return options;
    }
}
