#nullable enable

using System.Data;
using System.Globalization;
using System.Text.RegularExpressions;

namespace MMoneyCoderSharp.Data;

public sealed record ThemeCatalogItem(
    int InputIndex,
    string ItemCode,
    string ItemName);

public sealed record ThemeCatalogDefinition(
    ThemeMarket Market,
    string ThemeCode,
    string ThemeTitle,
    IReadOnlyList<ThemeCatalogItem> Items);

public sealed record ThemeCatalogStoredResult(
    ThemeSelectionIdentity Identity,
    DateTimeOffset RetrievedAt,
    IReadOnlyList<ThemeCatalogItem> Items,
    bool IsTruncated);

public interface IThemeCatalogPersistenceService
{
    bool CanMutate { get; }

    Task<ThemeCatalogStoredResult> ReadStoredAsync(
        ThemeSelectionIdentity identity,
        CancellationToken cancellationToken = default);

    Task<bool> ThemeNameExistsAsync(
        ThemeMarket market,
        string themeTitle,
        CancellationToken cancellationToken = default);

    Task<string> SuggestNextCodeAsync(
        ThemeMarket market,
        CancellationToken cancellationToken = default);

    Task<OperatorCatalogMutationReceipt> CreateAsync(
        ThemeCatalogDefinition definition,
        CancellationToken cancellationToken = default);

    Task<OperatorCatalogMutationReceipt> ReplaceAsync(
        ThemeSelectionIdentity expectedIdentity,
        string newTitle,
        IReadOnlyList<ThemeCatalogItem> items,
        CancellationToken cancellationToken = default);

