using System.Collections.ObjectModel;
using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyFixedMarket
{
    Overseas,
    Exchange,
    Index
}

public enum LegacyFixedCategory
{
    Plate,
    Market,
    Chart
}

public sealed record LegacyFixedSelection(
    string GroupCode,
    string Subject,
    string GraphicType,
    string Subtype,
    string DataCode);

public readonly record struct LegacyMovingAverageSelection(bool Ma5, bool Ma20);

public sealed record LegacyFixedRuntimeAsset(
    LegacyFixedMarket Market,
    string RelativePath,
    string Sha256);

public sealed class LegacyFixedAction
{
    internal LegacyFixedAction(
        string id,
        LegacyFixedMarket market,
        string section,
        string label,
        string detail,
        LegacyFixedCategory category,
        string builderKey,
        string code,
        IReadOnlyList<string> aliases,
        bool available,
        string prerequisite,
        LegacyFixedSelection selection,
        bool dynamicDate,
        bool dynamicNxtSession)
    {
        Id = id;
        Market = market;
        Section = section;
        Label = label;
        Detail = detail;
        Category = category;
        BuilderKey = builderKey;
        Code = code;
        Aliases = aliases;
        Available = available;
        Prerequisite = prerequisite;
        Selection = selection;
        DynamicDate = dynamicDate;
        DynamicNxtSession = dynamicNxtSession;
    }

    public string Id { get; }

    public LegacyFixedMarket Market { get; }

    public string Section { get; }

    public string Label { get; }

    public string Detail { get; }

    public LegacyFixedCategory Category { get; }

    public string BuilderKey { get; }

    public string Code { get; }

    public IReadOnlyList<string> Aliases { get; }

    public bool Available { get; }

    public string Prerequisite { get; }

    public LegacyFixedSelection Selection { get; }

    public bool DynamicDate { get; }

    public bool DynamicNxtSession { get; }

    public bool SupportsMovingAverages =>
        string.Equals(BuilderKey, "s8010", StringComparison.Ordinal);
}

public sealed record LegacyMaterializedFixedAction(
    LegacyFixedAction Action,
    LegacyFixedSelection Selection,
    LegacyFixedRuntimeAsset RuntimeAsset);

public sealed class LegacyFixedSection
{
    internal LegacyFixedSection(
        LegacyFixedMarket market,
        string name,
        IReadOnlyList<LegacyFixedAction> actions)
    {
        Market = market;
        Name = name;
        Actions = actions;
    }

    public LegacyFixedMarket Market { get; }

    public string Name { get; }

    public IReadOnlyList<LegacyFixedAction> Actions { get; }
}

/// <summary>
/// C#-authoritative catalog for the three fixed legacy UC1 tabs. The embedded JSON is a
/// generated build asset; callers can select only by the opaque action id and cannot provide
/// builder, cut alias, or scene-selection values.
/// </summary>
public sealed class LegacyFixedActionCatalog
{
    public const int ExpectedActionCount = 328;
    public const int ExpectedOverseasCount = 78;
    public const int ExpectedExchangeCount = 46;
    public const int ExpectedIndexCount = 204;
    public const int ExpectedAvailableCount = 322;
    public const string ExpectedDocumentSha256 =
        "E56FB9DE8247672022EE2F324AF4C37179EC58E399758425AD4C29DAF1BDDC56";
    public const string ExpectedGeneratorSourceSha256 =
        "31D1B0F81A53FF47700D179E45BEFCA55342D11EAD3A58E67F6A926607A874B5";

    private const string ResourceName =
        "MBN_STOCK_WEBVIEW.LegacyApplication.Catalog.LegacyFixedActions.json";
    private const string ExpectedGeneratedFrom = "Web/legacy-fixed-workflow.js";

    private static readonly JsonSerializerOptions StrictJsonOptions = new()
    {
        AllowTrailingCommas = false,
        PropertyNameCaseInsensitive = false,
        ReadCommentHandling = JsonCommentHandling.Disallow,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
    };

    private static readonly Lazy<LegacyFixedActionCatalog> EmbeddedCatalog =
        new(LoadEmbedded, LazyThreadSafetyMode.ExecutionAndPublication);

    private readonly IReadOnlyDictionary<string, LegacyFixedAction> _byId;
    private readonly IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>>
        _byMarket;
    private readonly IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>
        _sectionsByMarket;
    private readonly IReadOnlyDictionary<(LegacyFixedMarket Market, string Section),
        IReadOnlyList<LegacyFixedAction>> _bySection;

