using System.Globalization;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyNamedPlaylistReadStatus
{
    Idle,
    Loading,
    Ready,
    Error
}

public enum LegacyNamedPlaylistFreshness
{
    None,
    Optimistic,
    Fresh
}

public enum LegacyNamedPlaylistMutationOperation
{
    Create,
    SaveCurrent,
    Delete
}

public enum LegacyNamedPlaylistMutationOutcome
{
    CommittedFresh,
    CommittedOptimistic,
    RolledBack,
    NotFound,
    Busy,
    ReadOnly,
    OutcomeUnknown,
    Quarantined
}

public sealed record LegacyNamedPlaylistDefinitionView(
    string DefinitionId,
    string Title,
    bool IsSelected);

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

public sealed record LegacyNamedPlaylistPageView(
    int CurrentPage,
    int TotalPages,
    string PageText,
    bool IsWithinPlayoutBounds);

public sealed record LegacyNamedPlaylistRowView(
    string RowId,
    int ItemIndex,
    bool IsEnabled,
    LegacyNamedPlaylistSelectionView Selection,
    LegacyNamedPlaylistPageView? Page);

/// <summary>
/// A title-only create request minted by one workflow instance. The internal eight-digit
/// DC_LIST identity is deliberately absent and cannot be supplied by Web callers.
/// </summary>
public sealed class LegacyNamedPlaylistProgramDraft
{
    internal LegacyNamedPlaylistProgramDraft(
        object authority,
        string draftId,
        string title)
    {
        Authority = authority;
        DraftId = draftId;
        Title = title;
    }

    public string DraftId { get; }

    public string Title { get; }

    internal object Authority { get; }
}

/// <summary>
/// One trusted native playlist row projected into the original seven-field shape without
/// exposing a caret-delimited LIST_TEXT value. Array order becomes the contiguous item index.
/// </summary>
public sealed class LegacyNamedPlaylistRowDraft
{
    internal LegacyNamedPlaylistRowDraft(
        object authority,
        string draftId,
        bool isEnabled,
        LegacySceneSelection selection,
        NamedPlaylistPageState? page)
    {
        Authority = authority;
        DraftId = draftId;
        IsEnabled = isEnabled;
        Selection = LegacyNamedPlaylistWorkflowController.ToView(selection);
        Page = LegacyNamedPlaylistWorkflowController.ToView(page);
        StoredSelection = selection;
        StoredPage = page;
    }

    public string DraftId { get; }

    public bool IsEnabled { get; }

    public LegacyNamedPlaylistSelectionView Selection { get; }

    public LegacyNamedPlaylistPageView? Page { get; }

    internal object Authority { get; }

    internal LegacySceneSelection StoredSelection { get; }

    internal NamedPlaylistPageState? StoredPage { get; }
}

public sealed class LegacyNamedPlaylistWorkflowSnapshot
{
    internal LegacyNamedPlaylistWorkflowSnapshot(
        long revision,
        LegacyNamedPlaylistReadStatus readStatus,
        IEnumerable<LegacyNamedPlaylistDefinitionView> definitions,
        bool isListTruncated,
        string? selectedDefinitionId,
        string? selectedTitle,
        IEnumerable<LegacyNamedPlaylistRowView> rows,
        LegacyNamedPlaylistFreshness listFreshness,
        LegacyNamedPlaylistFreshness documentFreshness,
        bool isWriteInProgress,
        bool isWriteQuarantined,
        bool persistenceCanMutate,
        bool hasSuggestedIdentity,
        LegacyNamedPlaylistMutationOutcome? lastMutationOutcome,
        string statusMessage,
        LegacyOperatorStatusKind statusKind)
    {
        Revision = revision;
        ReadStatus = readStatus;
        Definitions = Array.AsReadOnly(definitions.ToArray());
        IsListTruncated = isListTruncated;
        SelectedDefinitionId = selectedDefinitionId;
        SelectedTitle = selectedTitle;
        Rows = Array.AsReadOnly(rows.ToArray());
        ListFreshness = listFreshness;
        DocumentFreshness = documentFreshness;
        IsWriteInProgress = isWriteInProgress;
        IsWriteQuarantined = isWriteQuarantined;
        CanMutate = persistenceCanMutate && !isWriteQuarantined;
        CanCreate = CanMutate && !isWriteInProgress && hasSuggestedIdentity;
        CanLoad = selectedDefinitionId is not null && !isWriteInProgress;
        CanSave = CanMutate && CanLoad;
        CanDelete = CanMutate && CanLoad;
        LastMutationOutcome = lastMutationOutcome;
        StatusMessage = statusMessage;
        StatusKind = statusKind;
    }

    public long Revision { get; }

    public LegacyNamedPlaylistReadStatus ReadStatus { get; }

    public IReadOnlyList<LegacyNamedPlaylistDefinitionView> Definitions { get; }

    public bool IsListTruncated { get; }

    public string? SelectedDefinitionId { get; }

    public string? SelectedTitle { get; }

    public IReadOnlyList<LegacyNamedPlaylistRowView> Rows { get; }

    public LegacyNamedPlaylistFreshness ListFreshness { get; }

    public LegacyNamedPlaylistFreshness DocumentFreshness { get; }

    public bool IsWriteInProgress { get; }

    public bool IsWriteQuarantined { get; }

    public bool CanMutate { get; }

    public bool CanCreate { get; }

