using System.Collections.ObjectModel;
using System.Globalization;
using System.Text;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyThemeSort
{
    InputOrder,
    GainRateDescending,
    GainRateAscending
}

public enum LegacyThemeValueKind
{
    Current,
    Expected
}

public sealed record LegacyThemeSortDefinition(
    string Id,
    string Label,
    LegacyThemeSort Value);

public sealed record LegacyThemeActionState(
    string ActionId,
    bool IsAvailable,
    string? UnavailableReason);

public sealed class LegacyThemeActionDefinition
{
    internal LegacyThemeActionDefinition(
        string id,
        string label,
        string builderKey,
        string code,
        int pageSize,
        LegacyThemeValueKind valueKind)
    {
        Id = id;
        Label = label;
        BuilderKey = builderKey;
        Code = code;
        Aliases = Array.AsReadOnly([code]);
        PageSize = pageSize;
        ValueKind = valueKind;
    }

    public string Id { get; }

    public string Label { get; }

    public string BuilderKey { get; }

    public string Code { get; }

    public IReadOnlyList<string> Aliases { get; }

    public int PageSize { get; }

    public LegacyThemeValueKind ValueKind { get; }
}

public sealed class LegacyThemeSearchRow
{
    internal LegacyThemeSearchRow(
        string resultId,
        ThemeSelectionIdentity identity,
        DataSourceKind source)
    {
        ResultId = resultId;
        Identity = identity;
        Market = identity.Market;
        Session = identity.Session;
        DisplayTitle = LegacyThemeWorkflow.DisplayTitle(identity);
        Source = source;
    }

    public string ResultId { get; }

    public ThemeMarket Market { get; }

    public ThemeSession Session { get; }

    public string DisplayTitle { get; }

    public DataSourceKind Source { get; }

    internal ThemeSelectionIdentity Identity { get; }
}

public sealed class LegacyThemePreviewRow
{
    internal LegacyThemePreviewRow(ThemeItemPreview item)
    {
        InputIndex = item.InputIndex;
        ItemName = item.ItemName;
        ItemCode = item.ItemCode;
    }

    public int InputIndex { get; }

    public string ItemName { get; }

    internal string ItemCode { get; }
}

public sealed record LegacySelectedThemeSummary(
    ThemeMarket Market,
    ThemeSession Session,
    string DisplayTitle,
    int PreviewItemCount,
    bool PreviewIsTruncated);

public sealed class LegacyThemeWorkflowSnapshot
{
    internal LegacyThemeWorkflowSnapshot(
        object authority,
        long revision,
        string query,
        ThemeSession nxtSession,
        LegacyThemeSort sort,
        IEnumerable<LegacyThemeSearchRow> searchResults,
        bool searchIsTruncated,
        string? selectedResultId,
        ThemeSelectionIdentity? selectedIdentity,
        IEnumerable<LegacyThemePreviewRow> previewItems,
        bool previewIsTruncated)
    {
        Authority = authority;
        Revision = revision;
        Query = query;
        NxtSession = nxtSession;
        Sort = sort;
        SearchResults = ReadOnly(searchResults);
        SearchIsTruncated = searchIsTruncated;
        SelectedResultId = selectedResultId;
        SelectedIdentity = selectedIdentity;
        PreviewItems = ReadOnly(previewItems);
        PreviewIsTruncated = previewIsTruncated;
        SelectedTheme = selectedIdentity is null
            ? null
            : new LegacySelectedThemeSummary(
                selectedIdentity.Market,
                selectedIdentity.Session,
                LegacyThemeWorkflow.DisplayTitle(selectedIdentity),
                PreviewItems.Count,
                previewIsTruncated);
        ActionStates = LegacyThemeWorkflow.CreateActionStates(
            selectedIdentity,
            PreviewItems.Count);
    }

    public long Revision { get; }

    public string Query { get; }

    public ThemeSession NxtSession { get; }

    public LegacyThemeSort Sort { get; }

    public string SortId => LegacyThemeWorkflow.SortId(Sort);

    public IReadOnlyList<LegacyThemeSearchRow> SearchResults { get; }

    public bool SearchIsTruncated { get; }

    public string? SelectedResultId { get; }

    public LegacySelectedThemeSummary? SelectedTheme { get; }