    private LegacyFixedActionCatalog(
        IReadOnlyList<LegacyFixedAction> actions,
        IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset> runtimeAssets,
        string documentSha256,
        string generatorSourceSha256)
    {
        Actions = actions;
        RuntimeAssets = runtimeAssets;
        DocumentSha256 = documentSha256;
        GeneratorSourceSha256 = generatorSourceSha256;

        _byId = new ReadOnlyDictionary<string, LegacyFixedAction>(
            actions.ToDictionary(action => action.Id, StringComparer.Ordinal));

        _byMarket = new ReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>>(
            Enum.GetValues<LegacyFixedMarket>().ToDictionary(
                market => market,
                market => ReadOnly(actions.Where(action => action.Market == market))));

        var sectionsByMarket = new Dictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>();
        var bySection = new Dictionary<(LegacyFixedMarket Market, string Section),
            IReadOnlyList<LegacyFixedAction>>();
        foreach (var market in Enum.GetValues<LegacyFixedMarket>())
        {
            var sections = _byMarket[market]
                .GroupBy(action => action.Section, StringComparer.Ordinal)
                .Select(group =>
                {
                    var sectionActions = ReadOnly(group);
                    bySection.Add((market, group.Key), sectionActions);
                    return new LegacyFixedSection(market, group.Key, sectionActions);
                });
            sectionsByMarket.Add(market, ReadOnly(sections));
        }

        _sectionsByMarket =
            new ReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedSection>>(
                sectionsByMarket);
        _bySection = new ReadOnlyDictionary<(LegacyFixedMarket Market, string Section),
            IReadOnlyList<LegacyFixedAction>>(bySection);
    }

    public static LegacyFixedActionCatalog Default => EmbeddedCatalog.Value;

    public IReadOnlyList<LegacyFixedAction> Actions { get; }

    public IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset> RuntimeAssets { get; }

    public string DocumentSha256 { get; }

    public string GeneratorSourceSha256 { get; }

    public IReadOnlyList<LegacyFixedAction> GetActions(LegacyFixedMarket market)
    {
        RequireMarket(market);
        return _byMarket[market];
    }

    public IReadOnlyList<LegacyFixedSection> GetSections(LegacyFixedMarket market)
    {
        RequireMarket(market);
        return _sectionsByMarket[market];
    }

    public IReadOnlyList<LegacyFixedAction> GetActions(
        LegacyFixedMarket market,
        string section)
    {
        RequireMarket(market);
        ArgumentException.ThrowIfNullOrWhiteSpace(section);
        return _bySection.TryGetValue((market, section), out var actions)
            ? actions
            : Array.Empty<LegacyFixedAction>();
    }

    public LegacyFixedAction? FindAction(string? actionId) =>
        actionId is not null && _byId.TryGetValue(actionId, out var action)
            ? action
            : null;

    public LegacyFixedAction GetAction(string actionId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(actionId);
        return _byId.TryGetValue(actionId, out var action)
            ? action
            : throw new KeyNotFoundException($"Unknown fixed legacy action id '{actionId}'.");
    }

    public LegacyFixedRuntimeAsset GetRuntimeAsset(LegacyFixedMarket market)
    {
        RequireMarket(market);
        return RuntimeAssets[market];
    }

    internal LegacyFixedActionCatalog WithRuntimeOrder(
        IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>> actionsByMarket)
    {
        ArgumentNullException.ThrowIfNull(actionsByMarket);
        var ordered = new List<LegacyFixedAction>(Actions.Count);
        foreach (var market in Enum.GetValues<LegacyFixedMarket>())
        {
            var source = actionsByMarket.TryGetValue(market, out var runtimeActions)
                ? runtimeActions
                : _byMarket[market];
            if (source is null || source.Count != _byMarket[market].Count ||
                source.Any(action => action is null || action.Market != market) ||
                !source.Select(action => action.Id).ToHashSet(StringComparer.Ordinal)
                    .SetEquals(_byMarket[market].Select(action => action.Id)))
            {
                throw new InvalidDataException(
                    $"The runtime fixed-action order for '{MarketText(market)}' is invalid.");
            }

            ordered.AddRange(source);
        }

        return new LegacyFixedActionCatalog(
            ReadOnly(ordered),
            RuntimeAssets,
            DocumentSha256,
            GeneratorSourceSha256);
    }