    public bool CanLoad { get; }

    public bool CanSave { get; }

    public bool CanDelete { get; }

    public LegacyNamedPlaylistMutationOutcome? LastMutationOutcome { get; }

    public string StatusMessage { get; }

    public LegacyOperatorStatusKind StatusKind { get; }
}

public sealed class LegacyNamedPlaylistMutationResult
{
    internal LegacyNamedPlaylistMutationResult(
        string mutationId,
        LegacyNamedPlaylistMutationOperation operation,
        LegacyNamedPlaylistMutationOutcome outcome,
        LegacyNamedPlaylistWorkflowSnapshot snapshot)
    {
        MutationId = mutationId;
        Operation = operation;
        Outcome = outcome;
        Snapshot = snapshot;
    }

    public string MutationId { get; }

    public LegacyNamedPlaylistMutationOperation Operation { get; }

    public LegacyNamedPlaylistMutationOutcome Outcome { get; }

    public bool AutomaticRetryAllowed => false;

    public bool IsCommitted => Outcome is
        LegacyNamedPlaylistMutationOutcome.CommittedFresh or
        LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;

    public LegacyNamedPlaylistWorkflowSnapshot Snapshot { get; }
}

/// <summary>
/// C# authority for the original modal PList/AList workflow. The injected
/// <see cref="INamedPlaylistPersistenceService"/> is the only read/write seam. A write is
/// attempted at most once, all rows are sent in one replace call, and an ambiguous result
/// permanently quarantines subsequent writes for this workflow instance.
/// </summary>
public sealed class LegacyNamedPlaylistWorkflowController
{
    public const int DefaultMaximumDefinitions =
        LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions;

    public const int MaximumDefinitions =
        LegacyNamedPlaylistPersistenceService.MaximumDefinitions;

    public const int MaximumItems = LegacyNamedPlaylistPersistenceService.MaximumItems;

    private const int MaximumTitleLength = 128;
    private const int MaximumSelectionFieldLength = 256;
    private const int MaximumSubjectFieldLength = 4_000;
    private const int MaximumListTextLength = 4_000;
    private const string QuarantineMessage =
        "A named-playlist write outcome is unknown. Restart before another write.";

    private readonly object _gate = new();
    private readonly object _authority = new();
    private readonly SemaphoreSlim _writeGate = new(1, 1);
    private readonly INamedPlaylistPersistenceService _persistence;
    private List<DefinitionState> _definitions = [];
    private List<RowState> _rows = [];
    private string? _selectedProgramCode;
    private string? _selectedDefinitionId;
    private string? _selectedTitle;
    private string? _suggestedProgramCode;
    private string _statusMessage = string.Empty;
    private LegacyNamedPlaylistReadStatus _readStatus;
    private LegacyNamedPlaylistFreshness _listFreshness;
    private LegacyNamedPlaylistFreshness _documentFreshness;
    private LegacyNamedPlaylistMutationOutcome? _lastMutationOutcome;
    private LegacyOperatorStatusKind _statusKind = LegacyOperatorStatusKind.Neutral;
    private bool _isListTruncated;
    private bool _isWriteInProgress;
    private bool _isWriteQuarantined;
    private int _listGeneration;
    private int _loadGeneration;
    private long _mutationEpoch;
    private long _opaqueSequence;
    private long _revision;

    public LegacyNamedPlaylistWorkflowController(
        INamedPlaylistPersistenceService persistence)
    {
        _persistence = persistence ?? throw new ArgumentNullException(nameof(persistence));
    }

    public LegacyNamedPlaylistWorkflowSnapshot Current
    {
        get
        {
            lock (_gate)
            {
                return CreateSnapshot();
            }
        }
    }