    public IReadOnlyList<LegacyThemePreviewRow> PreviewItems { get; }

    public bool PreviewIsTruncated { get; }

    public IReadOnlyList<LegacyThemeActionState> ActionStates { get; }

    internal object Authority { get; }

    internal ThemeSelectionIdentity? SelectedIdentity { get; }

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

public sealed record LegacyThemePlaylistSelection(
    string GroupCode,
    string Subject,
    string GraphicType,
    string Subtype,
    string DataCode,
    string? ExactSubject = null);

public sealed record LegacyThemePagePlan(
    int PreviewItemCount,
    int AccessibleItemCount,
    int PageSize,
    int PageCount,
    int MaximumPageCount,
    bool IsTruncated,
    string InitialPageText);

public sealed record LegacyThemePlaylistDraft(
    string ActionId,
    string BuilderKey,
    string Code,
    IReadOnlyList<string> Aliases,
    string Title,
    string PlaylistGraphicType,
    string PlaylistSubtype,
    string Detail,
    string Category,
    string Market,
    LegacyThemePlaylistSelection Selection,
    LegacyThemeValueKind ValueKind,
    LegacyThemeSort Sort,
    int PageSize,
    LegacyThemePagePlan PagePlan);

public sealed class LegacyThemeWorkflowDataException : Exception
{
    public LegacyThemeWorkflowDataException(string message)
        : base(message)
    {
    }
}

/// <summary>
/// C#-authoritative migration of the read-only UC4 theme selector. Search and preview data
/// come only from <see cref="IThemeSelectionService"/>; Web callers retain opaque result and
/// action ids and never provide scene codes, resolver groups, theme identities, or PageN data.
/// </summary>
public sealed class LegacyThemeWorkflow
{
    public const int MaximumSearchResults = LegacyThemeSelectionService.MaximumResults;
    public const int MaximumPreviewItems = 240;
    public const int MaximumPageCount = 20;

    private static readonly IReadOnlyList<LegacyThemeActionDefinition> RegisteredActions =
        CreateActions();
    private static readonly IReadOnlyList<LegacyThemeSortDefinition> RegisteredSorts =
        Array.AsReadOnly<LegacyThemeSortDefinition>(
        [
            new("INPUT_ORDER", "입력순", LegacyThemeSort.InputOrder),
            new("GAIN_DESC", "상승률순", LegacyThemeSort.GainRateDescending),
            new("GAIN_ASC", "하락률순", LegacyThemeSort.GainRateAscending)
        ]);
    private static readonly IReadOnlyDictionary<string, LegacyThemeActionDefinition> ActionsById =
        new ReadOnlyDictionary<string, LegacyThemeActionDefinition>(
            RegisteredActions.ToDictionary(action => action.Id, StringComparer.Ordinal));

    private readonly IThemeSelectionService _selectionService;
    private readonly Func<DateTimeOffset> _localNow;
    private readonly object _authority = new();

    static LegacyThemeWorkflow()
    {
        ValidateActions(RegisteredActions);
    }

    public LegacyThemeWorkflow(
        IThemeSelectionService selectionService,
        Func<DateTimeOffset>? localNow = null)
    {
        _selectionService = selectionService ??
            throw new ArgumentNullException(nameof(selectionService));
        _localNow = localNow ?? (() => DateTimeOffset.Now);
    }

    public static IReadOnlyList<LegacyThemeActionDefinition> Actions => RegisteredActions;

    public static IReadOnlyList<LegacyThemeSortDefinition> Sorts => RegisteredSorts;

    public ThemeSession CurrentNxtSession =>
        _localNow().Hour <= 12 ? ThemeSession.PreMarket : ThemeSession.AfterMarket;

    public LegacyThemeWorkflowSnapshot CreateInitial(
        ThemeSession nxtSession = ThemeSession.PreMarket)
    {
        RequireNxtSession(nxtSession);
        return NewSnapshot(
            revision: 0,
            query: string.Empty,
            nxtSession,
            LegacyThemeSort.InputOrder,
            [],
            searchIsTruncated: false,
            selectedResultId: null,
            selectedIdentity: null,
            previewItems: [],
            previewIsTruncated: false);
    }