    public LegacyMaterializedFixedAction Materialize(
        string actionId,
        DateTimeOffset localNow)
    {
        var action = GetAction(actionId);
        if (!action.Available)
        {
            throw new InvalidOperationException(action.Prerequisite);
        }

        var graphicType = action.DynamicNxtSession
            ? localNow.Hour <= 12 ? "PRE_MARKET" : "AFTER_MARKET"
            : action.Selection.GraphicType;
        var dataCode = action.DynamicDate
            ? localNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
            : action.Selection.DataCode;

        var selection = action.Selection with
        {
            GraphicType = graphicType,
            DataCode = dataCode
        };
        return new LegacyMaterializedFixedAction(
            action,
            selection,
            RuntimeAssets[action.Market]);
    }

    internal static LegacyFixedActionCatalog LoadForValidation(
        ReadOnlyMemory<byte> utf8Json,
        string? expectedDocumentSha256 = null) =>
        Load(utf8Json.ToArray(), expectedDocumentSha256);

    private static LegacyFixedActionCatalog LoadEmbedded()
    {
        using var stream = typeof(LegacyFixedActionCatalog).Assembly
            .GetManifestResourceStream(ResourceName)
            ?? throw new InvalidDataException(
                $"The embedded fixed legacy catalog '{ResourceName}' is missing.");
        using var buffer = new MemoryStream();
        stream.CopyTo(buffer);
        return Load(buffer.ToArray(), ExpectedDocumentSha256);
    }

    private static LegacyFixedActionCatalog Load(byte[] utf8Json, string? expectedDocumentSha256)
    {
        ArgumentNullException.ThrowIfNull(utf8Json);
        if (utf8Json.Length == 0)
        {
            throw new InvalidDataException("The fixed legacy catalog is empty.");
        }

        CatalogDocumentDto document;
        try
        {
            using var parsed = JsonDocument.Parse(utf8Json, new JsonDocumentOptions
            {
                AllowTrailingCommas = false,
                CommentHandling = JsonCommentHandling.Disallow
            });
            RejectDuplicateProperties(parsed.RootElement, "$root");
            document = JsonSerializer.Deserialize<CatalogDocumentDto>(utf8Json, StrictJsonOptions)
                ?? throw new InvalidDataException("The fixed legacy catalog root is null.");
        }
        catch (JsonException exception)
        {
            throw new InvalidDataException("The fixed legacy catalog JSON schema is invalid.", exception);
        }

        var documentHash = Convert.ToHexString(SHA256.HashData(utf8Json));
        if (expectedDocumentSha256 is not null &&
            !string.Equals(documentHash, expectedDocumentSha256, StringComparison.Ordinal))
        {
            throw new InvalidDataException(
                $"The fixed legacy catalog hash is invalid. Expected {expectedDocumentSha256}; actual {documentHash}.");
        }

        ValidateDocumentMetadata(document);
        var runtimeAssets = CreateRuntimeAssets(document.RuntimeAssets);
        var actions = CreateActions(document.Actions);
        ValidateCatalogInvariants(actions);
        return new LegacyFixedActionCatalog(
            ReadOnly(actions),
            runtimeAssets,
            documentHash,
            document.SourceSha256);
    }

    private static void ValidateDocumentMetadata(CatalogDocumentDto document)
    {
        if (document.SchemaVersion != 1)
        {
            throw Invalid("schemaVersion must be 1");
        }

        if (!string.Equals(document.GeneratedFrom, ExpectedGeneratedFrom, StringComparison.Ordinal))
        {
            throw Invalid($"generatedFrom must be '{ExpectedGeneratedFrom}'");
        }

        if (!string.Equals(
                document.SourceSha256,
                ExpectedGeneratorSourceSha256,
                StringComparison.Ordinal))
        {
            throw Invalid("the generator source hash is not approved");
        }

        if (document.RuntimeAssets is null)
        {
            throw Invalid("runtimeAssets is required");
        }

        if (document.Actions is null)
        {
            throw Invalid("actions is required");
        }
    }

