using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW;

public sealed partial class MainWindow
{
    private const string NamedPlaylistCorrelationQuarantineMessage =
        "A named-playlist write result could not be confirmed. Restart the app before another named-playlist write.";

    private async Task HandleNamedPlaylistListRequestAsync(JsonElement payload)
    {
        var requestId = GetString(payload, "requestId") ?? string.Empty;
        if (!HasOnlyProperties(payload, "requestId", "maximumResults") ||
            !TryGetSafeToken(
                payload,
                "requestId",
                MaximumPlayoutRequestIdLength,
                out requestId))
        {
            PostNamedPlaylistInvalidRequest(requestId, "list");
            return;
        }

        var maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions;
        if (payload.TryGetProperty("maximumResults", out var maximumElement) &&
            (maximumElement.ValueKind != JsonValueKind.Number ||
             !maximumElement.TryGetInt32(out maximumResults) ||
             maximumResults is < 1 or > LegacyNamedPlaylistPersistenceService.MaximumDefinitions))
        {
            PostNamedPlaylistInvalidRequest(requestId, "list");
            return;
        }

        var service = _namedPlaylistPersistenceService;
        if (service is null)
        {
            PostNamedPlaylistUnavailable(requestId, "list");
            return;
        }

        await ExecuteNamedPlaylistReadAsync(
            requestId,
            "list",
            async cancellationToken =>
            {
                var result = await service.ListAsync(maximumResults, cancellationToken);
                var suggestedProgramCode = await service.SuggestNextProgramCodeAsync(
                    cancellationToken);
                cancellationToken.ThrowIfCancellationRequested();
                PostMessage("named-playlist-list-results", new
                {
                    requestId,
                    result.RetrievedAt,
                    totalRowCount = result.Items.Count,
                    truncated = result.IsTruncated,
                    suggestedProgramCode,
                    canMutate = service.CanMutate && !IsNamedPlaylistWriteQuarantined(),
                    writeQuarantined = IsNamedPlaylistWriteQuarantined(),
                    definitions = result.Items.Select(item => new
                    {
                        item.ProgramCode,
                        item.Title
                    })
                });
            });
    }

    private async Task HandleNamedPlaylistLoadRequestAsync(JsonElement payload)
    {
        var requestId = GetString(payload, "requestId") ?? string.Empty;
        var programCode = GetString(payload, "programCode") ?? string.Empty;
        if (!HasOnlyProperties(payload, "requestId", "programCode") ||
            !TryGetSafeToken(
                payload,
                "requestId",
                MaximumPlayoutRequestIdLength,
                out requestId) ||
            !TryGetNamedPlaylistProgramCode(payload, "programCode", out programCode))
        {
            PostNamedPlaylistInvalidRequest(requestId, "load", programCode);
            return;
        }

        var service = _namedPlaylistPersistenceService;
        if (service is null)
        {
            PostNamedPlaylistUnavailable(requestId, "load", programCode);
            return;
        }

        await ExecuteNamedPlaylistReadAsync(
            requestId,
            "load",
            async cancellationToken =>
            {
                var result = await service.LoadAsync(programCode, cancellationToken);
                cancellationToken.ThrowIfCancellationRequested();
                PostMessage("named-playlist-load-results", new
                {
                    requestId,
                    definition = new
                    {
                        result.Definition.ProgramCode,
                        result.Definition.Title
                    },
                    result.RetrievedAt,
                    totalRowCount = result.Items.Count,
                    canMutate = service.CanMutate && !IsNamedPlaylistWriteQuarantined(),
                    writeQuarantined = IsNamedPlaylistWriteQuarantined(),
                    rows = result.Items.Select(item => new
                    {
                        item.ItemIndex,
                        enabled = item.IsEnabled,
                        item.Selection.GroupCode,
                        item.Selection.Subject,
                        item.Selection.GraphicType,
                        item.Selection.Subtype,
                        pageText = item.Page?.ToString() ?? string.Empty,
                        item.Selection.DataCode
                    })
                });
                await PostNamedComparisonRestoreAsync(
                    requestId,
                    result.Items,
                    cancellationToken);
                await PostNamedForeignStockRestoreAsync(
                    requestId,
                    result.Items,
                    cancellationToken);
                await PostNamedForeignIndexCandleRestoreAsync(
                    requestId,
                    result.Items,
                    cancellationToken);
                await PostNamedKrxThemeRestoreAsync(
                    requestId,
                    result.Items,
                    cancellationToken);
                // Keep all named restore batches correlated to this load. NXT
                // theme verification runs last because it uses MariaDB after
                // the Oracle-backed identity batches and performs a bounded
                // item preview per title.
                await PostNamedNxtThemeRestoreAsync(
                    requestId,
                    result.Items,
                    cancellationToken);
            });
    }