    public LegacyThemeWorkflowSnapshot ResetForTabActivation(
        LegacyThemeWorkflowSnapshot snapshot,
        ThemeSession nxtSession = ThemeSession.PreMarket)
    {
        RequireSnapshot(snapshot);
        RequireNxtSession(nxtSession);
        return NewSnapshot(
            checked(snapshot.Revision + 1),
            query: string.Empty,
            nxtSession,
            LegacyThemeSort.InputOrder,
            [],
            searchIsTruncated: false,
            selectedResultId: null,
            selectedIdentity: null,
            previewItems: [],
            previewIsTruncated: false);
    }

    public async Task<LegacyThemeWorkflowSnapshot> SearchAsync(
        LegacyThemeWorkflowSnapshot snapshot,
        string query,
        ThemeSession nxtSession,
        CancellationToken cancellationToken = default)
    {
        RequireSnapshot(snapshot);
        var normalizedQuery = NormalizeQuery(query);
        RequireNxtSession(nxtSession);
        cancellationToken.ThrowIfCancellationRequested();

        var result = await _selectionService.SearchAsync(
            normalizedQuery,
            nxtSession,
            MaximumSearchResults,
            cancellationToken).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();
        var rows = ValidateSearchResult(result, normalizedQuery, nxtSession);
        var retainedSelection = snapshot.SelectedIdentity is null
            ? null
            : rows.SingleOrDefault(row => row.Identity == snapshot.SelectedIdentity);
        return NewSnapshot(
            checked(snapshot.Revision + 1),
            normalizedQuery,
            nxtSession,
            snapshot.Sort,
            rows,
            result.IsTruncated,
            retainedSelection?.ResultId,
            retainedSelection?.Identity,
            retainedSelection is null ? [] : snapshot.PreviewItems,
            retainedSelection is not null && snapshot.PreviewIsTruncated);
    }

    public async Task<LegacyThemeWorkflowSnapshot> SelectAsync(
        LegacyThemeWorkflowSnapshot snapshot,
        string resultId,
        CancellationToken cancellationToken = default)
    {
        RequireSnapshot(snapshot);
        ArgumentException.ThrowIfNullOrWhiteSpace(resultId);
        var selected = snapshot.SearchResults.FirstOrDefault(row =>
            string.Equals(row.ResultId, resultId, StringComparison.Ordinal))
            ?? throw new KeyNotFoundException($"Unknown theme result id '{resultId}'.");
        cancellationToken.ThrowIfCancellationRequested();

        var preview = await _selectionService.PreviewAsync(
            selected.Identity,
            MaximumPreviewItems,
            cancellationToken).ConfigureAwait(false);
        cancellationToken.ThrowIfCancellationRequested();
        var previewRows = ValidatePreviewResult(preview, selected.Identity);
        return NewSnapshot(
            checked(snapshot.Revision + 1),
            snapshot.Query,
            snapshot.NxtSession,
            snapshot.Sort,
            snapshot.SearchResults,
            snapshot.SearchIsTruncated,
            selected.ResultId,
            selected.Identity,
            previewRows,
            preview.IsTruncated);
    }

    public LegacyThemeWorkflowSnapshot RemoveSelectedResult(
        LegacyThemeWorkflowSnapshot snapshot)
    {
        RequireSnapshot(snapshot);
        if (snapshot.SelectedResultId is null)
        {
            return snapshot;
        }

        return NewSnapshot(
            checked(snapshot.Revision + 1),
            snapshot.Query,
            snapshot.NxtSession,
            snapshot.Sort,
            snapshot.SearchResults.Where(row => !string.Equals(
                row.ResultId,
                snapshot.SelectedResultId,
                StringComparison.Ordinal)),
            snapshot.SearchIsTruncated,
            selectedResultId: null,
            selectedIdentity: null,
            previewItems: [],
            previewIsTruncated: false);
    }

    public LegacyThemeWorkflowSnapshot SelectSort(
        LegacyThemeWorkflowSnapshot snapshot,
        string sortId)
    {
        RequireSnapshot(snapshot);
        var sort = ParseSort(sortId);
        return NewSnapshot(
            checked(snapshot.Revision + 1),
            snapshot.Query,
            snapshot.NxtSession,
            sort,
            snapshot.SearchResults,
            snapshot.SearchIsTruncated,
            snapshot.SelectedResultId,
            snapshot.SelectedIdentity,
            snapshot.PreviewItems,
            snapshot.PreviewIsTruncated);
    }