    private static IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>
        CreateRuntimeAssets(RuntimeAssetsDto runtimeAssets)
    {
        var assets = new Dictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>
        {
            [LegacyFixedMarket.Overseas] = ValidateRuntimeAsset(
                LegacyFixedMarket.Overseas,
                runtimeAssets.Overseas,
                "bin/Debug/Res/해외.ini",
                "DBDC4EAC28BED9FD0DEABED7FE880F21CB49E48503FD3CCB95C0A29128818B2A"),
            [LegacyFixedMarket.Exchange] = ValidateRuntimeAsset(
                LegacyFixedMarket.Exchange,
                runtimeAssets.Exchange,
                "bin/Debug/Res/환율.ini",
                "7A99F8DFB5DD8D8DB514605F232A0A7973545CA6A4A46ABE2D102ADD025B586C"),
            [LegacyFixedMarket.Index] = ValidateRuntimeAsset(
                LegacyFixedMarket.Index,
                runtimeAssets.Index,
                "bin/Debug/Res/지수.ini",
                "E90ED317FD3ECC1A716AD17DD9BC21FE8B838574E1C20854BD0A886A96BA17E6")
        };
        return new ReadOnlyDictionary<LegacyFixedMarket, LegacyFixedRuntimeAsset>(assets);
    }

    private static LegacyFixedRuntimeAsset ValidateRuntimeAsset(
        LegacyFixedMarket market,
        RuntimeAssetDto asset,
        string expectedPath,
        string expectedSha256)
    {
        if (asset is null ||
            !string.Equals(asset.RelativePath, expectedPath, StringComparison.Ordinal) ||
            !string.Equals(asset.Sha256, expectedSha256, StringComparison.Ordinal))
        {
            throw Invalid($"runtime asset metadata for '{MarketText(market)}' is invalid");
        }

        RequireSha256(asset.Sha256, $"runtimeAssets.{MarketText(market)}.sha256");
        return new LegacyFixedRuntimeAsset(market, asset.RelativePath, asset.Sha256);
    }

    private static LegacyFixedAction[] CreateActions(IReadOnlyList<ActionDto> source)
    {
        if (source.Count != ExpectedActionCount)
        {
            throw Invalid($"exactly {ExpectedActionCount} actions are required");
        }

        var actions = new LegacyFixedAction[source.Count];
        for (var index = 0; index < source.Count; index++)
        {
            var dto = source[index]
                ?? throw Invalid($"action at index {index} is null");
            var expectedId = $"fixed-{index + 1:000}";
            if (!string.Equals(dto.Id, expectedId, StringComparison.Ordinal))
            {
                throw Invalid($"action id at index {index} must be '{expectedId}'");
            }

            var market = ParseMarket(dto.Market);
            var category = ParseCategory(dto.Category);
            var section = RequireText(dto.Section, $"{expectedId}.section");
            var label = RequireText(dto.Label, $"{expectedId}.label");
            var detail = RequireText(dto.Detail, $"{expectedId}.detail");
            var builderKey = RequireText(dto.BuilderKey, $"{expectedId}.builderKey");
            var code = RequireText(dto.Code, $"{expectedId}.code");
            ValidateSceneIdentity(expectedId, builderKey, code, dto.Aliases);
            var selection = ValidateSelection(expectedId, dto);

            var prerequisite = RequireText(
                dto.Prerequisite,
                $"{expectedId}.prerequisite",
                allowEmpty: true,
                maximumLength: 512);
            if (dto.Available != string.IsNullOrEmpty(prerequisite))
            {
                throw Invalid($"{expectedId} availability and prerequisite do not agree");
            }

            actions[index] = new LegacyFixedAction(
                expectedId,
                market,
                section,
                label,
                detail,
                category,
                builderKey,
                code,
                Array.AsReadOnly(dto.Aliases.ToArray()),
                dto.Available,
                prerequisite,
                selection,
                dto.DynamicDate,
                dto.DynamicNxtSession);
        }

        return actions;
    }

    private static void ValidateSceneIdentity(
        string actionId,
        string builderKey,
        string code,
        IReadOnlyList<string> aliases)
    {
        if (aliases is null)
        {
            throw Invalid($"{actionId}.aliases is required");
        }
        var definition = LegacySceneCatalog.Definitions.SingleOrDefault(candidate =>
            string.Equals(candidate.BuilderKey, builderKey, StringComparison.Ordinal))
            ?? throw Invalid($"{actionId} has unknown builder '{builderKey}'");

        if (!definition.CutAliases.Contains(code, StringComparer.Ordinal))
        {
            throw Invalid($"{actionId} code '{code}' is not an alias of '{builderKey}'");
        }

        if (aliases.Count != 1 || !string.Equals(aliases[0], code, StringComparison.Ordinal))
        {
            throw Invalid($"{actionId} must declare exactly its code as its sole alias");
        }

        var aliasMatches = LegacySceneCatalog.FindByCutAlias(code);
        if (!aliasMatches.Any(candidate =>
                string.Equals(candidate.BuilderKey, builderKey, StringComparison.Ordinal)))
        {
            throw Invalid($"{actionId} alias '{code}' does not resolve to '{builderKey}'");
        }
    }