    private async Task HandleNamedPlaylistCreateRequestAsync(JsonElement payload)
    {
        var requestId = GetString(payload, "requestId") ?? string.Empty;
        var programCode = GetString(payload, "programCode") ?? string.Empty;
        if (!HasOnlyProperties(payload, "requestId", "programCode", "title") ||
            !TryGetSafeToken(
                payload,
                "requestId",
                MaximumPlayoutRequestIdLength,
                out requestId) ||
            !TryGetNamedPlaylistProgramCode(payload, "programCode", out programCode) ||
            !TryGetNamedPlaylistText(
                payload,
                "title",
                maximumLength: 128,
                allowEmpty: false,
                allowCaret: true,
                out var title))
        {
            PostNamedPlaylistInvalidRequest(requestId, "create", programCode);
            return;
        }

        await ExecuteNamedPlaylistWriteAsync(
            requestId,
            "create",
            programCode,
            (service, cancellationToken) => service.CreateDefinitionAsync(
                programCode,
                title,
                cancellationToken));
    }

    private async Task HandleNamedPlaylistReplaceRequestAsync(JsonElement payload)
    {
        var requestId = GetString(payload, "requestId") ?? string.Empty;
        var programCode = GetString(payload, "programCode") ?? string.Empty;
        if (!HasOnlyProperties(payload, "requestId", "programCode", "items") ||
            !TryGetSafeToken(
                payload,
                "requestId",
                MaximumPlayoutRequestIdLength,
                out requestId) ||
            !TryGetNamedPlaylistProgramCode(payload, "programCode", out programCode) ||
            !TryParseNamedPlaylistItems(payload, out var items))
        {
            PostNamedPlaylistInvalidRequest(requestId, "replace", programCode);
            return;
        }

        await ExecuteNamedPlaylistWriteAsync(
            requestId,
            "replace",
            programCode,
            (service, cancellationToken) => service.ReplaceItemsAsync(
                programCode,
                items,
                cancellationToken));
    }

    private async Task HandleNamedPlaylistDeleteRequestAsync(JsonElement payload)
    {
        var requestId = GetString(payload, "requestId") ?? string.Empty;
        var programCode = GetString(payload, "programCode") ?? string.Empty;
        if (!HasOnlyProperties(payload, "requestId", "programCode") ||
            !TryGetSafeToken(
                payload,
                "requestId",
                MaximumPlayoutRequestIdLength,
                out requestId) ||
            !TryGetNamedPlaylistProgramCode(payload, "programCode", out programCode))
        {
            PostNamedPlaylistInvalidRequest(requestId, "delete", programCode);
            return;
        }

        await ExecuteNamedPlaylistWriteAsync(
            requestId,
            "delete",
            programCode,
            (service, cancellationToken) => service.DeleteAsync(
                programCode,
                cancellationToken));
    }