    /// <summary>
    /// Materializes from a trusted snapshot and a closed action id only. All K3D-facing values,
    /// theme identity fields, sort values, and PageN calculations are rebuilt in this method.
    /// </summary>
    public LegacyThemePlaylistDraft MaterializePlaylistDraft(
        LegacyThemeWorkflowSnapshot snapshot,
        string actionId)
    {
        RequireSnapshot(snapshot);
        ArgumentException.ThrowIfNullOrWhiteSpace(actionId);
        var action = ActionsById.TryGetValue(actionId, out var found)
            ? found
            : throw new KeyNotFoundException($"Unknown theme action id '{actionId}'.");
        var identity = snapshot.SelectedIdentity
            ?? throw new InvalidOperationException("A theme must be selected and previewed first.");
        if (snapshot.PreviewItems.Count == 0)
        {
            throw new InvalidOperationException("An empty theme preview cannot be added to the playlist.");
        }

        if (action.ValueKind == LegacyThemeValueKind.Expected &&
            identity.Market == ThemeMarket.Nxt)
        {
            throw new InvalidOperationException("NXT는 데이터가 없습니다.");
        }

        var sortId = SortId(snapshot.Sort);
        var valueKind = ValueKindId(action.ValueKind);
        var displayTitle = DisplayTitle(identity);
        var playlistGraphicType = PlaylistGraphicType(action);
        var playlistSubtype =
            $"테마-{ValueKindLabel(action.ValueKind)}({SortLabel(snapshot.Sort)})";
        var selection = identity.Market == ThemeMarket.Krx
            ? new LegacyThemePlaylistSelection(
                "PAGED_THEME",
                identity.ThemeTitle,
                sortId,
                valueKind,
                identity.ThemeCode,
                identity.ThemeTitle)
            : new LegacyThemePlaylistSelection(
                "PAGED_NXT_THEME",
                displayTitle,
                SessionId(CurrentNxtSession),
                sortId,
                identity.ThemeCode,
                identity.ThemeTitle);

        var maximumItems = checked(action.PageSize * MaximumPageCount);
        var accessibleItems = Math.Min(snapshot.PreviewItems.Count, maximumItems);
        var pageCount = (accessibleItems + action.PageSize - 1) / action.PageSize;
        var pagePlan = new LegacyThemePagePlan(
            snapshot.PreviewItems.Count,
            accessibleItems,
            action.PageSize,
            pageCount,
            MaximumPageCount,
            snapshot.PreviewIsTruncated || snapshot.PreviewItems.Count > maximumItems,
            $"1/{pageCount.ToString(CultureInfo.InvariantCulture)}");

        return new LegacyThemePlaylistDraft(
            action.Id,
            action.BuilderKey,
            action.Code,
            action.Aliases,
            displayTitle,
            playlistGraphicType,
            playlistSubtype,
            playlistSubtype,
            "plate",
            "theme",
            selection,
            action.ValueKind,
            snapshot.Sort,
            action.PageSize,
            pagePlan);
    }

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

    public LegacyThemeActionState GetActionState(
        LegacyThemeWorkflowSnapshot snapshot,
        string actionId)
    {
        RequireSnapshot(snapshot);
        ArgumentException.ThrowIfNullOrWhiteSpace(actionId);
        return snapshot.ActionStates.FirstOrDefault(state =>
            string.Equals(state.ActionId, actionId, StringComparison.Ordinal))
            ?? throw new KeyNotFoundException($"Unknown theme action id '{actionId}'.");
    }

    public static string SortId(LegacyThemeSort sort) => sort switch
    {
        LegacyThemeSort.InputOrder => "INPUT_ORDER",
        LegacyThemeSort.GainRateDescending => "GAIN_DESC",
        LegacyThemeSort.GainRateAscending => "GAIN_ASC",
        _ => throw new ArgumentOutOfRangeException(nameof(sort), sort, "Unknown theme sort.")
    };