    private static LegacyFixedSelection ValidateSelection(string actionId, ActionDto action)
    {
        var source = action.Selection
            ?? throw Invalid($"{actionId}.selection is required");
        var selection = new LegacyFixedSelection(
            RequireText(source.GroupCode, $"{actionId}.selection.groupCode"),
            RequireText(source.Subject, $"{actionId}.selection.subject", allowEmpty: true),
            RequireText(source.GraphicType, $"{actionId}.selection.graphicType", allowEmpty: true),
            RequireText(source.Subtype, $"{actionId}.selection.subtype", allowEmpty: true),
            RequireText(source.DataCode, $"{actionId}.selection.dataCode", allowEmpty: true));

        if (action.DynamicDate && action.DynamicNxtSession)
        {
            throw Invalid($"{actionId} cannot have two dynamic selection fields");
        }

        if (action.DynamicDate !=
            string.Equals(selection.DataCode, "$TODAY", StringComparison.Ordinal))
        {
            throw Invalid($"{actionId} has an invalid dynamic-date declaration");
        }

        if (action.DynamicNxtSession !=
            string.Equals(selection.GraphicType, "$NXT_SESSION", StringComparison.Ordinal))
        {
            throw Invalid($"{actionId} has an invalid NXT-session declaration");
        }

        if (string.Equals(action.BuilderKey, "s8010", StringComparison.Ordinal) &&
            !string.IsNullOrEmpty(selection.GraphicType))
        {
            throw Invalid($"{actionId} moving-average flags must be materialized by C#");
        }

        var values = new[]
        {
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            selection.Subtype,
            selection.DataCode
        };
        if (values.Any(value => value.Contains('$', StringComparison.Ordinal)) &&
            !action.DynamicDate && !action.DynamicNxtSession)
        {
            throw Invalid($"{actionId} contains an unapproved selection placeholder");
        }

        return selection;
    }

    private static void ValidateCatalogInvariants(IReadOnlyList<LegacyFixedAction> actions)
    {
        RequireCount(actions, LegacyFixedMarket.Overseas, ExpectedOverseasCount);
        RequireCount(actions, LegacyFixedMarket.Exchange, ExpectedExchangeCount);
        RequireCount(actions, LegacyFixedMarket.Index, ExpectedIndexCount);
        if (actions.Count(action => action.Available) != ExpectedAvailableCount)
        {
            throw Invalid($"exactly {ExpectedAvailableCount} actions must be available");
        }

        if (actions.Take(ExpectedOverseasCount).Any(action =>
                action.Market != LegacyFixedMarket.Overseas) ||
            actions.Skip(ExpectedOverseasCount).Take(ExpectedExchangeCount).Any(action =>
                action.Market != LegacyFixedMarket.Exchange) ||
            actions.Skip(ExpectedOverseasCount + ExpectedExchangeCount).Any(action =>
                action.Market != LegacyFixedMarket.Index))
        {
            throw Invalid("market action ranges are not in the approved legacy order");
        }
    }

    private static void RequireCount(
        IEnumerable<LegacyFixedAction> actions,
        LegacyFixedMarket market,
        int expected)
    {
        var actual = actions.Count(action => action.Market == market);
        if (actual != expected)
        {
            throw Invalid(
                $"market '{MarketText(market)}' must contain {expected} actions, not {actual}");
        }
    }

    private static LegacyFixedMarket ParseMarket(string value) => value switch
    {
        "overseas" => LegacyFixedMarket.Overseas,
        "exchange" => LegacyFixedMarket.Exchange,
        "index" => LegacyFixedMarket.Index,
        _ => throw Invalid($"unknown market '{value}'")
    };

    private static LegacyFixedCategory ParseCategory(string value) => value switch
    {
        "plate" => LegacyFixedCategory.Plate,
        "market" => LegacyFixedCategory.Market,
        "chart" => LegacyFixedCategory.Chart,
        _ => throw Invalid($"unknown category '{value}'")
    };

    private static string MarketText(LegacyFixedMarket market) => market switch
    {
        LegacyFixedMarket.Overseas => "overseas",
        LegacyFixedMarket.Exchange => "exchange",
        LegacyFixedMarket.Index => "index",
        _ => throw new ArgumentOutOfRangeException(nameof(market), market, "Unknown market.")
    };

    private static void RequireMarket(LegacyFixedMarket market)
    {
        if (!Enum.IsDefined(market))
        {
            throw new ArgumentOutOfRangeException(nameof(market), market, "Unknown market.");
        }
    }