    Task<OperatorCatalogMutationReceipt> DeleteAsync(
        ThemeSelectionIdentity expectedIdentity,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Atomic migration of ThemeA/UC4 theme CRUD. KRX stays on Oracle and NXT stays
/// on MariaDB. Suggested codes are advisory; create uses serializable,
/// conditional insertion so a stale suggestion cannot overwrite another theme.
/// </summary>
public sealed partial class LegacyThemeCatalogPersistenceService : IThemeCatalogPersistenceService
{
    public const int MaximumItems = 240;

    internal const string NextKrxCodeQueryName = "THEME_NEXT_CODE_KRX";
    internal const string NextNxtCodeQueryName = "THEME_NEXT_CODE_NXT";
    internal const string NameExistsKrxQueryName = "THEME_NAME_EXISTS_KRX";
    internal const string NameExistsNxtQueryName = "THEME_NAME_EXISTS_NXT";

    private const int MaximumThemeTitleLength = 128;
    private const int MaximumItemNameLength = 200;

    private const string NextKrxCodeSql = """
        SELECT CASE
                   WHEN NVL(MAX(
                       CASE
                           WHEN REGEXP_LIKE(TRIM(SB_CODE), '^[0-9]{3,8}$', 'c')
                           THEN TO_NUMBER(TRIM(SB_CODE))
                       END), 0) < 99999999
                   THEN LPAD(
                       TO_CHAR(NVL(MAX(
                           CASE
                               WHEN REGEXP_LIKE(TRIM(SB_CODE), '^[0-9]{3,8}$', 'c')
                               THEN TO_NUMBER(TRIM(SB_CODE))
                           END), 0) + 1),
                       8,
                       '0')
                   ELSE 'EXHAUSTED'
               END THEME_CODE
        FROM SB_LIST
        """;

    private const string NextNxtCodeSql = """
        SELECT CASE
                   WHEN COALESCE(MAX(
                       CASE
                           WHEN TRIM(SB_CODE) REGEXP BINARY '^[0-9]{3,8}$'
                           THEN CAST(TRIM(SB_CODE) AS UNSIGNED)
                       END), 0) < 99999999
                   THEN LPAD(
                       CAST(COALESCE(MAX(
                           CASE
                               WHEN TRIM(SB_CODE) REGEXP BINARY '^[0-9]{3,8}$'
                               THEN CAST(TRIM(SB_CODE) AS UNSIGNED)
                           END), 0) + 1 AS CHAR),
                       8,
                       '0')
                   ELSE 'EXHAUSTED'
               END THEME_CODE
        FROM SB_LIST
        """;

    private const string NameExistsKrxSql = """
        SELECT COUNT(*) THEME_COUNT
        FROM SB_LIST s
        WHERE s.SB_MARKET = 'KRX'
          AND s.SB_TITLE = :theme_title
        """;

    private const string NameExistsNxtSql = """
        SELECT COUNT(*) THEME_COUNT
        FROM SB_LIST s
        WHERE s.SB_MARKET = 'NXT'
          AND s.SB_TITLE = @theme_title
        """;

    private readonly IDataQueryExecutor _queryExecutor;
    private readonly IOperatorCatalogMutationExecutor? _mutationExecutor;

    public LegacyThemeCatalogPersistenceService(IDataQueryExecutor queryExecutor)
        : this(queryExecutor, null)
    {
    }

    public LegacyThemeCatalogPersistenceService(
        IDataQueryExecutor queryExecutor,
        IOperatorCatalogMutationExecutor? mutationExecutor)
    {
        _queryExecutor = queryExecutor ?? throw new ArgumentNullException(nameof(queryExecutor));
        _mutationExecutor = mutationExecutor;
    }

    public bool CanMutate => _mutationExecutor is not null;

    public async Task<bool> ThemeNameExistsAsync(
        ThemeMarket market,
        string themeTitle,
        CancellationToken cancellationToken = default)
    {
        var canonicalTitle = ValidateThemeTitle(themeTitle, market, nameof(themeTitle));
        var profile = ThemeMutationProfiles.For(market);
        cancellationToken.ThrowIfCancellationRequested();
        var query = new DataQuerySpec(
            market == ThemeMarket.Krx ? NameExistsKrxSql : NameExistsNxtSql,
            [new DataQueryParameter("theme_title", canonicalTitle, DbType.String)]);
        query.ValidateFor(profile.Source);
        var table = await _queryExecutor.ExecuteAsync(
            profile.Source,
            market == ThemeMarket.Krx ? NameExistsKrxQueryName : NameExistsNxtQueryName,
            query,
            cancellationToken).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();

        if (table is null ||
            table.Columns.Count != 1 ||
            !string.Equals(table.Columns[0].ColumnName, "THEME_COUNT", StringComparison.Ordinal) ||
            table.Rows.Count != 1 ||
            !TryReadNonNegativeCount(table.Rows[0][0], out var count))
        {
            throw new ThemeSelectionDataException(
                "Theme name availability data has an invalid schema or value.");
        }

        return count > 0;
    }

    public async Task<string> SuggestNextCodeAsync(
        ThemeMarket market,
        CancellationToken cancellationToken = default)
    {
        var profile = ThemeMutationProfiles.For(market);
        cancellationToken.ThrowIfCancellationRequested();
        var query = new DataQuerySpec(
            market == ThemeMarket.Krx ? NextKrxCodeSql : NextNxtCodeSql);
        query.ValidateFor(profile.Source);
        var table = await _queryExecutor.ExecuteAsync(
            profile.Source,
            market == ThemeMarket.Krx ? NextKrxCodeQueryName : NextNxtCodeQueryName,
            query,
            cancellationToken).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();

        if (table is null ||
            table.Columns.Count != 1 ||
            !string.Equals(table.Columns[0].ColumnName, "THEME_CODE", StringComparison.Ordinal) ||
            table.Columns[0].DataType != typeof(string) ||
            table.Rows.Count != 1 ||
            table.Rows[0][0] is not string code ||
            !ThemeCodePattern().IsMatch(code))
        {
            throw new ThemeSelectionDataException(
                "Theme next-code data has an invalid schema, value, or exhausted code range.");
        }

        return code;
    }

    public Task<OperatorCatalogMutationReceipt> CreateAsync(
        ThemeCatalogDefinition definition,
        CancellationToken cancellationToken = default)
    {
        var canonical = ValidateDefinition(definition, nameof(definition));
        var profile = ThemeMutationProfiles.For(canonical.Market);
        var commands = new List<OperatorCatalogDbCommand>(canonical.Items.Count + 1)
        {
            profile.CreateThemeCommand(canonical.ThemeCode, canonical.ThemeTitle)
        };
        commands.AddRange(canonical.Items.Select(item =>
            profile.CreateItemCommand(canonical.ThemeCode, item)));

        return ExecuteAsync(
            new OperatorCatalogDbTransaction(
                Guid.NewGuid(),
                OperatorCatalogMutationKind.CreateTheme,
                profile.Source,
                canonical.ThemeCode,
                commands),
            cancellationToken);
    }

    public Task<OperatorCatalogMutationReceipt> ReplaceAsync(
        ThemeSelectionIdentity expectedIdentity,
        string newTitle,
        IReadOnlyList<ThemeCatalogItem> items,
        CancellationToken cancellationToken = default)
    {
        var expected = ValidateIdentity(expectedIdentity, nameof(expectedIdentity));
        var title = string.Equals(newTitle, expected.ThemeTitle, StringComparison.Ordinal)
            ? ValidateStoredThemeTitle(newTitle, nameof(newTitle))
            : ValidateThemeTitle(newTitle, expected.Market, nameof(newTitle));
        var canonicalItems = ValidateItems(
            items,
            expected.Market,
            allowLegacyCrossMarketPrefix: true,
            nameof(items));
        var profile = ThemeMutationProfiles.For(expected.Market);
        var commands = new List<OperatorCatalogDbCommand>(canonicalItems.Count + 2)
        {
            profile.CreateUpdateCommand(expected, title),
            profile.CreateDeleteItemsCommand(expected.ThemeCode)
        };
        commands.AddRange(canonicalItems.Select(item =>
            profile.CreateItemCommand(expected.ThemeCode, item)));

        return ExecuteAsync(
            new OperatorCatalogDbTransaction(
                Guid.NewGuid(),
                OperatorCatalogMutationKind.ReplaceTheme,
                profile.Source,
                expected.ThemeCode,
                commands),
            cancellationToken);
    }

    public Task<OperatorCatalogMutationReceipt> DeleteAsync(
        ThemeSelectionIdentity expectedIdentity,
        CancellationToken cancellationToken = default)
    {
        var expected = ValidateIdentity(expectedIdentity, nameof(expectedIdentity));
        var profile = ThemeMutationProfiles.For(expected.Market);
        var transaction = new OperatorCatalogDbTransaction(
            Guid.NewGuid(),
            OperatorCatalogMutationKind.DeleteTheme,
            profile.Source,
            expected.ThemeCode,
            [
                profile.CreateDeleteItemsCommand(expected.ThemeCode),
                profile.CreateDeleteThemeCommand(expected)
            ]);
        return ExecuteAsync(transaction, cancellationToken);
    }

    private async Task<OperatorCatalogMutationReceipt> ExecuteAsync(
        OperatorCatalogDbTransaction transaction,
        CancellationToken cancellationToken)
    {
        var executor = _mutationExecutor ?? throw new InvalidOperationException(
            "Theme catalog persistence is configured for read-only access.");
        cancellationToken.ThrowIfCancellationRequested();

        OperatorCatalogDbTransactionResult result;
        try
        {
            result = await executor.ExecuteTransactionAsync(transaction, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperatorCatalogMutationException)
        {
            throw;
        }
        catch (Exception exception)
        {
            throw new OperatorCatalogMutationException(
                $"The {transaction.Kind} transaction outcome is unknown; do not retry automatically.",
                outcomeUnknown: true,
                exception);
        }

        if (result is null ||
            result.OperationId != transaction.OperationId ||
            result.AffectedRows is null ||
            result.AffectedRows.Count != transaction.Commands.Count ||
            result.AffectedRows.Any(static count => count < 0))
        {
            throw new OperatorCatalogMutationException(
                $"The {transaction.Kind} transaction returned an ambiguous commit receipt; do not retry automatically.",
                outcomeUnknown: true);
        }

        for (var index = 0; index < transaction.Commands.Count; index++)
        {
            if (transaction.Commands[index].AffectedRowsRule ==
                    OperatorCatalogAffectedRowsRule.ExactlyOne &&
                result.AffectedRows[index] != 1)
            {
                throw new OperatorCatalogMutationException(
                    $"The {transaction.Kind} transaction returned an ambiguous affected-row receipt; do not retry automatically.",
                    outcomeUnknown: true);
            }
        }

        return new OperatorCatalogMutationReceipt(
            result.OperationId,
            transaction.Kind,
            result.CommittedAt);
    }

    private static ThemeCatalogDefinition ValidateDefinition(
        ThemeCatalogDefinition definition,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(definition);
        if (!Enum.IsDefined(definition.Market))
        {
            throw new ArgumentOutOfRangeException(parameterName, "The theme market is invalid.");
        }

        var code = ValidateNewThemeCode(definition.ThemeCode, parameterName);
        var title = ValidateThemeTitle(definition.ThemeTitle, definition.Market, parameterName);
        var items = ValidateItems(
            definition.Items,
            definition.Market,
            allowLegacyCrossMarketPrefix: false,
            parameterName);
        return new ThemeCatalogDefinition(definition.Market, code, title, items);
    }

    private static ThemeSelectionIdentity ValidateIdentity(
        ThemeSelectionIdentity identity,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(identity);
        if (!Enum.IsDefined(identity.Market) || !Enum.IsDefined(identity.Session) ||
            identity.Market == ThemeMarket.Krx && identity.Session != ThemeSession.NotApplicable ||
            identity.Market == ThemeMarket.Nxt &&
            identity.Session is not (ThemeSession.PreMarket or ThemeSession.AfterMarket))
        {
            throw new ArgumentException("The theme identity is invalid.", parameterName);
        }

        return identity with
        {
            ThemeCode = ValidateExistingThemeCode(identity.ThemeCode, parameterName),
            ThemeTitle = ValidateStoredThemeTitle(identity.ThemeTitle, parameterName)
        };
    }

    private static IReadOnlyList<ThemeCatalogItem> ValidateItems(
        IReadOnlyList<ThemeCatalogItem> items,
        ThemeMarket market,
        bool allowLegacyCrossMarketPrefix,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(items);
        if (items.Count > MaximumItems)
        {
            throw new ArgumentOutOfRangeException(
                parameterName,
                $"A theme can contain at most {MaximumItems} items.");
        }

        var canonical = new ThemeCatalogItem[items.Count];
        for (var index = 0; index < items.Count; index++)
        {
            var item = items[index] ?? throw new ArgumentException(
                "A theme item is missing.",
                parameterName);
            if (item.InputIndex != index)
            {
                throw new ArgumentException(
                    "Theme item indexes must be contiguous and zero based.",
                    parameterName);
            }

            var code = ValidateCanonicalText(item.ItemCode, 13, parameterName);
            var matchesExpectedMarket = market == ThemeMarket.Krx
                ? KrxItemCodePattern().IsMatch(code)
                : NxtItemCodePattern().IsMatch(code);
            var matchesClosedLegacyPrefix =
                KrxItemCodePattern().IsMatch(code) || NxtItemCodePattern().IsMatch(code);
            if (!matchesExpectedMarket &&
                (!allowLegacyCrossMarketPrefix || !matchesClosedLegacyPrefix))
            {
                throw new ArgumentException(
                    "A theme item code does not match its market.",
                    parameterName);
            }

            var name = allowLegacyCrossMarketPrefix
                ? ValidateStoredText(item.ItemName, MaximumItemNameLength, parameterName)
                : ValidateCanonicalText(item.ItemName, MaximumItemNameLength, parameterName);
            canonical[index] = new ThemeCatalogItem(index, code, name);
        }

        return Array.AsReadOnly(canonical);
    }

    private static string ValidateNewThemeCode(string value, string parameterName)
    {
        var code = ValidateCanonicalText(value, 8, parameterName);
        if (!ThemeCodePattern().IsMatch(code))
        {
            throw new ArgumentException("A theme code must contain eight digits.", parameterName);
        }

        return code;
    }

    private static string ValidateExistingThemeCode(string value, string parameterName)
    {
        var code = ValidateCanonicalText(value, 8, parameterName);
        if (!ExistingThemeCodePattern().IsMatch(code))
        {
            throw new ArgumentException(
                "An existing theme code must contain three through eight digits.",
                parameterName);
        }

        return code;
    }

    private static string ValidateThemeTitle(
        string value,
        ThemeMarket market,
        string parameterName)
    {
        var title = ValidateCanonicalText(value, MaximumThemeTitleLength, parameterName);
        if (market == ThemeMarket.Nxt &&
            title.Contains("(NXT)", StringComparison.OrdinalIgnoreCase))
        {
            throw new ArgumentException(
                "The NXT display suffix is not part of the stored theme title.",
                parameterName);
        }

        return title;
    }

    private static string ValidateStoredThemeTitle(string value, string parameterName)
        => ValidateStoredText(value, MaximumThemeTitleLength, parameterName);

    private static string ValidateStoredText(
        string value,
        int maximumLength,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(value);
        if (value.Length < 1 || value.Length > maximumLength ||
            string.IsNullOrWhiteSpace(value) ||
            !IsSafeText(value))
        {
            throw new ArgumentException("A stored theme value is invalid.", parameterName);
        }

        return value;
    }

    private static string ValidateCanonicalText(
        string value,
        int maximumLength,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(value);
        if (value.Length is < 1 || value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            !IsSafeText(value))
        {
            throw new ArgumentException("A theme catalog value is invalid.", parameterName);
        }

        return value;
    }

    private static bool TryReadNonNegativeCount(object? value, out decimal count)
    {
        count = 0;
        if (value is null or DBNull || value is bool || value is char || value is string)
        {
            return false;
        }

        try
        {
            count = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
            return count >= 0 && decimal.Truncate(count) == count;
        }
        catch (Exception exception) when (
            exception is InvalidCastException or FormatException or OverflowException)
        {
            count = 0;
            return false;
        }
    }

    private static bool IsSafeText(string value)
    {
        foreach (var character in value)
        {
            var category = CharUnicodeInfo.GetUnicodeCategory(character);
            if (char.IsControl(character) || char.IsSurrogate(character) ||
                character == '\uFFFD' ||
                category is UnicodeCategory.Format or
                    UnicodeCategory.LineSeparator or
                    UnicodeCategory.ParagraphSeparator)
            {
                return false;
            }
        }

        return true;
    }

    [GeneratedRegex("^[0-9]{8}$", RegexOptions.CultureInvariant)]
    private static partial Regex ThemeCodePattern();

    [GeneratedRegex("^[0-9]{3,8}$", RegexOptions.CultureInvariant)]
    private static partial Regex ExistingThemeCodePattern();

    [GeneratedRegex("^[PD][A-Za-z0-9]{1,12}$", RegexOptions.CultureInvariant)]
    private static partial Regex KrxItemCodePattern();

    [GeneratedRegex("^[XT][A-Za-z0-9]{1,12}$", RegexOptions.CultureInvariant)]
    private static partial Regex NxtItemCodePattern();
}

internal sealed record ThemeMutationProfile(
    ThemeMarket Market,
    DataSourceKind Source,
    string DatabaseMarket,
    char Marker,
    string CreateSql,
    string UpdateSql,
    string DeleteItemsSql,
    string InsertItemSql,
    string DeleteSql)
{
    internal OperatorCatalogDbCommand CreateThemeCommand(string code, string title) =>
        Command(
            OperatorCatalogDbCommandKind.InsertTheme,
            CreateSql,
            OperatorCatalogAffectedRowsRule.ExactlyOne,
            String("theme_code", code),
            String("theme_title", title),
            String("conflict_code", code),
            String("conflict_title", title));

    internal OperatorCatalogDbCommand CreateUpdateCommand(
        ThemeSelectionIdentity expected,
        string newTitle) =>
        Command(
            OperatorCatalogDbCommandKind.UpdateTheme,
            UpdateSql,
            OperatorCatalogAffectedRowsRule.ExactlyOne,
            String("new_title", newTitle),
            String("theme_code", expected.ThemeCode),
            String("expected_title", expected.ThemeTitle));

    internal OperatorCatalogDbCommand CreateDeleteItemsCommand(string code) =>
        Command(
            OperatorCatalogDbCommandKind.DeleteThemeItems,
            DeleteItemsSql,
            OperatorCatalogAffectedRowsRule.AnyNonNegative,
            String("theme_code", code));

    internal OperatorCatalogDbCommand CreateItemCommand(
        string code,
        ThemeCatalogItem item) =>
        Command(
            OperatorCatalogDbCommandKind.InsertThemeItem,
            InsertItemSql,
            OperatorCatalogAffectedRowsRule.ExactlyOne,
            String("theme_code", code),
            String("item_code", item.ItemCode),
            String("item_name", item.ItemName),
            new OperatorCatalogDbParameter("item_index", item.InputIndex, DbType.Int32));

    internal OperatorCatalogDbCommand CreateDeleteThemeCommand(ThemeSelectionIdentity expected) =>
        Command(
            OperatorCatalogDbCommandKind.DeleteTheme,
            DeleteSql,
            OperatorCatalogAffectedRowsRule.ExactlyOne,
            String("theme_code", expected.ThemeCode),
            String("expected_title", expected.ThemeTitle));

    private static OperatorCatalogDbParameter String(string name, string value) =>
        new(name, value, DbType.String);

    private static OperatorCatalogDbCommand Command(
        OperatorCatalogDbCommandKind kind,
        string sql,
        OperatorCatalogAffectedRowsRule rule,
        params OperatorCatalogDbParameter[] parameters) =>
        new(kind, sql, rule, parameters);
}

internal static class ThemeMutationProfiles
{
    private static readonly ThemeMutationProfile Krx = new(
        ThemeMarket.Krx,
        DataSourceKind.Oracle,
        "KRX",
        ':',
        """
        INSERT INTO SB_LIST(SB_CODE, SB_TITLE, SB_MARKET)
        SELECT :theme_code, :theme_title, 'KRX'
        FROM DUAL
        WHERE NOT EXISTS (
            SELECT 1 FROM SB_LIST WHERE SB_CODE = :conflict_code)
          AND NOT EXISTS (
            SELECT 1
            FROM SB_LIST
            WHERE SB_MARKET = 'KRX'
              AND SB_TITLE = :conflict_title)
        """,
        """
        UPDATE SB_LIST target
        SET SB_TITLE = :new_title
        WHERE target.SB_CODE = :theme_code
          AND target.SB_TITLE = :expected_title
          AND target.SB_MARKET = 'KRX'
        """,
        "DELETE FROM SB_ITEM WHERE SB_M_CODE = :theme_code",
        """
        INSERT INTO SB_ITEM(SB_M_CODE, SB_I_CODE, SB_I_NAME, SB_I_INDEX)
        VALUES (:theme_code, :item_code, :item_name, :item_index)
        """,
        """
        DELETE FROM SB_LIST
        WHERE SB_CODE = :theme_code
          AND SB_TITLE = :expected_title
          AND SB_MARKET = 'KRX'
        """);

    private static readonly ThemeMutationProfile Nxt = new(
        ThemeMarket.Nxt,
        DataSourceKind.MariaDb,
        "NXT",
        '@',
        """
        INSERT INTO SB_LIST(SB_CODE, SB_TITLE, SB_MARKET)
        SELECT @theme_code, @theme_title, 'NXT'
        WHERE NOT EXISTS (
            SELECT 1 FROM SB_LIST WHERE SB_CODE = @conflict_code)
          AND NOT EXISTS (
            SELECT 1
            FROM SB_LIST
            WHERE SB_MARKET = 'NXT'
              AND SB_TITLE = @conflict_title)
        """,
        """
        UPDATE SB_LIST target
        SET target.SB_TITLE = @new_title
        WHERE target.SB_CODE = @theme_code
          AND target.SB_TITLE = @expected_title
          AND target.SB_MARKET = 'NXT'
        """,
        "DELETE FROM SB_ITEM WHERE SB_M_CODE = @theme_code",
        """
        INSERT INTO SB_ITEM(SB_M_CODE, SB_I_CODE, SB_I_NAME, SB_I_INDEX)
        VALUES (@theme_code, @item_code, @item_name, @item_index)
        """,
        """
        DELETE FROM SB_LIST
        WHERE SB_CODE = @theme_code
          AND SB_TITLE = @expected_title
          AND SB_MARKET = 'NXT'
        """);

    internal static ThemeMutationProfile For(ThemeMarket market) => market switch
    {
        ThemeMarket.Krx => Krx,
        ThemeMarket.Nxt => Nxt,
        _ => throw new ArgumentOutOfRangeException(nameof(market), market, "Unsupported theme market.")
    };
}