    /// <summary>
    /// Conservatively latches the no-retry barrier when an exception escapes the
    /// native dispatch boundary after a named-playlist write was accepted. The
    /// caller cannot prove whether the database observed the mutation, so every
    /// later write in this workflow instance must remain blocked until restart.
    /// </summary>
    public LegacyNamedPlaylistWorkflowSnapshot QuarantineAmbiguousWriteOutcome()
    {
        lock (_gate)
        {
            _isWriteQuarantined = true;
            _lastMutationOutcome = LegacyNamedPlaylistMutationOutcome.OutcomeUnknown;
            _statusMessage = QuarantineMessage;
            _statusKind = LegacyOperatorStatusKind.Error;
            ++_mutationEpoch;
            ++_listGeneration;
            ++_loadGeneration;
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyNamedPlaylistWorkflowSnapshot> RefreshAsync(
        int maximumResults = DefaultMaximumDefinitions,
        CancellationToken cancellationToken = default)
    {
        if (maximumResults is < 1 or > MaximumDefinitions)
        {
            throw new ArgumentOutOfRangeException(nameof(maximumResults));
        }

        cancellationToken.ThrowIfCancellationRequested();
        int generation;
        long mutationEpoch;
        lock (_gate)
        {
            RequireNoWriteInProgress();
            generation = ++_listGeneration;
            mutationEpoch = _mutationEpoch;
            _readStatus = LegacyNamedPlaylistReadStatus.Loading;
            _statusMessage = "이름 있는 편성표 목록을 조회하고 있습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
        }

        try
        {
            var result = await _persistence.ListAsync(
                maximumResults,
                cancellationToken).ConfigureAwait(false);
            var suggested = await _persistence.SuggestNextProgramCodeAsync(
                cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            var definitions = ValidateList(result, maximumResults);
            ValidateProgramCode(suggested);

            lock (_gate)
            {
                if (generation != _listGeneration || mutationEpoch != _mutationEpoch)
                {
                    return CreateSnapshot();
                }

                ApplyDefinitionList(
                    definitions,
                    result.IsTruncated,
                    suggested,
                    LegacyNamedPlaylistFreshness.Fresh);
                _readStatus = LegacyNamedPlaylistReadStatus.Ready;
                _statusMessage = $"이름 있는 편성표 {_definitions.Count}개를 조회했습니다.";
                _statusKind = LegacyOperatorStatusKind.Information;
                Touch();
                return CreateSnapshot();
            }
        }
        catch (OperationCanceledException)
        {
            lock (_gate)
            {
                if (generation == _listGeneration && mutationEpoch == _mutationEpoch)
                {
                    _readStatus = LegacyNamedPlaylistReadStatus.Idle;
                    _statusMessage = "이름 있는 편성표 목록 조회가 취소되었습니다.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                    Touch();
                }
            }

            throw;
        }
        catch
        {
            lock (_gate)
            {
                if (generation == _listGeneration && mutationEpoch == _mutationEpoch)
                {
                    _readStatus = LegacyNamedPlaylistReadStatus.Error;
                    _statusMessage = "이름 있는 편성표 목록을 안전하게 조회하지 못했습니다.";
                    _statusKind = LegacyOperatorStatusKind.Error;
                    Touch();
                }
            }

            throw;
        }
    }

    public LegacyNamedPlaylistWorkflowSnapshot SelectDefinition(string definitionId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(definitionId);
        lock (_gate)
        {
            RequireNoWriteInProgress();
            var selected = _definitions.FirstOrDefault(definition =>
                string.Equals(definition.DefinitionId, definitionId, StringComparison.Ordinal)) ??
                throw new KeyNotFoundException("The named-playlist definition id is stale or unknown.");
            _selectedProgramCode = selected.Summary.ProgramCode;
            _selectedDefinitionId = selected.DefinitionId;
            _selectedTitle = selected.Summary.Title;
            _rows = [];
            _documentFreshness = LegacyNamedPlaylistFreshness.None;
            ++_loadGeneration;
            _statusMessage = $"'{selected.Summary.Title}' 편성표를 선택했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
            return CreateSnapshot();
        }
    }

    public async Task<LegacyNamedPlaylistWorkflowSnapshot> LoadSelectedAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        string programCode;
        int generation;
        long mutationEpoch;
        lock (_gate)
        {
            RequireNoWriteInProgress();
            programCode = _selectedProgramCode ??
                throw new InvalidOperationException("Select a named playlist first.");
            generation = ++_loadGeneration;
            mutationEpoch = _mutationEpoch;
            _readStatus = LegacyNamedPlaylistReadStatus.Loading;
            _statusMessage = "선택한 편성표를 불러오고 있습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
            Touch();
        }

        try
        {
            var document = ValidateDocument(
                await _persistence.LoadAsync(programCode, cancellationToken).ConfigureAwait(false),
                programCode);
            cancellationToken.ThrowIfCancellationRequested();
            lock (_gate)
            {
                if (generation != _loadGeneration || mutationEpoch != _mutationEpoch ||
                    !string.Equals(
                        programCode,
                        _selectedProgramCode,
                        StringComparison.Ordinal))
                {
                    return CreateSnapshot();
                }

                ApplyDocument(document, LegacyNamedPlaylistFreshness.Fresh);
                _readStatus = LegacyNamedPlaylistReadStatus.Ready;
                _statusMessage = $"'{document.Definition.Title}' 편성표를 불러왔습니다.";
                _statusKind = LegacyOperatorStatusKind.Information;
                Touch();
                return CreateSnapshot();
            }
        }
        catch (OperationCanceledException)
        {
            lock (_gate)
            {
                if (generation == _loadGeneration && mutationEpoch == _mutationEpoch)
                {
                    _readStatus = LegacyNamedPlaylistReadStatus.Idle;
                    _statusMessage = "선택한 편성표 불러오기가 취소되었습니다.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                    Touch();
                }
            }

            throw;
        }
        catch
        {
            lock (_gate)
            {
                if (generation == _loadGeneration && mutationEpoch == _mutationEpoch)
                {
                    _readStatus = LegacyNamedPlaylistReadStatus.Error;
                    _statusMessage = "선택한 편성표를 안전하게 불러오지 못했습니다.";
                    _statusKind = LegacyOperatorStatusKind.Error;
                    Touch();
                }
            }

            throw;
        }
    }

    public LegacyNamedPlaylistProgramDraft CreateProgramDraft(string title)
    {
        var canonicalTitle = ValidateCanonicalText(
            title,
            MaximumTitleLength,
            allowEmpty: false,
            allowCaret: true,
            nameof(title));
        lock (_gate)
        {
            return new LegacyNamedPlaylistProgramDraft(
                _authority,
                NextOpaqueId("named-program-draft"),
                canonicalTitle);
        }
    }

    public LegacyNamedPlaylistRowDraft CreateRowDraft(
        LegacyPlaylistEntry entry,
        NamedPlaylistPageState? page)
    {
        ArgumentNullException.ThrowIfNull(entry);
        var selection = entry.Selection ??
            throw new ArgumentException("A storable playlist entry requires a selection.", nameof(entry));
        var validated = ValidateStoredItem(
            new NamedPlaylistStoredItem(0, entry.IsEnabled, selection, page),
            0,
            nameof(entry));
        lock (_gate)
        {
            return CreateRowDraftLocked(validated);
        }
    }

    public LegacyNamedPlaylistRowDraft CreateDraftFromLoadedRow(string rowId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(rowId);
        lock (_gate)
        {
            var row = _rows.FirstOrDefault(candidate =>
                string.Equals(candidate.RowId, rowId, StringComparison.Ordinal)) ??
                throw new KeyNotFoundException("The named-playlist row id is stale or unknown.");
            return CreateRowDraftLocked(row.Item);
        }
    }

    public Task<LegacyNamedPlaylistMutationResult> CreateProgramAsync(
        LegacyNamedPlaylistProgramDraft draft,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(draft);
        string programCode;
        lock (_gate)
        {
            RequireOwnedDraft(draft.Authority, nameof(draft));
            programCode = _suggestedProgramCode ??
                throw new InvalidOperationException(
                    "Refresh the named-playlist list before creating a program.");
        }

        return ExecuteMutationAsync(
            LegacyNamedPlaylistMutationOperation.Create,
            token => _persistence.CreateDefinitionAsync(programCode, draft.Title, token),
            () => ApplyOptimisticCreate(programCode, draft.Title),
            token => ConfirmCreatedAsync(programCode, draft.Title, token),
            cancellationToken);
    }

    public Task<LegacyNamedPlaylistMutationResult> SaveCurrentAsync(
        IReadOnlyList<LegacyNamedPlaylistRowDraft> rows,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(rows);
        if (rows.Count > MaximumItems)
        {
            throw new ArgumentOutOfRangeException(nameof(rows));
        }

        string programCode;
        NamedPlaylistStoredItem[] items;
        lock (_gate)
        {
            programCode = _selectedProgramCode ??
                throw new InvalidOperationException("Select a named playlist first.");
            items = new NamedPlaylistStoredItem[rows.Count];
            for (var index = 0; index < rows.Count; index++)
            {
                var draft = rows[index] ??
                    throw new ArgumentException("A named-playlist row draft is missing.", nameof(rows));
                RequireOwnedDraft(draft.Authority, nameof(rows));
                items[index] = ValidateStoredItem(
                    new NamedPlaylistStoredItem(
                        index,
                        draft.IsEnabled,
                        draft.StoredSelection,
                        draft.StoredPage),
                    index,
                    nameof(rows));
            }
        }

        var immutableItems = Array.AsReadOnly(items);
        return ExecuteMutationAsync(
            LegacyNamedPlaylistMutationOperation.SaveCurrent,
            token => _persistence.ReplaceItemsAsync(programCode, immutableItems, token),
            () => ApplyOptimisticRows(items),
            token => ConfirmSavedAsync(programCode, items, token),
            cancellationToken);
    }

    public Task<LegacyNamedPlaylistMutationResult> DeleteSelectedAsync(
        CancellationToken cancellationToken = default)
    {
        string programCode;
        lock (_gate)
        {
            programCode = _selectedProgramCode ??
                throw new InvalidOperationException("Select a named playlist first.");
        }

        return ExecuteMutationAsync(
            LegacyNamedPlaylistMutationOperation.Delete,
            token => _persistence.DeleteAsync(programCode, token),
            () => ApplyOptimisticDelete(programCode),
            token => ConfirmDeletedAsync(programCode, token),
            cancellationToken);
    }

    private async Task<LegacyNamedPlaylistMutationResult> ExecuteMutationAsync(
        LegacyNamedPlaylistMutationOperation operation,
        Func<CancellationToken, Task> mutation,
        Action applyOptimistic,
        Func<CancellationToken, Task<bool>> confirmFresh,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        string mutationId;
        lock (_gate)
        {
            mutationId = NextOpaqueId("named-mutation");
            if (!_persistence.CanMutate)
            {
                return CompleteWithoutAttempt(
                    mutationId,
                    operation,
                    LegacyNamedPlaylistMutationOutcome.ReadOnly,
                    "이름 있는 편성표 저장소가 읽기 전용입니다.");
            }

            if (_isWriteQuarantined)
            {
                return CompleteWithoutAttempt(
                    mutationId,
                    operation,
                    LegacyNamedPlaylistMutationOutcome.Quarantined,
                    QuarantineMessage);
            }
        }

        if (!await _writeGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
        {
            return new LegacyNamedPlaylistMutationResult(
                mutationId,
                operation,
                LegacyNamedPlaylistMutationOutcome.Busy,
                Current);
        }

        var began = false;
        var outcome = LegacyNamedPlaylistMutationOutcome.OutcomeUnknown;
        try
        {
            lock (_gate)
            {
                if (_isWriteQuarantined)
                {
                    outcome = LegacyNamedPlaylistMutationOutcome.Quarantined;
                    _lastMutationOutcome = outcome;
                    _statusMessage = QuarantineMessage;
                    _statusKind = LegacyOperatorStatusKind.Error;
                    Touch();
                }
                else
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    _isWriteInProgress = true;
                    _statusMessage = "이름 있는 편성표 변경을 한 번만 실행하고 있습니다.";
                    _statusKind = LegacyOperatorStatusKind.Information;
                    began = true;
                    Touch();
                }
            }

            if (began)
            {
                var mutationCommitted = false;
                try
                {
                    // From this point onward cancellation or an unclassified exception is
                    // ambiguous. The workflow never invokes this delegate more than once.
                    await mutation(cancellationToken).ConfigureAwait(false);
                    mutationCommitted = true;
                }
                catch (NamedPlaylistMutationException exception)
                {
                    outcome = exception.OutcomeUnknown
                        ? LegacyNamedPlaylistMutationOutcome.OutcomeUnknown
                        : LegacyNamedPlaylistMutationOutcome.RolledBack;
                    RecordMutationFailure(outcome);
                }
                catch (NamedPlaylistNotFoundException)
                {
                    outcome = LegacyNamedPlaylistMutationOutcome.NotFound;
                    RecordMutationFailure(outcome);
                }
                catch (ArgumentException)
                {
                    outcome = LegacyNamedPlaylistMutationOutcome.RolledBack;
                    RecordMutationFailure(outcome);
                }
                catch
                {
                    outcome = LegacyNamedPlaylistMutationOutcome.OutcomeUnknown;
                    RecordMutationFailure(outcome);
                }

                if (mutationCommitted)
                {
                    lock (_gate)
                    {
                        applyOptimistic();
                        ++_mutationEpoch;
                        ++_listGeneration;
                        ++_loadGeneration;
                        _statusMessage = "변경은 커밋되었으며 DB 재조회로 확인하고 있습니다.";
                        _statusKind = LegacyOperatorStatusKind.Information;
                        Touch();
                    }

                    var isFresh = false;
                    try
                    {
                        isFresh = await confirmFresh(cancellationToken).ConfigureAwait(false);
                    }
                    catch
                    {
                        // The mutation already returned a known committed result. A failed read-back
                        // keeps the optimistic copy and never causes a second mutation attempt.
                        isFresh = false;
                    }

                    outcome = isFresh
                        ? LegacyNamedPlaylistMutationOutcome.CommittedFresh
                        : LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
                    lock (_gate)
                    {
                        _lastMutationOutcome = outcome;
                        _readStatus = LegacyNamedPlaylistReadStatus.Ready;
                        _statusMessage = isFresh
                            ? "이름 있는 편성표 변경을 DB 재조회로 확인했습니다."
                            : "변경은 커밋되었지만 재조회에 실패해 optimistic 상태로 표시합니다.";
                        _statusKind = isFresh
                            ? LegacyOperatorStatusKind.Information
                            : LegacyOperatorStatusKind.Warning;
                        Touch();
                    }
                }
            }
        }
        finally
        {
            if (began)
            {
                lock (_gate)
                {
                    _isWriteInProgress = false;
                    Touch();
                }
            }

            _writeGate.Release();
        }

        return FinishMutation(mutationId, operation, outcome);
    }

    private LegacyNamedPlaylistMutationResult FinishMutation(
        string mutationId,
        LegacyNamedPlaylistMutationOperation operation,
        LegacyNamedPlaylistMutationOutcome outcome) =>
        new(mutationId, operation, outcome, Current);

    private LegacyNamedPlaylistMutationResult CompleteWithoutAttempt(
        string mutationId,
        LegacyNamedPlaylistMutationOperation operation,
        LegacyNamedPlaylistMutationOutcome outcome,
        string message)
    {
        _lastMutationOutcome = outcome;
        _statusMessage = message;
        _statusKind = outcome == LegacyNamedPlaylistMutationOutcome.ReadOnly
            ? LegacyOperatorStatusKind.Warning
            : LegacyOperatorStatusKind.Error;
        Touch();
        return new LegacyNamedPlaylistMutationResult(
            mutationId,
            operation,
            outcome,
            CreateSnapshot());
    }

    private void RecordMutationFailure(LegacyNamedPlaylistMutationOutcome outcome)
    {
        lock (_gate)
        {
            _lastMutationOutcome = outcome;
            switch (outcome)
            {
                case LegacyNamedPlaylistMutationOutcome.OutcomeUnknown:
                    _isWriteQuarantined = true;
                    _statusMessage = QuarantineMessage;
                    _statusKind = LegacyOperatorStatusKind.Error;
                    break;

                case LegacyNamedPlaylistMutationOutcome.NotFound:
                    _statusMessage = "선택한 이름 있는 편성표가 더 이상 존재하지 않습니다.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                    break;

                default:
                    _statusMessage = "변경은 롤백되었으며 자동 재시도하지 않았습니다.";
                    _statusKind = LegacyOperatorStatusKind.Warning;
                    break;
            }

            Touch();
        }
    }

    private async Task<bool> ConfirmCreatedAsync(
        string programCode,
        string expectedTitle,
        CancellationToken cancellationToken)
    {
        var document = ValidateDocument(
            await _persistence.LoadAsync(programCode, cancellationToken).ConfigureAwait(false),
            programCode);
        if (!string.Equals(document.Definition.Title, expectedTitle, StringComparison.Ordinal) ||
            document.Items.Count != 0)
        {
            return false;
        }

        var list = await _persistence.ListAsync(
            MaximumDefinitions,
            cancellationToken).ConfigureAwait(false);
        var definitions = ValidateList(list, MaximumDefinitions);
        var suggested = await _persistence.SuggestNextProgramCodeAsync(
            cancellationToken).ConfigureAwait(false);
        ValidateProgramCode(suggested);
        if (!definitions.Any(definition =>
                string.Equals(definition.ProgramCode, programCode, StringComparison.Ordinal) &&
                string.Equals(definition.Title, expectedTitle, StringComparison.Ordinal)))
        {
            return false;
        }

        lock (_gate)
        {
            ApplyDefinitionList(
                definitions,
                list.IsTruncated,
                suggested,
                LegacyNamedPlaylistFreshness.Fresh);
            Touch();
        }

        return true;
    }

    private async Task<bool> ConfirmSavedAsync(
        string programCode,
        IReadOnlyList<NamedPlaylistStoredItem> expectedItems,
        CancellationToken cancellationToken)
    {
        var document = ValidateDocument(
            await _persistence.LoadAsync(programCode, cancellationToken).ConfigureAwait(false),
            programCode);
        if (!document.Items.SequenceEqual(expectedItems))
        {
            return false;
        }

        lock (_gate)
        {
            if (!string.Equals(programCode, _selectedProgramCode, StringComparison.Ordinal))
            {
                return false;
            }

            ApplyDocument(document, LegacyNamedPlaylistFreshness.Fresh);
            Touch();
        }

        return true;
    }

    private async Task<bool> ConfirmDeletedAsync(
        string deletedProgramCode,
        CancellationToken cancellationToken)
    {
        var list = await _persistence.ListAsync(
            MaximumDefinitions,
            cancellationToken).ConfigureAwait(false);
        var definitions = ValidateList(list, MaximumDefinitions);
        var suggested = await _persistence.SuggestNextProgramCodeAsync(
            cancellationToken).ConfigureAwait(false);
        ValidateProgramCode(suggested);
        if (definitions.Any(definition => string.Equals(
                definition.ProgramCode,
                deletedProgramCode,
                StringComparison.Ordinal)))
        {
            return false;
        }

        lock (_gate)
        {
            ApplyDefinitionList(
                definitions,
                list.IsTruncated,
                suggested,
                LegacyNamedPlaylistFreshness.Fresh);
            _rows = [];
            _documentFreshness = LegacyNamedPlaylistFreshness.None;
            Touch();
        }

        return true;
    }

    private void ApplyOptimisticCreate(string programCode, string title)
    {
        var definition = new DefinitionState(
            NextOpaqueId("named-definition"),
            new NamedPlaylistSummary(programCode, title));
        _definitions.Add(definition);
        _definitions = _definitions
            .OrderBy(item => item.Summary.Title, StringComparer.Ordinal)
            .ThenBy(item => item.Summary.ProgramCode, StringComparer.Ordinal)
            .ToList();
        _selectedProgramCode = programCode;
        _selectedDefinitionId = definition.DefinitionId;
        _selectedTitle = title;
        _rows = [];
        _suggestedProgramCode = null;
        _listFreshness = LegacyNamedPlaylistFreshness.Optimistic;
        _documentFreshness = LegacyNamedPlaylistFreshness.Optimistic;
    }

    private void ApplyOptimisticRows(IReadOnlyList<NamedPlaylistStoredItem> items)
    {
        _rows = CreateRowStates(items);
        _documentFreshness = LegacyNamedPlaylistFreshness.Optimistic;
    }

    private void ApplyOptimisticDelete(string programCode)
    {
        _definitions.RemoveAll(definition => string.Equals(
            definition.Summary.ProgramCode,
            programCode,
            StringComparison.Ordinal));
        _selectedProgramCode = null;
        _selectedDefinitionId = null;
        _selectedTitle = null;
        _rows = [];
        _suggestedProgramCode = null;
        _listFreshness = LegacyNamedPlaylistFreshness.Optimistic;
        _documentFreshness = LegacyNamedPlaylistFreshness.None;
    }

    private void ApplyDefinitionList(
        IReadOnlyList<NamedPlaylistSummary> summaries,
        bool isTruncated,
        string suggestedProgramCode,
        LegacyNamedPlaylistFreshness freshness)
    {
        var previousSelectedCode = _selectedProgramCode;
        _definitions = summaries.Select(summary => new DefinitionState(
            NextOpaqueId("named-definition"),
            summary)).ToList();
        _isListTruncated = isTruncated;
        _suggestedProgramCode = suggestedProgramCode;
        _listFreshness = freshness;

        // WinForms assigns a new DataSource on every PList refresh. A non-empty
        // ListBox therefore selects index zero, including after add and delete.
        // Keep that operator-visible reset instead of preserving a Web selection
        // or selecting the newly created definition.
        var selected = _definitions.FirstOrDefault();
        if (selected is null)
        {
            _selectedProgramCode = null;
            _selectedDefinitionId = null;
            _selectedTitle = null;
            _rows = [];
            _documentFreshness = LegacyNamedPlaylistFreshness.None;
            ++_loadGeneration;
            return;
        }

        _selectedProgramCode = selected.Summary.ProgramCode;
        _selectedDefinitionId = selected.DefinitionId;
        _selectedTitle = selected.Summary.Title;
        if (!string.Equals(
                previousSelectedCode,
                selected.Summary.ProgramCode,
                StringComparison.Ordinal))
        {
            _rows = [];
            _documentFreshness = LegacyNamedPlaylistFreshness.None;
            ++_loadGeneration;
        }
    }

    private void ApplyDocument(
        NamedPlaylistDocument document,
        LegacyNamedPlaylistFreshness freshness)
    {
        var definitionIndex = _definitions.FindIndex(definition => string.Equals(
            definition.Summary.ProgramCode,
            document.Definition.ProgramCode,
            StringComparison.Ordinal));
        if (definitionIndex >= 0)
        {
            var existing = _definitions[definitionIndex];
            _definitions[definitionIndex] = existing with { Summary = document.Definition };
            _selectedDefinitionId = existing.DefinitionId;
        }

        _selectedProgramCode = document.Definition.ProgramCode;
        _selectedTitle = document.Definition.Title;
        _rows = CreateRowStates(document.Items);
        _documentFreshness = freshness;
    }

    private List<RowState> CreateRowStates(IEnumerable<NamedPlaylistStoredItem> items) =>
        items.Select(item => new RowState(
            NextOpaqueId("named-row"),
            item)).ToList();

    private LegacyNamedPlaylistRowDraft CreateRowDraftLocked(NamedPlaylistStoredItem item) =>
        new(
            _authority,
            NextOpaqueId("named-row-draft"),
            item.IsEnabled,
            item.Selection,
            item.Page);

    private LegacyNamedPlaylistWorkflowSnapshot CreateSnapshot()
    {
        var definitions = _definitions.Select(definition =>
            new LegacyNamedPlaylistDefinitionView(
                definition.DefinitionId,
                definition.Summary.Title,
                string.Equals(
                    definition.DefinitionId,
                    _selectedDefinitionId,
                    StringComparison.Ordinal))).ToArray();
        var rows = _rows.Select(row => new LegacyNamedPlaylistRowView(
            row.RowId,
            row.Item.ItemIndex,
            row.Item.IsEnabled,
            ToView(row.Item.Selection),
            ToView(row.Item.Page))).ToArray();
        return new LegacyNamedPlaylistWorkflowSnapshot(
            _revision,
            _readStatus,
            definitions,
            _isListTruncated,
            _selectedDefinitionId,
            _selectedTitle,
            rows,
            _listFreshness,
            _documentFreshness,
            _isWriteInProgress,
            _isWriteQuarantined,
            _persistence.CanMutate,
            _suggestedProgramCode is not null,
            _lastMutationOutcome,
            _statusMessage,
            _statusKind);
    }

    internal static LegacyNamedPlaylistSelectionView ToView(LegacySceneSelection selection) =>
        new(
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            selection.Subtype,
            selection.DataCode);

    internal static LegacyNamedPlaylistPageView? ToView(NamedPlaylistPageState? page) =>
        page is null
            ? null
            : new LegacyNamedPlaylistPageView(
                page.CurrentPage,
                page.TotalPages,
                page.ToString(),
                page.IsWithinPlayoutBounds);

    private static IReadOnlyList<NamedPlaylistSummary> ValidateList(
        NamedPlaylistListResult result,
        int maximumResults)
    {
        ArgumentNullException.ThrowIfNull(result);
        if (result.Items is null || result.Items.Count > maximumResults ||
            result.IsTruncated && result.Items.Count != maximumResults)
        {
            throw new NamedPlaylistDataException("The named-playlist list envelope is invalid.");
        }

        var codes = new HashSet<string>(StringComparer.Ordinal);
        var items = new NamedPlaylistSummary[result.Items.Count];
        for (var index = 0; index < result.Items.Count; index++)
        {
            var item = result.Items[index] ??
                throw new NamedPlaylistDataException("A named-playlist definition is missing.");
            ValidateProgramCode(item.ProgramCode);
            try
            {
                ValidateCanonicalText(
                    item.Title,
                    MaximumTitleLength,
                    allowEmpty: false,
                    allowCaret: true,
                    nameof(result));
            }
            catch (ArgumentException exception)
            {
                throw new NamedPlaylistDataException(
                    $"A named-playlist definition title is invalid: {exception.Message}");
            }

            if (!codes.Add(item.ProgramCode))
            {
                throw new NamedPlaylistDataException(
                    "A named-playlist definition identity is duplicated.");
            }

            items[index] = item;
        }

        return Array.AsReadOnly(items);
    }

    private static NamedPlaylistDocument ValidateDocument(
        NamedPlaylistDocument document,
        string expectedProgramCode)
    {
        ArgumentNullException.ThrowIfNull(document);
        ArgumentNullException.ThrowIfNull(document.Definition);
        ValidateProgramCode(document.Definition.ProgramCode);
        if (!string.Equals(
                document.Definition.ProgramCode,
                expectedProgramCode,
                StringComparison.Ordinal))
        {
            throw new NamedPlaylistDataException(
                "The named-playlist document identity does not match the request.");
        }

        string title;
        try
        {
            title = ValidateCanonicalText(
                document.Definition.Title,
                MaximumTitleLength,
                allowEmpty: false,
                allowCaret: true,
                nameof(document));
        }
        catch (ArgumentException exception)
        {
            throw new NamedPlaylistDataException(
                $"A named-playlist document title is invalid: {exception.Message}");
        }

        if (document.Items is null || document.Items.Count > MaximumItems)
        {
            throw new NamedPlaylistDataException("The named-playlist row count is invalid.");
        }

        var items = new NamedPlaylistStoredItem[document.Items.Count];
        for (var index = 0; index < document.Items.Count; index++)
        {
            try
            {
                items[index] = ValidateStoredItem(document.Items[index], index, nameof(document));
            }
            catch (ArgumentException exception)
            {
                throw new NamedPlaylistDataException(
                    $"A named-playlist document row is invalid: {exception.Message}");
            }
        }

        return new NamedPlaylistDocument(
            new NamedPlaylistSummary(expectedProgramCode, title),
            document.RetrievedAt,
            Array.AsReadOnly(items));
    }

    private static NamedPlaylistStoredItem ValidateStoredItem(
        NamedPlaylistStoredItem? item,
        int expectedIndex,
        string parameterName)
    {
        if (item is null || item.ItemIndex != expectedIndex || item.Selection is null)
        {
            throw new ArgumentException(
                "Named-playlist rows require contiguous zero-based indexes.",
                parameterName);
        }

        var groupCode = ValidateCanonicalText(
            item.Selection.GroupCode,
            MaximumSelectionFieldLength,
            allowEmpty: true,
            allowCaret: false,
            parameterName);
        var dataCode = ValidateCanonicalText(
            item.Selection.DataCode,
            MaximumSelectionFieldLength,
            allowEmpty: true,
            allowCaret: false,
            parameterName);
        var subject = CanPreserveClosedThemeSubject(
                groupCode,
                item.Selection.Subject,
                dataCode)
            ? item.Selection.Subject
            : ValidateCanonicalText(
                item.Selection.Subject,
                MaximumSubjectFieldLength,
                allowEmpty: true,
                allowCaret: false,
                parameterName);
        var selection = new LegacySceneSelection(
            groupCode,
            subject,
            ValidateCanonicalText(
                item.Selection.GraphicType,
                MaximumSelectionFieldLength,
                allowEmpty: true,
                allowCaret: false,
                parameterName),
            ValidateCanonicalText(
                item.Selection.Subtype,
                MaximumSelectionFieldLength,
                allowEmpty: true,
                allowCaret: false,
                parameterName),
            dataCode);
        var validated = new NamedPlaylistStoredItem(
            expectedIndex,
            item.IsEnabled,
            selection,
            item.Page);
        var serializedLength = string.Join(
            '^',
            validated.IsEnabled ? "1" : "0",
            selection.GroupCode,
            selection.Subject,
            selection.GraphicType,
            selection.Subtype,
            validated.Page?.ToString() ?? string.Empty,
            selection.DataCode).Length;
        if (serializedLength > MaximumListTextLength)
        {
            throw new ArgumentException(
                "A named-playlist seven-field row is too long.",
                parameterName);
        }

        return validated;
    }

    private static string ValidateCanonicalText(
        string value,
        int maximumLength,
        bool allowEmpty,
        bool allowCaret,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(value);
        if ((!allowEmpty && value.Length == 0) || value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            !IsSafeText(value) || !allowCaret && value.Contains('^'))
        {
            throw new ArgumentException("A named-playlist text value is invalid.", parameterName);
        }

        return value;
    }

    private static bool CanPreserveClosedThemeSubject(
        string groupCode,
        string subject,
        string dataCode)
    {
        if (dataCode.Length is < 3 or > 8 ||
            dataCode.Any(character => !char.IsAsciiDigit(character)))
        {
            return false;
        }

        var isNxt = groupCode is "테마_NXT" or "PAGED_NXT_THEME";
        if (!isNxt && groupCode is not ("테마" or "PAGED_THEME"))
        {
            return false;
        }

        const string suffix = "(NXT)";
        var exactTitle = isNxt && subject.EndsWith(suffix, StringComparison.Ordinal)
            ? subject[..^suffix.Length]
            : isNxt ? string.Empty : subject;
        return exactTitle.Length is >= 1 and <= 128 &&
            !string.IsNullOrWhiteSpace(exactTitle) &&
            !subject.Contains('^') &&
            IsSafeText(subject);
    }

    private static void ValidateProgramCode(string value)
    {
        if (value is null || value.Length != 8 ||
            value.Any(static character => !char.IsAsciiDigit(character)))
        {
            throw new NamedPlaylistDataException(
                "A named-playlist internal identity must contain eight digits.");
        }
    }

    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;
    }

    private void RequireOwnedDraft(object authority, string parameterName)
    {
        if (!ReferenceEquals(authority, _authority))
        {
            throw new ArgumentException(
                "The named-playlist draft belongs to another workflow instance.",
                parameterName);
        }
    }

    private void RequireNoWriteInProgress()
    {
        if (_isWriteInProgress)
        {
            throw new InvalidOperationException(
                "The named-playlist state cannot change while a write is in progress.");
        }
    }

    private string NextOpaqueId(string prefix) =>
        $"{prefix}-{EncodeOpaqueSequence(++_opaqueSequence)}";

    private static string EncodeOpaqueSequence(long value)
    {
        Span<char> encoded = stackalloc char[12];
        for (var index = encoded.Length - 1; index >= 0; index--)
        {
            encoded[index] = (char)('A' + value % 26);
            value /= 26;
        }

        return new string(encoded);
    }

    private void Touch() => _revision++;

    private sealed record DefinitionState(
        string DefinitionId,
        NamedPlaylistSummary Summary);

    private sealed record RowState(
        string RowId,
        NamedPlaylistStoredItem Item);
}