    public static string SortLabel(LegacyThemeSort sort) => sort switch
    {
        LegacyThemeSort.InputOrder => "입력순",
        LegacyThemeSort.GainRateDescending => "상승률순",
        LegacyThemeSort.GainRateAscending => "하락률순",
        _ => throw new ArgumentOutOfRangeException(nameof(sort), sort, "Unknown theme sort.")
    };

    private LegacyThemeWorkflowSnapshot NewSnapshot(
        long revision,
        string query,
        ThemeSession nxtSession,
        LegacyThemeSort sort,
        IEnumerable<LegacyThemeSearchRow> searchResults,
        bool searchIsTruncated,
        string? selectedResultId,
        ThemeSelectionIdentity? selectedIdentity,
        IEnumerable<LegacyThemePreviewRow> previewItems,
        bool previewIsTruncated) =>
        new(
            _authority,
            revision,
            query,
            nxtSession,
            sort,
            searchResults,
            searchIsTruncated,
            selectedResultId,
            selectedIdentity,
            previewItems,
            previewIsTruncated);

    private void RequireSnapshot(LegacyThemeWorkflowSnapshot snapshot)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        if (!ReferenceEquals(snapshot.Authority, _authority))
        {
            throw new ArgumentException(
                "The theme snapshot belongs to another workflow instance.",
                nameof(snapshot));
        }
    }

    private static IReadOnlyList<LegacyThemeSearchRow> ValidateSearchResult(
        ThemeSearchResult result,
        string query,
        ThemeSession nxtSession)
    {
        if (result is null || result.Items is null ||
            !string.Equals(result.Query, query, StringComparison.Ordinal) ||
            result.NxtSession != nxtSession)
        {
            throw InvalidData("search envelope");
        }

        var serviceItems = result.Items.ToArray();
        if (serviceItems.Length > MaximumSearchResults ||
            (result.IsTruncated &&
             serviceItems.Length != MaximumSearchResults &&
             !HangulSearchMatcher.IsInitialConsonantQuery(query)))
        {
            throw InvalidData("search envelope");
        }

        var codes = new Dictionary<ThemeMarket, HashSet<string>>
        {
            [ThemeMarket.Krx] = new(StringComparer.Ordinal),
            [ThemeMarket.Nxt] = new(StringComparer.Ordinal)
        };
        var sawNxt = false;
        foreach (var item in serviceItems)
        {
            if (item is null)
            {
                throw InvalidData("search item");
            }

            ValidateIdentity(item.Identity, nxtSession);
            var expectedSource = item.Identity.Market == ThemeMarket.Krx
                ? DataSourceKind.Oracle
                : DataSourceKind.MariaDb;
            if (item.Source != expectedSource ||
                !codes[item.Identity.Market].Add(item.Identity.ThemeCode))
            {
                throw InvalidData("search identity or provider");
            }

            // UC4 appended the Oracle list and then the MariaDB list. Preserve
            // each provider's returned order; do not sort rows by title or code.
            if (item.Identity.Market == ThemeMarket.Nxt)
            {
                sawNxt = true;
            }
            else if (sawNxt)
            {
                throw InvalidData("search provider order");
            }
        }

        var rows = new LegacyThemeSearchRow[serviceItems.Length];
        for (var index = 0; index < serviceItems.Length; index++)
        {
            rows[index] = new LegacyThemeSearchRow(
                $"theme-result-{index + 1:000}",
                serviceItems[index].Identity,
                serviceItems[index].Source);
        }

        return Array.AsReadOnly(rows);
    }

    private static IReadOnlyList<LegacyThemePreviewRow> ValidatePreviewResult(
        ThemePreviewResult result,
        ThemeSelectionIdentity identity)
    {
        if (result is null || result.Items is null || result.Identity != identity)
        {
            throw InvalidData("preview envelope");
        }

        var serviceItems = result.Items.ToArray();
        if (serviceItems.Length > MaximumPreviewItems ||
            result.IsTruncated && serviceItems.Length != MaximumPreviewItems)
        {
            throw InvalidData("preview envelope");
        }

        var indexes = new HashSet<int>();
        var previousIndex = -1;
        var rows = new LegacyThemePreviewRow[serviceItems.Length];
        for (var index = 0; index < serviceItems.Length; index++)
        {
            var item = serviceItems[index]
                ?? throw InvalidData("preview item");
            if (item.InputIndex is < 0 or > 9_999 || item.InputIndex <= previousIndex ||
                !indexes.Add(item.InputIndex) ||
                !IsItemCodeForMarket(item.ItemCode, identity.Market) ||
                !IsCanonicalText(item.ItemName, 200))
            {
                throw InvalidData("preview item identity");
            }

            previousIndex = item.InputIndex;
            rows[index] = new LegacyThemePreviewRow(item);
        }

        return Array.AsReadOnly(rows);
    }

