using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW;

/// <summary>
/// Application composition root for native DB-to-scene routes. Routes are added only
/// when their explicit loader and mutation builder have been ported and tested.
/// </summary>
internal static class LegacySceneRuntimeFactory
{
    public static LegacyPlayoutWorkflow Create(
        IPlayoutEngine engine,
        IDataQueryExecutor executor,
        LegacySceneCueCompositionOptions? compositionOptions = null,
        ILegacySceneCueCompositionOptionsSource? compositionOptionsSource = null,
        IS5025TrustedManualDataSource? trustedManualSource = null,
        ITrustedViStockNameSnapshotSource? trustedViSource = null)
    {
        ArgumentNullException.ThrowIfNull(engine);
        ArgumentNullException.ThrowIfNull(executor);

        var revenueLoader = new S5076SceneDataLoader(executor);
        var profitLoader = new S5081SceneDataLoader(executor);
        var tradingLoader = new S5082SceneDataLoader(executor);
        var nxtDetailLoader = new S5006NxtSceneDataLoader(executor);
        var domesticDetailLoader = new S5006DomesticSceneDataLoader(executor);
        var equityDetailLoader = new S5011SceneDataLoader(executor);
        var threePanelLoader = new S5016SceneDataLoader(executor);
        var twoPanelLoader = new S50160SceneDataLoader(executor);
        var imageQuoteLoader = new S6001SceneDataLoader(executor);
        var worldMapLoader = new S8067SceneDataLoader(executor);
        var candleLoader = new S8010SceneDataLoader(executor);
        var parameterizedRequestResolver = new LegacyParameterizedSceneRequestResolver(executor);
        var oneColumnLoader = new S5001SceneDataLoader(executor);
        var futuresPairLoader = new S5032SceneDataLoader(executor);
        var twoColumnLoader = new S8018SceneDataLoader(executor);
        var sectorSquareLoader = new S8001SceneDataLoader(executor);
        var comparisonRequestResolver = new ComparisonAndYieldLegacyRequestResolver(executor);
        var candlePairLoader = new S5026SceneDataLoader(executor);
        var comparisonYieldLoader = new S5029SceneDataLoader(executor);
        var singleYieldLoader = new S5086SceneDataLoader(executor);
        var quotedYieldLoader = new S50860SceneDataLoader(executor);
        var linePairLoader = new S5087SceneDataLoader(executor);
        var chartRequestResolver = new ChartLegacySceneRequestResolver();
        var sectorIndexLoader = new S5078SceneDataLoader(executor);
        var growthMetricsLoader = new S5079SceneDataLoader(executor);
        var salesLoader = new S5080SceneDataLoader(executor);
        var participantTrendLoader = new S5083SceneDataLoader(executor);
        var indexTrendLoader = new S5084SceneDataLoader(executor);
        if (trustedManualSource is null)
        {
            var manualDataDirectory = Environment.GetEnvironmentVariable(
                S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable);
            trustedManualSource = string.IsNullOrWhiteSpace(manualDataDirectory)
                ? null
                : new S5025TrustedManualFileDataSource(manualDataDirectory);
        }
        var gridMarketRequestResolver = new LegacyGridMarketSceneRequestResolver(
            executor,
            trustedManualSource);
        var dailyInvestorLoader = new S5023SceneDataLoader(executor);
        var investorBarLoader = new S5024SceneDataLoader(executor);
        var traderLoader = new S5037SceneDataLoader(executor);
        var programTradingLoader = new S5085SceneDataLoader(executor);
        var institutionNetBuyLoader = new S6067SceneDataLoader(executor);
        var orderBookLoader = new S8003SceneDataLoader(executor);

        var dataSource = new LegacySceneDataSourceRouter(
        [
            new LegacySceneDataSourceRoute(["5076"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await revenueLoader.LoadAsync(
                    selection.Subject,
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s5076", data, 1);
            }),
            new LegacySceneDataSourceRoute(["5081"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await profitLoader.LoadAsync(
                    selection.Subject,
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s5081", data, 1);
            }),
            new LegacySceneDataSourceRoute(["5082"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var data = await tradingLoader.LoadAsync(cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s5082", data, 1);
            }),
            new LegacySceneDataSourceRoute(["5001", "N5001"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var request = parameterizedRequestResolver.CreateS5001Request(entry);
                var data = await oneColumnLoader.LoadAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5001", data, 1);
            }),
            new LegacySceneDataSourceRoute(["8018", "8032", "5032"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var resolved = await parameterizedRequestResolver.ResolveTwoColumnRequestAsync(
                    entry,
                    cancellationToken).ConfigureAwait(false);
                return resolved switch
                {
                    LegacyS5032LoadRequest request => new LegacySceneDataPage(
                        "s5032",
                        await futuresPairLoader.LoadAsync(request.Request, cancellationToken)
                            .ConfigureAwait(false),
                        1),
                    LegacyS8018LoadRequest request => new LegacySceneDataPage(
                        "s8018",
                        await twoColumnLoader.LoadAsync(request.Request, cancellationToken)
                            .ConfigureAwait(false),
                        1),
                    _ => throw new LegacySceneDataException(
                        "The two-column scene selection is invalid.")
                };
            }),
            new LegacySceneDataSourceRoute(["8001", "8002"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var request = parameterizedRequestResolver.CreateS8001Request(entry);
                var data = await sectorSquareLoader.LoadAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s8001", data, 1);
            }),
            new LegacySceneDataSourceRoute(
                ["5026", "5029", "5086", "50860", "5087"],
                async (entry, page, cancellationToken) =>
                {
                    RequireFirstPage(page);
                    var resolved = await comparisonRequestResolver.ResolveAsync(
                        entry,
                        cancellationToken).ConfigureAwait(false);
                    return resolved switch
                    {
                        LegacyS5026SceneLoadRequest request => new LegacySceneDataPage(
                            "s5026",
                            await candlePairLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5029SceneLoadRequest request => new LegacySceneDataPage(
                            "s5029",
                            await comparisonYieldLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5086SceneLoadRequest request => new LegacySceneDataPage(
                            "s5086",
                            await singleYieldLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS50860SceneLoadRequest request => new LegacySceneDataPage(
                            "s50860",
                            await quotedYieldLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5087SceneLoadRequest request => new LegacySceneDataPage(
                            "s5087",
                            await linePairLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        _ => throw new LegacySceneDataException(
                            "The comparison scene selection is invalid.")
                    };
                }),
            new LegacySceneDataSourceRoute(
                ["5078", "5079", "5080", "5083", "5084"],
                async (entry, page, cancellationToken) =>
                {
                    RequireFirstPage(page);
                    var resolved = chartRequestResolver.Resolve(entry);
                    return resolved switch
                    {
                        LegacyS5078ChartLoadRequest request => new LegacySceneDataPage(
                            "s5078",
                            await sectorIndexLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5079ChartLoadRequest request => new LegacySceneDataPage(
                            "s5079",
                            await growthMetricsLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5080ChartLoadRequest request => new LegacySceneDataPage(
                            "s5080",
                            await salesLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5083ChartLoadRequest request => new LegacySceneDataPage(
                            "s5083",
                            await participantTrendLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5084ChartLoadRequest request => new LegacySceneDataPage(
                            "s5084",
                            await indexTrendLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        _ => throw new LegacySceneDataException(
                            "The chart scene selection is invalid.")
                    };
                }),
            new LegacySceneDataSourceRoute(
                ["5023", "5024", "5025", "5037", "5085", "6067", "8003"],
                async (entry, page, cancellationToken) =>
                {
                    RequireFirstPage(page);
                    var resolved = await gridMarketRequestResolver.ResolveAsync(
                        entry,
                        cancellationToken).ConfigureAwait(false);
                    return resolved switch
                    {
                        LegacyS5023SceneLoadRequest request => new LegacySceneDataPage(
                            "s5023",
                            await dailyInvestorLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5024SceneLoadRequest request => new LegacySceneDataPage(
                            "s5024",
                            await investorBarLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5025SceneLoadRequest => new LegacySceneDataPage(
                            "s5025",
                            await gridMarketRequestResolver.LoadTrustedS5025Async(
                                entry,
                                cancellationToken).ConfigureAwait(false),
                            1),
                        LegacyS5037SceneLoadRequest request => new LegacySceneDataPage(
                            "s5037",
                            await traderLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS5085SceneLoadRequest => new LegacySceneDataPage(
                            "s5085",
                            await programTradingLoader.LoadAsync(cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS6067SceneLoadRequest => new LegacySceneDataPage(
                            "s6067",
                            await institutionNetBuyLoader.LoadAsync(cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        LegacyS8003SceneLoadRequest request => new LegacySceneDataPage(
                            "s8003",
                            await orderBookLoader.LoadAsync(request.Request, cancellationToken)
                                .ConfigureAwait(false),
                            1),
                        _ => throw new LegacySceneDataException(
                            "The grid-market scene selection is invalid.")
                    };
                }),
            new LegacySceneDataSourceRoute(["5006"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var market = ParseDomesticMarket(selection.GroupCode);
                S5006SceneData data;
                if (market is LegacyDomesticEquityMarket.NxtKospi or
                    LegacyDomesticEquityMarket.NxtKosdaq)
                {
                    data = await nxtDetailLoader.LoadAsync(
                        market,
                        selection.DataCode,
                        cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    data = await domesticDetailLoader.LoadAsync(
                        new S5006DomesticSceneDataRequest(market, selection.DataCode),
                        cancellationToken).ConfigureAwait(false);
                }

                return new LegacySceneDataPage("s5006", data, 1);
            }),
            new LegacySceneDataSourceRoute(["5011"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await equityDetailLoader.LoadAsync(
                    new S5011SceneDataRequest(
                        ParseDomesticMarket(selection.GroupCode),
                        ParseClosedEnum<S5011DetailBranch>(selection.Subtype, "5011 detail branch"),
                        selection.DataCode),
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s5011", data, 1);
            }),
            new LegacySceneDataSourceRoute(["5016"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await threePanelLoader.LoadAsync(
                    ParseClosedEnum<S5016PanelTarget>(selection.Subtype, "5016 panel target"),
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s5016", data, 1);
            }),
            new LegacySceneDataSourceRoute(["50160"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await twoPanelLoader.LoadAsync(
                    ParseClosedEnum<S50160PanelTarget>(selection.Subtype, "50160 panel target"),
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s50160", data, 1);
            }),
            new LegacySceneDataSourceRoute(["6001"], async (entry, page, cancellationToken) =>
            {
                RequireFirstPage(page);
                var selection = RequireSelection(entry);
                var data = await imageQuoteLoader.LoadAsync(
                    ParseClosedEnum<S6001QuoteTarget>(selection.Subtype, "6001 quote target"),
                    cancellationToken).ConfigureAwait(false);
                return new LegacySceneDataPage("s6001", data, 1);
            }),
            new LegacySceneDataSourceRoute(
                ["8067", "5068", "5070", "5072"],
                async (entry, page, cancellationToken) =>
                {
                    RequireFirstPage(page);
                    var data = await worldMapLoader.LoadAsync(cancellationToken)
                        .ConfigureAwait(false);
                    return new LegacySceneDataPage("s8067", data, 1);
                }),
            new LegacySceneDataSourceRoute(
                ["8035", "8061", "8040", "8046", "8051", "8056"],
                async (entry, page, cancellationToken) =>
                {
                    RequireFirstPage(page);
                    var selection = RequireSelection(entry);
                    var target = ParseCandleMarket(selection.GroupCode);
                    var foreignIdentity = target is
                        CandleMarketTarget.OverseasIndex or CandleMarketTarget.OverseasStock
                            ? ParseForeignCandleIdentity(selection.DataCode, target)
                            : default;
                    var request = new S8010SceneDataRequest(
                        ParseCandleAlias(entry.CutCode),
                        target,
                        ParseCandleMode(selection.Subtype),
                        CandleRequiresInstrument(target)
                            ? RequireLookupValue(selection.Subject, "instrument")
                            : null,
                        HasClosedFlag(selection.GraphicType, "MA5"),
                        HasClosedFlag(selection.GraphicType, "MA20"),
                        foreignIdentity.Symbol,
                        foreignIdentity.NationCode);
                    var data = await candleLoader.LoadAsync(
                        request,
                        cancellationToken).ConfigureAwait(false);
                    return new LegacySceneDataPage("s8010", data, 1);
                }),
            .. CreatePagedQuoteRoutes(executor, trustedViSource)
        ]);

        LegacySceneRuntimeCoverage.Validate(dataSource);
        if (compositionOptions is not null && compositionOptionsSource is not null)
        {
            throw new ArgumentException(
                "Specify either fixed or mutable scene composition options, not both.");
        }

        var cueProvider = compositionOptionsSource is null
            ? new RegisteredLegacySceneCueProvider(dataSource, options: compositionOptions)
            : new RegisteredLegacySceneCueProvider(dataSource, compositionOptionsSource);
        return new LegacyPlayoutWorkflow(engine, cueProvider);
    }

    /// <summary>
    /// Creates the read-only PageN boundary used when a fixed 5-, 6-, or 12-row
    /// action is first added to the operator playlist. This path does not create a
    /// cue, dispatch an engine command, or touch COM/vendor objects.
    /// </summary>
    public static ILegacyScenePagePlanProvider CreatePagePlanProvider(
        IDataQueryExecutor executor,
        ITrustedViStockNameSnapshotSource? trustedViSource = null)
    {
        ArgumentNullException.ThrowIfNull(executor);
        var dataSource = new LegacySceneDataSourceRouter(
            CreatePagedQuoteRoutes(executor, trustedViSource));
        return new RegisteredLegacySceneCueProvider(dataSource);
    }

    private static IReadOnlyList<LegacySceneDataSourceRoute> CreatePagedQuoteRoutes(
        IDataQueryExecutor executor,
        ITrustedViStockNameSnapshotSource? trustedViSource)
    {
        var fiveRowLoader = new S5074SceneDataLoader(executor);
        var sixRowLoader = new S5077SceneDataLoader(executor);
        var twelveRowLoader = new S5088SceneDataLoader(executor);
        return
        [
            new LegacySceneDataSourceRoute(["5074"], async (entry, page, cancellationToken) =>
            {
                var selection = RequireSelection(entry);
                var request = selection.GroupCode == "PAGED_VI"
                    ? await TrustedViManualReference.ResolveAsync(
                        selection,
                        page,
                        trustedViSource,
                        cancellationToken).ConfigureAwait(false)
                    : CreatePagedQuoteRequest(selection, page);
                var data = await fiveRowLoader.LoadAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5074", data, data.Rows.Count);
            }, async (entry, cancellationToken) =>
            {
                var selection = RequireSelection(entry);
                var request = selection.GroupCode == "PAGED_VI"
                    ? await TrustedViManualReference.ResolveAsync(
                        selection,
                        pageIndexZeroBased: 0,
                        trustedSource: trustedViSource,
                        cancellationToken: cancellationToken).ConfigureAwait(false)
                    : CreatePagedQuoteRequest(selection, 0);
                var data = await fiveRowLoader.LoadPagePlanAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5074", data, data.Rows.Count);
            }),
            new LegacySceneDataSourceRoute(["5077"], async (entry, page, cancellationToken) =>
            {
                var request = CreatePagedQuoteRequest(RequireSelection(entry), page);
                var data = await sixRowLoader.LoadAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5077", data, data.Rows.Count);
            }, async (entry, cancellationToken) =>
            {
                var request = CreatePagedQuoteRequest(RequireSelection(entry), 0);
                var data = await sixRowLoader.LoadPagePlanAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5077", data, data.Rows.Count);
            }),
            new LegacySceneDataSourceRoute(["5088"], async (entry, page, cancellationToken) =>
            {
                var request = CreatePagedQuoteRequest(RequireSelection(entry), page);
                var data = await twelveRowLoader.LoadAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5088", data, data.Rows.Count);
            }, async (entry, cancellationToken) =>
            {
                var request = CreatePagedQuoteRequest(RequireSelection(entry), 0);
                var data = await twelveRowLoader.LoadPagePlanAsync(request, cancellationToken)
                    .ConfigureAwait(false);
                return new LegacySceneDataPage("s5088", data, data.Rows.Count);
            })
        ];
    }

    private static LegacySceneSelection RequireSelection(LegacyPlaylistEntry entry) =>
        entry.Selection ?? throw new LegacySceneDataException(
            "The selected cut requires native database lookup information.");

    private static void RequireFirstPage(int pageIndexZeroBased)
    {
        if (pageIndexZeroBased != 0)
        {
            throw new LegacySceneDataException("The selected cut is not paged.");
        }
    }

    private static LegacyDomesticEquityMarket ParseDomesticMarket(string value) => value switch
    {
        "KOSPI" or "KOSPI_STOCK" or "코스피" or "코스피종목" =>
            LegacyDomesticEquityMarket.Kospi,
        "KOSDAQ" or "KOSDAQ_STOCK" or "코스닥" or "코스닥종목" =>
            LegacyDomesticEquityMarket.Kosdaq,
        "NXT_KOSPI" or "NXT코스피" or "코스피(NXT)" =>
            LegacyDomesticEquityMarket.NxtKospi,
        "NXT_KOSDAQ" or "NXT코스닥" or "코스닥(NXT)" =>
            LegacyDomesticEquityMarket.NxtKosdaq,
        _ => throw new LegacySceneDataException(
            "The domestic equity market selection is invalid.")
    };

    private static CandleCutAlias ParseCandleAlias(string value) => value switch
    {
        "8035" => CandleCutAlias.Cut8035,
        "8061" => CandleCutAlias.Cut8061,
        "8040" => CandleCutAlias.Cut8040,
        "8046" => CandleCutAlias.Cut8046,
        "8051" => CandleCutAlias.Cut8051,
        "8056" => CandleCutAlias.Cut8056,
        _ => throw new LegacySceneDataException("The candle cut alias is invalid.")
    };

    private static CandleMarketTarget ParseCandleMarket(string value) => value switch
    {
        "KOSPI_INDEX" or "코스피지수" => CandleMarketTarget.KospiIndex,
        "KOSDAQ_INDEX" or "코스닥지수" => CandleMarketTarget.KosdaqIndex,
        "KOSPI200_INDEX" or "코스피200" => CandleMarketTarget.Kospi200Index,
        "KRX100_INDEX" or "KRX100" => CandleMarketTarget.Krx100Index,
        "FUTURES_INDEX" or "선물" => CandleMarketTarget.FuturesIndex,
        "KOSPI_INDUSTRY" or "코스피업종" => CandleMarketTarget.KospiIndustry,
        "KOSDAQ_INDUSTRY" or "코스닥업종" => CandleMarketTarget.KosdaqIndustry,
        "FOREIGN_INDEX" or "해외지수" => CandleMarketTarget.OverseasIndex,
        "FOREIGN_STOCK" or "해외종목" => CandleMarketTarget.OverseasStock,
        "KOSPI_STOCK" or "코스피종목" => CandleMarketTarget.KospiStock,
        "KOSDAQ_STOCK" or "코스닥종목" => CandleMarketTarget.KosdaqStock,
        _ => throw new LegacySceneDataException("The candle market selection is invalid.")
    };

    private static CandleChartMode ParseCandleMode(string value) => value switch
    {
        "" or "PRICE" or "현재가" => CandleChartMode.Price,
        "EXPECTED" or "예상체결" => CandleChartMode.ExpectedExecution,
        "VOLUME" or "거래량" => CandleChartMode.PriceWithVolume,
        "TRADING_HALT" or "거래정지" => CandleChartMode.TradingHalt,
        _ => throw new LegacySceneDataException("The candle chart mode is invalid.")
    };

    private static bool CandleRequiresInstrument(CandleMarketTarget value) => value is
        CandleMarketTarget.KospiIndustry or
        CandleMarketTarget.KosdaqIndustry or
        CandleMarketTarget.OverseasIndex or
        CandleMarketTarget.OverseasStock or
        CandleMarketTarget.KospiStock or
        CandleMarketTarget.KosdaqStock;

    private static (string? Symbol, string? NationCode) ParseForeignCandleIdentity(
        string value,
        CandleMarketTarget target)
    {
        var parts = value.Split('|', StringSplitOptions.None);
        var expectedType = target switch
        {
            CandleMarketTarget.OverseasIndex => "0",
            CandleMarketTarget.OverseasStock => "1",
            _ => throw new LegacySceneDataException(
                "The foreign candle target is invalid.")
        };
        if (parts.Length != 3 ||
            !LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol(parts[0]) ||
            parts[1].Length != 2 ||
            parts[1].Any(character => character is < 'A' or > 'Z') ||
            !string.Equals(parts[2], expectedType, StringComparison.Ordinal))
        {
            throw new LegacySceneDataException(
                "The foreign candle identity is invalid.");
        }

        return (parts[0], parts[1]);
    }

    private static string RequireLookupValue(string value, string label)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new LegacySceneDataException($"The scene {label} selection is required.");
        }

        return value;
    }

    private static string ResolveNxtThemeLoaderTitle(LegacySceneSelection selection)
        => RequireLookupValue(selection.Subject, "NXT theme");

    private static bool HasClosedFlag(string value, string flag) =>
        value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
            .Contains(flag, StringComparer.Ordinal);

    private static PagedQuoteLoadRequest CreatePagedQuoteRequest(
        LegacySceneSelection selection,
        int pageIndexZeroBased)
    {
        var page = checked(pageIndexZeroBased + 1);
        return selection.GroupCode switch
        {
            "PAGED_DOMESTIC_KOSPI" => new DomesticPagedQuoteLoadRequest(
                PagedQuoteMarket.Kospi,
                ParseDomesticPagedList(selection.Subtype),
                ParseAsOfDate(selection.DataCode),
                page),
            "PAGED_DOMESTIC_KOSDAQ" => new DomesticPagedQuoteLoadRequest(
                PagedQuoteMarket.Kosdaq,
                ParseDomesticPagedList(selection.Subtype),
                ParseAsOfDate(selection.DataCode),
                page),
            "PAGED_NXT_KOSPI" => new NxtPagedQuoteLoadRequest(
                PagedQuoteMarket.Kospi,
                ParseNxtPagedList(selection.Subtype),
                ParseNxtSession(selection.GraphicType),
                page),
            "PAGED_NXT_KOSDAQ" => new NxtPagedQuoteLoadRequest(
                PagedQuoteMarket.Kosdaq,
                ParseNxtPagedList(selection.Subtype),
                ParseNxtSession(selection.GraphicType),
                page),
            "PAGED_THEME" => new ThemePagedQuoteLoadRequest(
                RequireLookupValue(selection.DataCode, "theme code"),
                RequireLookupValue(selection.ExactSubject ?? selection.Subject, "theme"),
                ParseThemeSort(selection.GraphicType),
                ParseThemeValueKind(selection.Subtype),
                page),
            "PAGED_NXT_THEME" => new NxtThemePagedQuoteLoadRequest(
                RequireLookupValue(selection.DataCode, "NXT theme code"),
                ResolveNxtThemeLoaderTitle(selection),
                ParseThemeSort(selection.Subtype),
                ParseNxtSession(selection.GraphicType),
                page,
                selection.ExactSubject),
            "PAGED_EXPERT" => new ExpertPagedQuoteLoadRequest(
                RequireLookupValue(selection.DataCode, "expert code"),
                RequireLookupValue(selection.Subject, "expert"),
                page),
            "PAGED_VI" => new ViPagedQuoteLoadRequest(
                ParseStockNames(selection.Subject),
                page),
            _ => throw new LegacySceneDataException("The paged quote branch is invalid.")
        };
    }

    private static DomesticPagedQuoteList ParseDomesticPagedList(string value) => value switch
    {
        "EXPECTED_MARKET_CAP" => DomesticPagedQuoteList.ExpectedMarketCapitalization,
        "MARKET_CAP" => DomesticPagedQuoteList.MarketCapitalization,
        "INDUSTRY" => DomesticPagedQuoteList.Industry,
        "AFTER_HOURS_GAIN_RATE" => DomesticPagedQuoteList.AfterHoursGainRate,
        "AFTER_HOURS_LOSS_RATE" => DomesticPagedQuoteList.AfterHoursLossRate,
        "GAIN_RATE" => DomesticPagedQuoteList.GainRate,
        "LOSS_RATE" => DomesticPagedQuoteList.LossRate,
        "52_WEEK_HIGH" => DomesticPagedQuoteList.FiftyTwoWeekHigh,
        "52_WEEK_LOW" => DomesticPagedQuoteList.FiftyTwoWeekLow,
        "KOSPI200" => DomesticPagedQuoteList.Kospi200,
        "ETF" => DomesticPagedQuoteList.Etf,
        _ => throw new LegacySceneDataException("The domestic paged quote list is invalid.")
    };

    private static NxtPagedQuoteList ParseNxtPagedList(string value) => value switch
    {
        "MARKET_CAP" => NxtPagedQuoteList.MarketCapitalization,
        "AFTER_HOURS_GAIN_RATE" => NxtPagedQuoteList.AfterHoursGainRate,
        "AFTER_HOURS_LOSS_RATE" => NxtPagedQuoteList.AfterHoursLossRate,
        "GAIN_RATE" => NxtPagedQuoteList.GainRate,
        "LOSS_RATE" => NxtPagedQuoteList.LossRate,
        "VOLUME" => NxtPagedQuoteList.Volume,
        _ => throw new LegacySceneDataException("The NXT paged quote list is invalid.")
    };

    private static NxtMarketSession ParseNxtSession(string value) => value switch
    {
        "PRE_MARKET" => NxtMarketSession.PreMarket,
        "AFTER_MARKET" => NxtMarketSession.AfterMarket,
        _ => throw new LegacySceneDataException("The NXT market session is invalid.")
    };

    private static PagedQuoteThemeSort ParseThemeSort(string value) => value switch
    {
        "" or "INPUT_ORDER" => PagedQuoteThemeSort.InputOrder,
        "GAIN_DESC" => PagedQuoteThemeSort.GainRateDescending,
        "GAIN_ASC" => PagedQuoteThemeSort.GainRateAscending,
        _ => throw new LegacySceneDataException("The theme sort selection is invalid.")
    };

    private static PagedQuoteThemeValueKind ParseThemeValueKind(string value) => value switch
    {
        "" or "CURRENT" => PagedQuoteThemeValueKind.Current,
        "EXPECTED" => PagedQuoteThemeValueKind.ExpectedExecution,
        _ => throw new LegacySceneDataException("The theme value selection is invalid.")
    };

    private static DateOnly ParseAsOfDate(string value) =>
        DateOnly.TryParseExact(
            value,
            "yyyy-MM-dd",
            System.Globalization.CultureInfo.InvariantCulture,
            System.Globalization.DateTimeStyles.None,
            out var date)
            ? date
            : throw new LegacySceneDataException("The paged quote as-of date is invalid.");

    private static IReadOnlyList<string> ParseStockNames(string value)
    {
        var names = value.Split(
            ',',
            StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
        if (names.Length is < 1 or > 240)
        {
            throw new LegacySceneDataException("The VI stock-name selection is invalid.");
        }

        return Array.AsReadOnly(names);
    }

    private static TEnum ParseClosedEnum<TEnum>(string value, string label)
        where TEnum : struct, Enum
    {
        if (!Enum.TryParse<TEnum>(value, ignoreCase: false, out var parsed) ||
            !Enum.IsDefined(parsed))
        {
            throw new LegacySceneDataException($"The {label} selection is invalid.");
        }

        return parsed;
    }
}