    private static string RequireText(
        string value,
        string field,
        bool allowEmpty = false,
        int maximumLength = 256)
    {
        if (value is null || (!allowEmpty && value.Length == 0) ||
            value.Length > maximumLength || value.Any(char.IsControl) ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal))
        {
            throw Invalid($"{field} is invalid");
        }

        return value;
    }

    private static void RequireSha256(string value, string field)
    {
        if (value is null || value.Length != 64 || value.Any(character =>
                !(char.IsAsciiDigit(character) || character is >= 'A' and <= 'F')))
        {
            throw Invalid($"{field} is not an uppercase SHA-256 value");
        }
    }

    private static void RejectDuplicateProperties(JsonElement element, string path)
    {
        if (element.ValueKind == JsonValueKind.Object)
        {
            var names = new HashSet<string>(StringComparer.Ordinal);
            foreach (var property in element.EnumerateObject())
            {
                if (!names.Add(property.Name))
                {
                    throw new JsonException($"Duplicate property '{property.Name}' at {path}.");
                }

                RejectDuplicateProperties(property.Value, $"{path}.{property.Name}");
            }
        }
        else if (element.ValueKind == JsonValueKind.Array)
        {
            var index = 0;
            foreach (var item in element.EnumerateArray())
            {
                RejectDuplicateProperties(item, $"{path}[{index++}]");
            }
        }
    }

    private static IReadOnlyList<T> ReadOnly<T>(IEnumerable<T> source) =>
        Array.AsReadOnly(source.ToArray());

    private static InvalidDataException Invalid(string message) =>
        new($"The fixed legacy catalog is invalid: {message}.");

    private sealed class CatalogDocumentDto
    {
        [JsonPropertyName("schemaVersion")]
        public required int SchemaVersion { get; init; }

        [JsonPropertyName("generatedFrom")]
        public required string GeneratedFrom { get; init; }

        [JsonPropertyName("sourceSha256")]
        public required string SourceSha256 { get; init; }

        [JsonPropertyName("runtimeAssets")]
        public required RuntimeAssetsDto RuntimeAssets { get; init; }

        [JsonPropertyName("actions")]
        public required List<ActionDto> Actions { get; init; }
    }

    private sealed class RuntimeAssetsDto
    {
        [JsonPropertyName("overseas")]
        public required RuntimeAssetDto Overseas { get; init; }

        [JsonPropertyName("exchange")]
        public required RuntimeAssetDto Exchange { get; init; }

        [JsonPropertyName("index")]
        public required RuntimeAssetDto Index { get; init; }
    }

    private sealed class RuntimeAssetDto
    {
        [JsonPropertyName("relativePath")]
        public required string RelativePath { get; init; }

        [JsonPropertyName("sha256")]
        public required string Sha256 { get; init; }
    }

    private sealed class ActionDto
    {
        [JsonPropertyName("id")]
        public required string Id { get; init; }

        [JsonPropertyName("market")]
        public required string Market { get; init; }

        [JsonPropertyName("section")]
        public required string Section { get; init; }

        [JsonPropertyName("label")]
        public required string Label { get; init; }

        [JsonPropertyName("detail")]
        public required string Detail { get; init; }

        [JsonPropertyName("category")]
        public required string Category { get; init; }

        [JsonPropertyName("builderKey")]
        public required string BuilderKey { get; init; }

        [JsonPropertyName("code")]
        public required string Code { get; init; }

        [JsonPropertyName("aliases")]
        public required List<string> Aliases { get; init; }

        [JsonPropertyName("available")]
        public required bool Available { get; init; }

        [JsonPropertyName("prerequisite")]
        public required string Prerequisite { get; init; }

        [JsonPropertyName("selection")]
        public required SelectionDto Selection { get; init; }

        [JsonPropertyName("dynamicDate")]
        public required bool DynamicDate { get; init; }

        [JsonPropertyName("dynamicNxtSession")]
        public required bool DynamicNxtSession { get; init; }
    }

    private sealed class SelectionDto
    {
        [JsonPropertyName("groupCode")]
        public required string GroupCode { get; init; }

        [JsonPropertyName("subject")]
        public required string Subject { get; init; }

        [JsonPropertyName("graphicType")]
        public required string GraphicType { get; init; }

        [JsonPropertyName("subtype")]
        public required string Subtype { get; init; }

        [JsonPropertyName("dataCode")]
        public required string DataCode { get; init; }
    }
}