    private async Task ExecuteNamedPlaylistReadAsync(
        string requestId,
        string operation,
        Func<CancellationToken, Task> operationBody)
    {
        var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource(
            _lifetimeCancellation.Token);
        var requestToken = requestCancellation.Token;
        var browserGeneration = Volatile.Read(ref _namedPlaylistBrowserGeneration);
        var previousCancellation = Interlocked.Exchange(
            ref _namedPlaylistReadCancellation,
            requestCancellation);
        CancelRequest(previousCancellation);

        var databaseActivityEntered = false;
        try
        {
            if (Volatile.Read(ref _playoutCommandInFlight) != 0)
            {
                requestCancellation.Cancel();
                PostNamedPlaylistReadCancellationIfCurrent(
                    requestId,
                    operation,
                    requestCancellation);
                return;
            }

            await _databaseActivityGate.WaitAsync(requestToken);
            databaseActivityEntered = true;
            requestToken.ThrowIfCancellationRequested();
            if (Volatile.Read(ref _playoutCommandInFlight) != 0)
            {
                requestCancellation.Cancel();
                PostNamedPlaylistReadCancellationIfCurrent(
                    requestId,
                    operation,
                    requestCancellation);
                return;
            }

            if (browserGeneration != Volatile.Read(ref _namedPlaylistBrowserGeneration))
            {
                requestCancellation.Cancel();
                return;
            }

            await operationBody(requestToken);
            requestToken.ThrowIfCancellationRequested();
        }
        catch (OperationCanceledException) when (requestToken.IsCancellationRequested)
        {
            PostNamedPlaylistReadCancellationIfCurrent(
                requestId,
                operation,
                requestCancellation);
        }
        catch (DatabaseOperationException) when (requestToken.IsCancellationRequested)
        {
            PostNamedPlaylistReadCancellationIfCurrent(
                requestId,
                operation,
                requestCancellation);
        }
        catch (Exception) when (requestToken.IsCancellationRequested)
        {
            PostNamedPlaylistReadCancellationIfCurrent(
                requestId,
                operation,
                requestCancellation);
        }
        catch (NamedPlaylistNotFoundException)
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "NOT_FOUND",
                "The named playlist does not exist.",
                retryable: false,
                outcomeUnknown: false);
        }
        catch (NamedPlaylistDataException)
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "INVALID_DATA",
                "The stored named-playlist data is invalid.",
                retryable: false,
                outcomeUnknown: false);
            _ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
        }
        catch (ArgumentException)
        {
            PostNamedPlaylistInvalidRequest(requestId, operation);
        }
        catch (DatabaseOperationException exception)
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "DATABASE_ERROR",
                exception.Message,
                retryable: true,
                outcomeUnknown: false);
            _ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
        }
        catch
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "DATABASE_ERROR",
                "The named-playlist database request failed.",
                retryable: true,
                outcomeUnknown: false);
            _ = PostDatabaseStatusAsync(_lifetimeCancellation.Token);
        }
        finally
        {
            if (databaseActivityEntered)
            {
                _databaseActivityGate.Release();
            }

            Interlocked.CompareExchange(
                ref _namedPlaylistReadCancellation,
                null,
                requestCancellation);
            requestCancellation.Dispose();
        }
    }

    private async Task ExecuteNamedPlaylistWriteAsync(
        string requestId,
        string operation,
        string programCode,
        Func<INamedPlaylistPersistenceService, CancellationToken, Task> operationBody)
    {
        var service = _namedPlaylistPersistenceService;
        if (service is null || !service.CanMutate)
        {
            PostNamedPlaylistUnavailable(requestId, operation, programCode);
            return;
        }

        if (IsNamedPlaylistWriteQuarantined())
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "OUTCOME_UNKNOWN",
                Volatile.Read(ref _namedPlaylistWriteQuarantineMessage) ??
                    NamedPlaylistCorrelationQuarantineMessage,
                retryable: false,
                outcomeUnknown: true,
                programCode);
            return;
        }

        if (!await _namedPlaylistWriteGate.WaitAsync(0))
        {
            PostNamedPlaylistError(
                requestId,
                operation,
                "WRITE_BUSY",
                "Another named-playlist write is already running.",
                retryable: false,
                outcomeUnknown: false,
                programCode);
            return;
        }

        var requestCancellation = CancellationTokenSource.CreateLinkedTokenSource(
            _lifetimeCancellation.Token);
        var requestToken = requestCancellation.Token;
        var browserGeneration = Volatile.Read(ref _namedPlaylistBrowserGeneration);
        Volatile.Write(ref _namedPlaylistWriteCancellation, requestCancellation);
        var databaseActivityEntered = false;
        var mutationStarted = false;
        try
        {
            if (Volatile.Read(ref _playoutCommandInFlight) != 0)
            {
                requestCancellation.Cancel();
                PostNamedPlaylistWriteCancellationIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    requestCancellation);
                return;
            }

            await _databaseActivityGate.WaitAsync(requestToken);
            databaseActivityEntered = true;
            requestToken.ThrowIfCancellationRequested();
            if (Volatile.Read(ref _playoutCommandInFlight) != 0)
            {
                requestCancellation.Cancel();
                PostNamedPlaylistWriteCancellationIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    requestCancellation);
                return;
            }

            if (IsNamedPlaylistWriteQuarantined())
            {
                PostNamedPlaylistError(
                    requestId,
                    operation,
                    "OUTCOME_UNKNOWN",
                    Volatile.Read(ref _namedPlaylistWriteQuarantineMessage) ??
                        NamedPlaylistCorrelationQuarantineMessage,
                    retryable: false,
                    outcomeUnknown: true,
                    programCode);
                return;
            }

            mutationStarted = true;
            Interlocked.Exchange(ref _namedPlaylistWriteInFlight, 1);
            if (browserGeneration != Volatile.Read(ref _namedPlaylistBrowserGeneration))
            {
                mutationStarted = false;
                requestCancellation.Cancel();
                return;
            }

            await operationBody(service, requestToken);
            if (browserGeneration != Volatile.Read(ref _namedPlaylistBrowserGeneration) ||
                !ReferenceEquals(
                    Volatile.Read(ref _namedPlaylistWriteCancellation),
                    requestCancellation))
            {
                LatchNamedPlaylistWriteQuarantine(
                    NamedPlaylistCorrelationQuarantineMessage);
                return;
            }

            PostMessage("named-playlist-mutation-results", new
            {
                requestId,
                operation,
                programCode,
                completedAt = DateTimeOffset.UtcNow,
                writeQuarantined = IsNamedPlaylistWriteQuarantined()
            });
        }
        catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
        {
            // App shutdown owns the cancellation and no Web document can receive it.
        }
        catch (NamedPlaylistMutationException exception)
        {
            if (exception.OutcomeUnknown)
            {
                LatchNamedPlaylistWriteQuarantine(exception.Message);
                PostNamedPlaylistWriteErrorIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    "OUTCOME_UNKNOWN",
                    NamedPlaylistCorrelationQuarantineMessage,
                    outcomeUnknown: true,
                    requestCancellation);
            }
            else if (requestToken.IsCancellationRequested)
            {
                PostNamedPlaylistWriteCancellationIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    requestCancellation);
            }
            else
            {
                PostNamedPlaylistWriteErrorIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    "WRITE_FAILED",
                    "The named-playlist write rolled back and was not retried.",
                    outcomeUnknown: false,
                    requestCancellation);
            }
        }
        catch (NamedPlaylistNotFoundException)
        {
            PostNamedPlaylistWriteErrorIfCurrent(
                requestId,
                operation,
                programCode,
                "NOT_FOUND",
                "The named playlist does not exist.",
                outcomeUnknown: false,
                requestCancellation);
        }
        catch (ArgumentException)
        {
            PostNamedPlaylistWriteErrorIfCurrent(
                requestId,
                operation,
                programCode,
                "INVALID_REQUEST",
                "The named-playlist write request is invalid.",
                outcomeUnknown: false,
                requestCancellation);
        }
        catch (OperationCanceledException)
        {
            if (mutationStarted)
            {
                LatchNamedPlaylistWriteQuarantine(
                    NamedPlaylistCorrelationQuarantineMessage);
                PostNamedPlaylistWriteErrorIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    "OUTCOME_UNKNOWN",
                    NamedPlaylistCorrelationQuarantineMessage,
                    outcomeUnknown: true,
                    requestCancellation);
            }
            else
            {
                PostNamedPlaylistWriteCancellationIfCurrent(
                    requestId,
                    operation,
                    programCode,
                    requestCancellation);
            }
        }
        catch
        {
            if (mutationStarted)
            {
                LatchNamedPlaylistWriteQuarantine(
                    NamedPlaylistCorrelationQuarantineMessage);
            }

            PostNamedPlaylistWriteErrorIfCurrent(
                requestId,
                operation,
                programCode,
                mutationStarted ? "OUTCOME_UNKNOWN" : "WRITE_FAILED",
                mutationStarted
                    ? NamedPlaylistCorrelationQuarantineMessage
                    : "The named-playlist write failed before a transaction began.",
                outcomeUnknown: mutationStarted,
                requestCancellation);
        }
        finally
        {
            Interlocked.Exchange(ref _namedPlaylistWriteInFlight, 0);
            if (databaseActivityEntered)
            {
                _databaseActivityGate.Release();
            }

            Interlocked.CompareExchange(
                ref _namedPlaylistWriteCancellation,
                null,
                requestCancellation);
            requestCancellation.Dispose();
            _namedPlaylistWriteGate.Release();
        }
    }

    private void PostNamedPlaylistReadCancellationIfCurrent(
        string requestId,
        string operation,
        CancellationTokenSource requestCancellation)
    {
        if (_lifetimeCancellation.IsCancellationRequested ||
            !ReferenceEquals(
                Volatile.Read(ref _namedPlaylistReadCancellation),
                requestCancellation))
        {
            return;
        }

        PostNamedPlaylistError(
            requestId,
            operation,
            "PLAYOUT_PRIORITY",
            "The named-playlist read was canceled because playout has database priority.",
            retryable: true,
            outcomeUnknown: false);
    }

    private void PostNamedPlaylistWriteCancellationIfCurrent(
        string requestId,
        string operation,
        string programCode,
        CancellationTokenSource requestCancellation)
    {
        if (_lifetimeCancellation.IsCancellationRequested ||
            !ReferenceEquals(
                Volatile.Read(ref _namedPlaylistWriteCancellation),
                requestCancellation))
        {
            return;
        }

        PostNamedPlaylistError(
            requestId,
            operation,
            "PLAYOUT_PRIORITY",
            "The named-playlist write stopped before execution because playout has database priority.",
            retryable: false,
            outcomeUnknown: false,
            programCode);
    }

    private void PostNamedPlaylistWriteErrorIfCurrent(
        string requestId,
        string operation,
        string programCode,
        string code,
        string message,
        bool outcomeUnknown,
        CancellationTokenSource requestCancellation)
    {
        if (_lifetimeCancellation.IsCancellationRequested ||
            !ReferenceEquals(
                Volatile.Read(ref _namedPlaylistWriteCancellation),
                requestCancellation))
        {
            return;
        }

        PostNamedPlaylistError(
            requestId,
            operation,
            code,
            message,
            retryable: false,
            outcomeUnknown,
            programCode);
    }

    private void PostNamedPlaylistInvalidRequest(
        string requestId,
        string operation,
        string programCode = "") =>
        PostNamedPlaylistError(
            requestId,
            operation,
            "INVALID_REQUEST",
            "The named-playlist request is invalid.",
            retryable: false,
            outcomeUnknown: false,
            programCode);

    private void PostNamedPlaylistUnavailable(
        string requestId,
        string operation,
        string programCode = "") =>
        PostNamedPlaylistError(
            requestId,
            operation,
            "DATABASE_UNAVAILABLE",
            _databaseInitializationError ?? "The named-playlist database is unavailable.",
            retryable: false,
            outcomeUnknown: false,
            programCode);

    private void PostNamedPlaylistError(
        string requestId,
        string operation,
        string code,
        string message,
        bool retryable,
        bool outcomeUnknown,
        string programCode = "")
    {
        PostMessage("named-playlist-error", new
        {
            requestId,
            operation,
            programCode,
            code,
            message,
            retryable = retryable && !outcomeUnknown,
            outcomeUnknown,
            writeQuarantined = IsNamedPlaylistWriteQuarantined()
        });
    }

    private bool IsNamedPlaylistWriteQuarantined() =>
        Volatile.Read(ref _namedPlaylistWriteQuarantined) != 0 ||
        IsOperatorMutationQuarantined();

    private void LatchNamedPlaylistWriteQuarantine(string message)
    {
        LatchOperatorMutationQuarantine(message);
        if (Interlocked.CompareExchange(ref _namedPlaylistWriteQuarantined, 1, 0) == 0)
        {
            Volatile.Write(
                ref _namedPlaylistWriteQuarantineMessage,
                string.IsNullOrWhiteSpace(message)
                    ? NamedPlaylistCorrelationQuarantineMessage
                    : message);
        }
    }

    private void InvalidateNamedPlaylistBrowserRequests()
    {
        Interlocked.Increment(ref _namedPlaylistBrowserGeneration);
        if (Volatile.Read(ref _namedPlaylistWriteInFlight) != 0)
        {
            LatchNamedPlaylistWriteQuarantine(
                NamedPlaylistCorrelationQuarantineMessage);
        }

        var readCancellation = Interlocked.Exchange(
            ref _namedPlaylistReadCancellation,
            null);
        CancelRequest(readCancellation);
        var writeCancellation = Interlocked.Exchange(
            ref _namedPlaylistWriteCancellation,
            null);
        CancelRequest(writeCancellation);
    }

    private static bool TryParseNamedPlaylistItems(
        JsonElement payload,
        out IReadOnlyList<NamedPlaylistStoredItem> items)
    {
        items = Array.Empty<NamedPlaylistStoredItem>();
        if (!payload.TryGetProperty("items", out var itemsElement) ||
            itemsElement.ValueKind != JsonValueKind.Array ||
            itemsElement.GetArrayLength() > LegacyNamedPlaylistPersistenceService.MaximumItems)
        {
            return false;
        }

        var parsed = new List<NamedPlaylistStoredItem>(itemsElement.GetArrayLength());
        var expectedIndex = 0;
        foreach (var itemElement in itemsElement.EnumerateArray())
        {
            if (itemElement.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(
                    itemElement,
                    "itemIndex",
                    "enabled",
                    "groupCode",
                    "subject",
                    "graphicType",
                    "subtype",
                    "pageText",
                    "dataCode") ||
                !itemElement.TryGetProperty("itemIndex", out var indexElement) ||
                indexElement.ValueKind != JsonValueKind.Number ||
                !indexElement.TryGetInt32(out var itemIndex) ||
                itemIndex != expectedIndex ||
                !itemElement.TryGetProperty("enabled", out var enabledElement) ||
                enabledElement.ValueKind is not (JsonValueKind.True or JsonValueKind.False) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "groupCode",
                    256,
                    allowEmpty: true,
                    allowCaret: false,
                    out var groupCode) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "subject",
                    4000,
                    allowEmpty: true,
                    allowCaret: false,
                    out var subject) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "graphicType",
                    256,
                    allowEmpty: true,
                    allowCaret: false,
                    out var graphicType) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "subtype",
                    256,
                    allowEmpty: true,
                    allowCaret: false,
                    out var subtype) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "pageText",
                    9,
                    allowEmpty: true,
                    allowCaret: false,
                    out var pageText) ||
                !TryGetNamedPlaylistText(
                    itemElement,
                    "dataCode",
                    256,
                    allowEmpty: true,
                    allowCaret: false,
                    out var dataCode) ||
                !TryParseNamedPlaylistPage(pageText, out var page))
            {
                return false;
            }

            parsed.Add(new NamedPlaylistStoredItem(
                itemIndex,
                enabledElement.GetBoolean(),
                new LegacySceneSelection(
                    groupCode,
                    subject,
                    graphicType,
                    subtype,
                    dataCode),
                page));
            expectedIndex++;
        }

        items = parsed.AsReadOnly();
        return true;
    }

    private static bool TryGetNamedPlaylistProgramCode(
        JsonElement payload,
        string propertyName,
        out string value)
    {
        value = GetString(payload, propertyName) ?? string.Empty;
        return value.Length == 8 && value.All(char.IsAsciiDigit);
    }

    private static bool TryGetNamedPlaylistText(
        JsonElement payload,
        string propertyName,
        int maximumLength,
        bool allowEmpty,
        bool allowCaret,
        out string value)
    {
        value = GetString(payload, propertyName) ?? string.Empty;
        if (!payload.TryGetProperty(propertyName, out var element) ||
            element.ValueKind != JsonValueKind.String ||
            (!allowEmpty && value.Length == 0) ||
            value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            (!allowCaret && value.Contains('^')))
        {
            return false;
        }

        return !value.Any(static character =>
            char.IsControl(character) ||
            char.IsSurrogate(character) ||
            character == '\uFFFD');
    }

    private static bool TryParseNamedPlaylistPage(
        string value,
        out NamedPlaylistPageState? page)
    {
        page = null;
        if (value.Length == 0)
        {
            return true;
        }

        var separator = value.IndexOf('/');
        if (separator < 1 || separator != value.LastIndexOf('/') ||
            !TryParseUnsignedPageNumber(value.AsSpan(0, separator), out var currentPage) ||
            !TryParseUnsignedPageNumber(value.AsSpan(separator + 1), out var totalPages))
        {
            return false;
        }

        try
        {
            page = new NamedPlaylistPageState(currentPage, totalPages);
            return string.Equals(page.ToString(), value, StringComparison.Ordinal);
        }
        catch (ArgumentOutOfRangeException)
        {
            return false;
        }
    }

    private static bool TryParseUnsignedPageNumber(
        ReadOnlySpan<char> value,
        out int number)
    {
        number = 0;
        if (value.Length is < 1 or > 4)
        {
            return false;
        }

        foreach (var character in value)
        {
            if (!char.IsAsciiDigit(character))
            {
                return false;
            }

            number = checked((number * 10) + (character - '0'));
        }

        return true;
    }
}