    private static void ValidateIdentity(
        ThemeSelectionIdentity identity,
        ThemeSession requestedNxtSession)
    {
        if (identity is null || !Enum.IsDefined(identity.Market) ||
            !Enum.IsDefined(identity.Session) ||
            identity.Market == ThemeMarket.Krx &&
            identity.Session != ThemeSession.NotApplicable ||
            identity.Market == ThemeMarket.Nxt && identity.Session != requestedNxtSession ||
            !IsThemeCode(identity.ThemeCode) ||
            !IsStoredThemeTitle(identity.ThemeTitle, 128))
        {
            throw InvalidData("theme identity");
        }
    }

    internal static string DisplayTitle(ThemeSelectionIdentity identity) =>
        identity.Market == ThemeMarket.Nxt &&
        !identity.ThemeTitle.EndsWith("(NXT)", StringComparison.Ordinal)
            ? $"{identity.ThemeTitle}(NXT)"
            : identity.ThemeTitle;

    private static string NormalizeQuery(string query)
    {
        ArgumentNullException.ThrowIfNull(query);
        if (!IsSafeText(query))
        {
            throw new ArgumentException("The theme search query is invalid.", nameof(query));
        }

        var normalized = query.Trim();
        if (normalized.Length > LegacyThemeSelectionService.MaximumQueryLength ||
            !IsSafeText(normalized))
        {
            throw new ArgumentException(
                $"A theme search query must contain at most {LegacyThemeSelectionService.MaximumQueryLength} visible characters.",
                nameof(query));
        }

        return normalized;
    }

    private static LegacyThemeSort ParseSort(string sortId)
    {
        ArgumentNullException.ThrowIfNull(sortId);
        return sortId switch
        {
            "INPUT_ORDER" => LegacyThemeSort.InputOrder,
            "GAIN_DESC" => LegacyThemeSort.GainRateDescending,
            "GAIN_ASC" => LegacyThemeSort.GainRateAscending,
            _ => throw new ArgumentOutOfRangeException(
                nameof(sortId),
                sortId,
                "Unknown theme sort id.")
        };
    }

    private static string SessionId(ThemeSession session) => session switch
    {
        ThemeSession.PreMarket => "PRE_MARKET",
        ThemeSession.AfterMarket => "AFTER_MARKET",
        _ => throw new ArgumentOutOfRangeException(
            nameof(session),
            session,
            "NXT requires an explicit session.")
    };

    private static string ValueKindId(LegacyThemeValueKind valueKind) => valueKind switch
    {
        LegacyThemeValueKind.Current => "CURRENT",
        LegacyThemeValueKind.Expected => "EXPECTED",
        _ => throw new ArgumentOutOfRangeException(
            nameof(valueKind),
            valueKind,
            "Unknown theme value kind.")
    };

    private static string ValueKindLabel(LegacyThemeValueKind valueKind) => valueKind switch
    {
        LegacyThemeValueKind.Current => "현재가",
        LegacyThemeValueKind.Expected => "예상체결가",
        _ => throw new ArgumentOutOfRangeException(
            nameof(valueKind),
            valueKind,
            "Unknown theme value kind.")
    };

    private static string PlaylistGraphicType(LegacyThemeActionDefinition action) =>
        action.Id switch
        {
            "five-row-current" or "five-row-expected" => "5단 표그래프",
            "six-row-current" => "6종목 현재가",
            "twelve-row-current" => "12종목 현재가",
            _ => throw new ArgumentOutOfRangeException(
                nameof(action),
                action.Id,
                "Unknown theme action.")
        };

    private static void RequireNxtSession(ThemeSession nxtSession)
    {
        if (nxtSession is not (ThemeSession.PreMarket or ThemeSession.AfterMarket))
        {
            throw new ArgumentOutOfRangeException(
                nameof(nxtSession),
                nxtSession,
                "An explicit NXT session is required.");
        }
    }

    private static bool IsThemeCode(string value) =>
        value is not null && value.Length is >= 3 and <= 8 && value.All(char.IsAsciiDigit);

    private static bool IsItemCodeForMarket(string value, ThemeMarket market)
    {
        if (value is null || value.Length is < 2 or > 13 || !value.All(char.IsAsciiLetterOrDigit))
        {
            return false;
        }

        return market switch
        {
            ThemeMarket.Krx => value[0] is 'P' or 'D',
            ThemeMarket.Nxt => value[0] is 'X' or 'T',
            _ => false
        };
    }

    private static bool IsCanonicalText(string value, int maximumLength) =>
        value is not null && value.Length is >= 1 &&
        value.Length <= maximumLength &&
        string.Equals(value, value.Trim(), StringComparison.Ordinal) &&
        IsSafeText(value);

    private static bool IsStoredThemeTitle(string value, int maximumLength) =>
        value is not null && value.Length is >= 1 &&
        value.Length <= maximumLength &&
        !string.IsNullOrWhiteSpace(value) &&
        IsSafeText(value);

    private static bool IsSafeText(string value)
    {
        foreach (var rune in value.EnumerateRunes())
        {
            var category = Rune.GetUnicodeCategory(rune);
            if (Rune.IsControl(rune) || rune.Value == '\uFFFD' ||
                category is UnicodeCategory.Format or UnicodeCategory.Surrogate or
                    UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator)
            {
                return false;
            }
        }

        return true;
    }

    private static IReadOnlyList<LegacyThemeActionDefinition> CreateActions() =>
        Array.AsReadOnly(
        [
            new LegacyThemeActionDefinition(
                "five-row-current",
                "5단 표그래프 · 현재가",
                "s5074",
                "5074",
                5,
                LegacyThemeValueKind.Current),
            new LegacyThemeActionDefinition(
                "five-row-expected",
                "5단 표그래프 · 예상체결가",
                "s5074",
                "5074",
                5,
                LegacyThemeValueKind.Expected),
            new LegacyThemeActionDefinition(
                "six-row-current",
                "6종목 현재가",
                "s5077",
                "5077",
                6,
                LegacyThemeValueKind.Current),
            new LegacyThemeActionDefinition(
                "twelve-row-current",
                "12종목 현재가",
                "s5088",
                "5088",
                12,
                LegacyThemeValueKind.Current)
        ]);

    internal static IReadOnlyList<LegacyThemeActionState> CreateActionStates(
        ThemeSelectionIdentity? selectedIdentity,
        int previewItemCount)
    {
        var states = new LegacyThemeActionState[RegisteredActions.Count];
        for (var index = 0; index < RegisteredActions.Count; index++)
        {
            var action = RegisteredActions[index];
            var reason = selectedIdentity is null
                ? "theme-not-selected"
                : previewItemCount == 0
                    ? "theme-preview-is-empty"
                    : null;
            states[index] = new LegacyThemeActionState(
                action.Id,
                reason is null,
                reason);
        }

        return Array.AsReadOnly(states);
    }

    private static void ValidateActions(
        IReadOnlyCollection<LegacyThemeActionDefinition> actions)
    {
        if (actions.Count != 4 ||
            actions.Select(action => action.Id).Distinct(StringComparer.Ordinal).Count() != 4)
        {
            throw new InvalidOperationException("The UC4 action catalog must contain four unique actions.");
        }

        foreach (var action in actions)
        {
            var scene = LegacySceneCatalog.Definitions.Single(definition =>
                string.Equals(definition.BuilderKey, action.BuilderKey, StringComparison.Ordinal));
            if (!scene.CutAliases.Contains(action.Code, StringComparer.Ordinal) ||
                action.Aliases.Count != 1 ||
                !string.Equals(action.Aliases[0], action.Code, StringComparison.Ordinal) ||
                (int)scene.PageSize != action.PageSize)
            {
                throw new InvalidOperationException(
                    $"Theme action '{action.Id}' does not match the Core scene catalog.");
            }
        }
    }

    private static LegacyThemeWorkflowDataException InvalidData(string detail) =>
        new($"The injected theme selection service returned invalid {detail} data.");
}
