using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Globalization;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyManualFinancialWorkflowStatus
{
    Idle,
    ListLoaded,
    DetailLoaded,
    StockCandidatesLoaded,
    StockVerified,
    WriteCommitted,
    WriteRejected,
    ReadBackRequired,
    OutcomeUnknown
}

public enum LegacyManualFinancialFindDirection
{
    Up,
    Down
}

public enum LegacyManualFinancialNameSortDirection
{
    Ascending,
    Descending
}

public sealed class LegacyManualFinancialScreenDefinition
{
    internal LegacyManualFinancialScreenDefinition(
        ManualFinancialScreenKind screen,
        string actionId,
        string label,
        string tableName,
        string builderKey,
        string code,
        string canonicalGraphicType,
        string deleteAllConfirmationToken)
    {
        Screen = screen;
        ActionId = actionId;
        Label = label;
        TableName = tableName;
        BuilderKey = builderKey;
        Code = code;
        Aliases = Array.AsReadOnly([code]);
        CanonicalGraphicType = canonicalGraphicType;
        DeleteAllConfirmationToken = deleteAllConfirmationToken;
    }

    public ManualFinancialScreenKind Screen { get; }

    public string ActionId { get; }

    public string Label { get; }

    public string TableName { get; }

    public string BuilderKey { get; }

    public string Code { get; }

    public IReadOnlyList<string> Aliases { get; }

    public string CanonicalGraphicType { get; }

    public string DeleteAllConfirmationToken { get; }
}

public sealed class LegacyManualFinancialListRow
{
    internal LegacyManualFinancialListRow(string resultId, ManualFinancialSnapshot snapshot)
    {
        ResultId = resultId;
        Snapshot = snapshot;
        StockName = snapshot.Record.Identity.StockName;
        PreviewValues = CreatePreviewValues(snapshot.Record);
    }

    internal LegacyManualFinancialListRow(
        string resultId,
        ManualFinancialRawSnapshot snapshot)
    {
        ResultId = resultId;
        RawSnapshot = snapshot;
        Snapshot = snapshot.StrictSnapshot;
        StockName = snapshot.Record.Identity.StockName;
        PreviewValues = Array.AsReadOnly(
            new[] { StockName }
                .Concat(snapshot.Record.StorageValues)
                .ToArray());
    }

    internal LegacyManualFinancialListRow(string resultId, string stockName)
    {
        ResultId = resultId;
        StockName = stockName;
        PreviewValues = Array.AsReadOnly([stockName]);
    }

    public string ResultId { get; }

    public string StockName { get; }

    /// <summary>
    /// Read-only GraphE list projection in the fixed INPUT_* storage-column
    /// order. It contains operator display values only: no row version, ROWID,
    /// native row handle, stock code, or database authority is exposed.
    /// </summary>
    public IReadOnlyList<string> PreviewValues { get; }

    public string RowVersion => RawSnapshot?.RowVersion ?? Snapshot?.RowVersion ?? string.Empty;

    public bool IsDataReadable => RawSnapshot is not null || Snapshot is not null;

    public bool IsPlayoutReady => Snapshot is not null;

    internal ManualFinancialSnapshot? Snapshot { get; }

    internal ManualFinancialRawSnapshot? RawSnapshot { get; }

    private static IReadOnlyList<string> CreatePreviewValues(
        ManualFinancialRecord record)
    {
        var values = record switch
        {
            ManualRevenueCompositionRecord revenue =>
                new[] { revenue.Identity.StockName, revenue.BaseDate }
                    .Concat(revenue.Slices.Select(static slice => slice is null
                        ? string.Empty
                        : $"{slice.Label}_{slice.PercentageText}")),
            ManualGrowthMetricsRecord growth =>
                new[] { growth.Identity.StockName }
                    .Concat(new[]
                    {
                        FormatGrowthSeries(growth.SalesGrowth),
                        FormatGrowthSeries(growth.OperatingProfitGrowth),
                        FormatGrowthSeries(growth.NetAssetGrowth),
                        FormatGrowthSeries(growth.NetIncomeGrowth),
                        string.Join('_', growth.Periods.ToArray())
                    }),
            ManualSalesRecord sales =>
                new[] { sales.Identity.StockName }
                    .Concat(sales.Quarters.Select(FormatQuarter)),
            ManualOperatingProfitRecord profit =>
                new[] { profit.Identity.StockName }
                    .Concat(profit.Quarters.Select(FormatQuarter)),
            _ => throw new ArgumentOutOfRangeException(nameof(record))
        };

        return Array.AsReadOnly(values.ToArray());
    }

    private static string FormatGrowthSeries(ManualGrowthSeriesValues series) =>
        string.Join('_', series.ToArray().Select(static value => value?.ToString(
            "G17",
            CultureInfo.InvariantCulture) ?? string.Empty));

    private static string FormatQuarter(ManualQuarterValue quarter) =>
        $"{quarter.Quarter}_{quarter.Value.ToString(CultureInfo.InvariantCulture)}";
}

public sealed class LegacyManualFinancialStockRow
{
    internal LegacyManualFinancialStockRow(string resultId, StockSearchItem item)
    {
        ResultId = resultId;
        Item = item;
    }

    public string ResultId { get; }

    public StockMarket Market => Item.Market;

    public string Name => Item.Name;

    public string Code => Item.Code;

    internal StockSearchItem Item { get; }
}

public sealed record LegacyManualFinancialVerifiedStockSummary(
    StockMarket Market,
    string Name,
    string Code);

public sealed record LegacyManualFinancialPlaylistDraft(
    string ActionId,
    string BuilderKey,
    string Code,
    IReadOnlyList<string> Aliases,
    string Title,
    string Detail,
    string Category,
    string Market,
    string StockCode,
    bool IsNxt,
    ManualFinancialCutSelection CutSelection,
    ManualFinancialSnapshot? OperatorSnapshot,
    ManualFinancialRawSnapshot? OperatorRawSnapshot);

public sealed record LegacyManualFinancialWorkflowSnapshot
{
    internal LegacyManualFinancialWorkflowSnapshot(
        object authority,
        long revision,
        LegacyManualFinancialScreenDefinition definition)
    {
        Authority = authority;
        Revision = revision;
        Definition = definition;
        Query = string.Empty;
        FindQuery = string.Empty;
        NameSortDirection = LegacyManualFinancialNameSortDirection.Ascending;
        Rows = Array.Empty<LegacyManualFinancialListRow>();
        StockQuery = string.Empty;
        StockCandidates = Array.Empty<LegacyManualFinancialStockRow>();
    }

    public long Revision { get; internal init; }

    public LegacyManualFinancialScreenDefinition Definition { get; internal init; }

    public ManualFinancialScreenKind Screen => Definition.Screen;

    public string Query { get; internal init; }

    public string FindQuery { get; internal init; }

    public LegacyManualFinancialNameSortDirection NameSortDirection { get; internal init; }

    public IReadOnlyList<LegacyManualFinancialListRow> Rows { get; internal init; }

    public bool ListIsTruncated { get; internal init; }

    public int ListRejectedItemCount { get; internal init; }

    public string? SelectedRowId { get; internal init; }

    public ManualFinancialRecord? SelectedRecord => Detail?.Record;

    public ManualFinancialRawRecord? SelectedRawRecord => RawDetail?.Record;

    public string? SelectedRowVersion => RawDetail?.RowVersion ?? Detail?.RowVersion;

    public bool IsDetailFresh { get; internal init; }

    public bool IsRawDetailFresh { get; internal init; }

    public bool IsPlayoutReady => IsDetailFresh && Detail is not null;

    public string StockQuery { get; internal init; }

    public IReadOnlyList<LegacyManualFinancialStockRow> StockCandidates { get; internal init; }

    public bool StockSearchIsTruncated { get; internal init; }

    public string? SelectedStockResultId { get; internal init; }

    public LegacyManualFinancialVerifiedStockSummary? VerifiedStock =>
        Verified is null
            ? null
            : new LegacyManualFinancialVerifiedStockSummary(
                Verified.Market,
                Verified.Name,
                Verified.Code);

    public LegacyManualFinancialWorkflowStatus Status { get; internal init; }

    public bool WritesQuarantined { get; internal init; }

    public string? LastMessage { get; internal init; }

    public string? LastRequestId { get; internal init; }

    public ManualFinancialMutationReceipt? LastReceipt { get; internal init; }

    public bool CanEdit =>
        !WritesQuarantined &&
        (IsRawDetailFresh && RawDetail is not null ||
            IsDetailFresh && Detail is not null);

    public bool CanSave =>
        !WritesQuarantined &&
        (RawDetail is not null
            ? IsRawDetailFresh
            : Verified is not null &&
                (Detail is null ||
                    IsDetailFresh &&
                    string.Equals(
                        VerifiedForRowVersion,
                        Detail.RowVersion,
                        StringComparison.Ordinal) &&
                    string.Equals(
                        Verified.Name,
                        Detail.Record.Identity.StockName,
                        StringComparison.Ordinal)));

    public bool CanDelete =>
        !WritesQuarantined &&
        (IsRawDetailFresh && RawDetail is not null ||
            IsDetailFresh && Detail is not null);

    public bool CanMaterializePlaylist =>
        !WritesQuarantined &&
        Verified is not null &&
        SelectedIdentity is { } identity &&
        SelectedRowVersion is { } rowVersion &&
        (IsRawDetailFresh || IsDetailFresh) &&
        string.Equals(VerifiedForRowVersion, rowVersion, StringComparison.Ordinal) &&
        string.Equals(
            Verified.Name,
            identity.StockName,
            StringComparison.Ordinal);

    internal object Authority { get; }

    internal long WriteEpoch { get; init; }

    internal ManualFinancialSnapshot? Detail { get; init; }

    internal ManualFinancialRawSnapshot? RawDetail { get; init; }

    internal ManualFinancialIdentity? SelectedIdentity =>
        RawDetail?.Record.Identity ?? Detail?.Record.Identity;

    internal VerifiedManualFinancialStock? Verified { get; init; }

    internal string? VerifiedForRowVersion { get; init; }
}

public abstract class LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialWriteRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialMutationKind kind,
        ManualFinancialScreenKind screen,
        string stockName)
    {
        Authority = authority;
        SourceRevision = sourceRevision;
        SourceWriteEpoch = sourceWriteEpoch;
        Kind = kind;
        Screen = screen;
        StockName = stockName;
        RequestId = Guid.NewGuid().ToString("N");
    }

    public string RequestId { get; }

    public ManualFinancialMutationKind Kind { get; }

    public ManualFinancialScreenKind Screen { get; }

    public string StockName { get; }

    public virtual string? ExpectedRowVersion => null;

    internal object Authority { get; }

    internal long SourceRevision { get; }

    internal long SourceWriteEpoch { get; }

    internal virtual VerifiedManualFinancialStock? VerifiedStock => null;
}

public sealed class LegacyManualFinancialCreateRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialCreateRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialRecord record,
        VerifiedManualFinancialStock verifiedStock)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.Create,
            record.Identity.Screen,
            record.Identity.StockName)
    {
        Record = record;
        Stock = verifiedStock;
    }

    public ManualFinancialRecord Record { get; }

    internal override VerifiedManualFinancialStock VerifiedStock => Stock;

    private VerifiedManualFinancialStock Stock { get; }
}

public sealed class LegacyManualFinancialUpdateRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialUpdateRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialSnapshot expected,
        ManualFinancialRecord replacement,
        VerifiedManualFinancialStock verifiedStock)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.Update,
            expected.Record.Identity.Screen,
            expected.Record.Identity.StockName)
    {
        Expected = expected;
        Replacement = replacement;
        Stock = verifiedStock;
    }

    public override string ExpectedRowVersion => Expected.RowVersion;

    public ManualFinancialRecord Replacement { get; }

    internal ManualFinancialSnapshot Expected { get; }

    internal override VerifiedManualFinancialStock VerifiedStock => Stock;

    private VerifiedManualFinancialStock Stock { get; }
}

public sealed class LegacyManualFinancialRawUpdateRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialRawUpdateRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialRawSnapshot expected,
        ManualFinancialRawRecord replacement)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.Update,
            expected.Record.Identity.Screen,
            expected.Record.Identity.StockName)
    {
        Expected = expected;
        Replacement = replacement;
    }

    public override string ExpectedRowVersion => Expected.RowVersion;

    public ManualFinancialRawRecord Replacement { get; }

    internal ManualFinancialRawSnapshot Expected { get; }
}

public sealed class LegacyManualFinancialDeleteRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialDeleteRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialSnapshot expected)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.DeleteOne,
            expected.Record.Identity.Screen,
            expected.Record.Identity.StockName)
    {
        Expected = expected;
    }

    public override string ExpectedRowVersion => Expected.RowVersion;

    internal ManualFinancialSnapshot Expected { get; }
}

public sealed class LegacyManualFinancialRawDeleteRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialRawDeleteRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        ManualFinancialRawSnapshot expected)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.DeleteOne,
            expected.Record.Identity.Screen,
            expected.Record.Identity.StockName)
    {
        Expected = expected;
    }

    public override string ExpectedRowVersion => Expected.RowVersion;

    internal ManualFinancialRawSnapshot Expected { get; }
}

public sealed class LegacyManualFinancialDeleteAllRequest : LegacyManualFinancialWriteRequest
{
    internal LegacyManualFinancialDeleteAllRequest(
        object authority,
        long sourceRevision,
        long sourceWriteEpoch,
        LegacyManualFinancialScreenDefinition definition)
        : base(
            authority,
            sourceRevision,
            sourceWriteEpoch,
            ManualFinancialMutationKind.DeleteAll,
            definition.Screen,
            string.Empty)
    {
        ConfirmationToken = definition.DeleteAllConfirmationToken;
    }

    public string ConfirmationToken { get; }
}

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

internal sealed class LegacyManualFinancialWriteSafetyState
{
    private readonly object _gate = new();
    private long _writeEpoch;
    private bool _writesQuarantined;
    private string? _quarantineReason;

    internal SemaphoreSlim WriteGate { get; } = new(1, 1);

    internal long CurrentEpoch
    {
        get
        {
            lock (_gate)
            {
                return _writeEpoch;
            }
        }
    }

    internal bool IsCurrent(long writeEpoch)
    {
        lock (_gate)
        {
            return writeEpoch == _writeEpoch;
        }
    }

    internal bool IsQuarantined(out string? reason)
    {
        lock (_gate)
        {
            reason = _quarantineReason;
            return _writesQuarantined;
        }
    }

    internal bool TryBeginWrite(
        long expectedEpoch,
        out long writeEpoch,
        out string reason)
    {
        lock (_gate)
        {
            writeEpoch = _writeEpoch;
            if (_writesQuarantined)
            {
                reason = $"Manual-financial writes are quarantined: {_quarantineReason}";
                return false;
            }

            if (expectedEpoch != _writeEpoch)
            {
                reason = "The manual-financial write request belongs to a stale workflow state.";
                return false;
            }

            writeEpoch = checked(++_writeEpoch);
            reason = string.Empty;
            return true;
        }
    }

    internal string Quarantine(string reason)
    {
        lock (_gate)
        {
            _writesQuarantined = true;
            _quarantineReason ??= reason;
            return _quarantineReason;
        }
    }
}

/// <summary>
/// C#-authoritative migration of Form/GraphE.cs. Web callers receive opaque row,
/// stock and action ids; fixed Core contracts own INPUT_* identities, optimistic
/// row versions and scene codes. Every write request is one-shot. An uncertain
/// outcome latches a process-lifetime write quarantine and is never retried.
/// </summary>
public sealed class LegacyManualFinancialWorkflow
{
    public const int MaximumListResults =
        LegacyManualFinancialScreenService.MaximumResults;
    public const int MaximumStockResults = LegacyStockSearchService.MaximumResults;

    private const string ValidationRowVersion =
        "0000000000000000000000000000000000000000000000000000000000000000";
    private const string ValidationStockCode = "VALIDATION1";

    private static readonly IReadOnlyList<LegacyManualFinancialScreenDefinition>
        RegisteredScreens = CreateDefinitions();
    private static readonly IReadOnlyDictionary<ManualFinancialScreenKind,
        LegacyManualFinancialScreenDefinition> ScreensByKind =
        new ReadOnlyDictionary<ManualFinancialScreenKind, LegacyManualFinancialScreenDefinition>(
            RegisteredScreens.ToDictionary(definition => definition.Screen));
    private static readonly IReadOnlyDictionary<string, LegacyManualFinancialScreenDefinition>
        ScreensByActionId =
        new ReadOnlyDictionary<string, LegacyManualFinancialScreenDefinition>(
            RegisteredScreens.ToDictionary(
                definition => definition.ActionId,
                StringComparer.Ordinal));
    private static readonly LegacyManualFinancialWriteSafetyState ProcessWriteSafety = new();

    private readonly IManualFinancialScreenService _screenService;
    private readonly IManualFinancialRawScreenService? _rawScreenService;
    private readonly IStockSearchService _stockSearchService;
    private readonly LegacyManualFinancialWriteSafetyState _writeSafety;
    private readonly object _authority = new();

    static LegacyManualFinancialWorkflow()
    {
        ValidateDefinitions(RegisteredScreens);
    }

    public LegacyManualFinancialWorkflow(
        IManualFinancialScreenService screenService,
        IStockSearchService stockSearchService)
        : this(screenService, stockSearchService, ProcessWriteSafety)
    {
    }

    internal LegacyManualFinancialWorkflow(
        IManualFinancialScreenService screenService,
        IStockSearchService stockSearchService,
        LegacyManualFinancialWriteSafetyState writeSafety)
    {
        _screenService = screenService ?? throw new ArgumentNullException(nameof(screenService));
        _rawScreenService = screenService as IManualFinancialRawScreenService;
        _stockSearchService = stockSearchService ??
            throw new ArgumentNullException(nameof(stockSearchService));
        _writeSafety = writeSafety ?? throw new ArgumentNullException(nameof(writeSafety));
    }

    public static IReadOnlyList<LegacyManualFinancialScreenDefinition> Screens =>
        RegisteredScreens;

    public static LegacyManualFinancialScreenDefinition GetScreen(
        ManualFinancialScreenKind screen) =>
        ScreensByKind.TryGetValue(screen, out var definition)
            ? definition
            : throw new ArgumentOutOfRangeException(nameof(screen));

    public LegacyManualFinancialWorkflowSnapshot CreateInitial(
        ManualFinancialScreenKind screen) =>
        new(_authority, 0, GetScreen(screen))
        {
            WriteEpoch = _writeSafety.CurrentEpoch,
            WritesQuarantined = IsQuarantined(out var reason),
            LastMessage = reason
        };

    public async Task<LegacyManualFinancialWorkflowSnapshot> SearchAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string query = "",
        CancellationToken cancellationToken = default)
    {
        snapshot = RequireOwned(snapshot);
        var normalizedQuery = NormalizeQuery(query, allowEmpty: true);
        var result = await _screenService.SearchAsync(
            snapshot.Screen,
            normalizedQuery,
            MaximumListResults,
            cancellationToken).ConfigureAwait(false);
        if (result is null || result.Screen != snapshot.Screen ||
            !string.Equals(result.Query, normalizedQuery, StringComparison.Ordinal) ||
            result.Items is null || result.Items.Count > MaximumListResults ||
            result.UnreadableStockNames is null ||
            result.RawItems is null ||
            (result.RawItemsComplete
                ? result.RawItems.Count > MaximumListResults
                : result.Items.Count + result.UnreadableStockNames.Count > MaximumListResults) ||
            result.RejectedItemCount is < 0 or > MaximumListResults ||
            result.UnreadableStockNames.Count > result.RejectedItemCount)
        {
            throw InvalidData("The manual-financial list response is not correlated to its request.");
        }

        var rows = new List<LegacyManualFinancialListRow>(result.RawItemsComplete
            ? result.RawItems.Count
            : result.Items.Count + result.UnreadableStockNames.Count);
        if (result.RawItemsComplete)
        {
            foreach (var item in result.RawItems)
            {
                var validated = ValidateRawSnapshot(
                    item,
                    snapshot.Screen,
                    "raw list response");
                rows.Add(new LegacyManualFinancialListRow(
                    NewOpaqueId("mf-row"),
                    validated));
            }
        }
        else
        {
            foreach (var item in result.Items)
            {
                var validated = ValidateSnapshot(item, snapshot.Screen, "list response");
                rows.Add(new LegacyManualFinancialListRow(NewOpaqueId("mf-row"), validated));
            }
            foreach (var stockName in result.UnreadableStockNames)
            {
                rows.Add(new LegacyManualFinancialListRow(
                    NewOpaqueId("mf-row"),
                    ValidateListStockName(stockName)));
            }
        }

        rows.Sort(static (left, right) =>
            StringComparer.Ordinal.Compare(left.StockName, right.StockName));

        var writesQuarantined = IsQuarantined(out var reason);
        var hiddenUnreadableCount =
            result.RejectedItemCount - result.UnreadableStockNames.Count;
        var listMessages = new List<string>();
        if (result.RejectedItemCount > 0)
        {
            listMessages.Add(hiddenUnreadableCount == 0
                ? $"기존 원문 형식 데이터 {result.RejectedItemCount}행도 " +
                    "목록에 표시되며 선택·편집할 수 있습니다."
                : $"기존 원문 형식 데이터 {result.UnreadableStockNames.Count}행은 " +
                    $"목록에 표시했고, 이름을 읽을 수 없는 {hiddenUnreadableCount}행은 제외했습니다.");
        }
        if (result.IsTruncated)
        {
            listMessages.Add(
                $"검색 결과가 {MaximumListResults}행을 초과해 일부만 표시됩니다.");
        }
        if (!string.IsNullOrWhiteSpace(reason))
        {
            listMessages.Add(reason);
        }
        var listMessage = listMessages.Count == 0
            ? null
            : string.Join(' ', listMessages);

        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            Query = normalizedQuery,
            FindQuery = string.Empty,
            NameSortDirection = LegacyManualFinancialNameSortDirection.Ascending,
            Rows = Array.AsReadOnly(rows.ToArray()),
            ListIsTruncated = result.IsTruncated,
            ListRejectedItemCount = result.RejectedItemCount,
            SelectedRowId = null,
            Detail = null,
            RawDetail = null,
            IsDetailFresh = false,
            IsRawDetailFresh = false,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.ListLoaded,
            WritesQuarantined = writesQuarantined,
            LastMessage = listMessage,
            LastRequestId = null,
            LastReceipt = null
        };
    }

    public LegacyManualFinancialWorkflowSnapshot ToggleNameSort(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        long expectedRevision)
    {
        snapshot = RequireCurrentRevision(snapshot, expectedRevision);
        var direction = snapshot.NameSortDirection ==
            LegacyManualFinancialNameSortDirection.Ascending
                ? LegacyManualFinancialNameSortDirection.Descending
                : LegacyManualFinancialNameSortDirection.Ascending;
        var rows = SortRows(snapshot.Rows, direction);
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            Rows = rows,
            NameSortDirection = direction,
            LastMessage = null
        };
    }

    public LegacyManualFinancialWorkflowSnapshot BeginLoad(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string resultId)
    {
        snapshot = RequireOwned(snapshot);
        var isCurrentRow = !string.IsNullOrWhiteSpace(resultId) &&
            snapshot.Rows.Any(row => string.Equals(
                row.ResultId,
                resultId,
                StringComparison.Ordinal));
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            SelectedRowId = isCurrentRow ? resultId : null,
            Detail = null,
            RawDetail = null,
            IsDetailFresh = false,
            IsRawDetailFresh = false,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.ListLoaded,
            LastMessage = null
        };
    }

    public LegacyManualFinancialWorkflowSnapshot FindByName(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string query,
        LegacyManualFinancialFindDirection direction,
        long expectedRevision)
    {
        snapshot = RequireCurrentRevision(snapshot, expectedRevision);
        if (!Enum.IsDefined(direction))
        {
            throw new ArgumentOutOfRangeException(nameof(direction));
        }

        var normalizedQuery = NormalizeQuery(query, allowEmpty: false);
        var selectedIndex = snapshot.SelectedRowId is null
            ? -1
            : snapshot.Rows.ToList().FindIndex(row => string.Equals(
                row.ResultId,
                snapshot.SelectedRowId,
                StringComparison.Ordinal));
        var matchIndex = -1;
        if (direction == LegacyManualFinancialFindDirection.Down)
        {
            for (var index = selectedIndex + 1; index < snapshot.Rows.Count; index++)
            {
                if (HangulSearchMatcher.IsMatch(
                        snapshot.Rows[index].StockName,
                        normalizedQuery))
                {
                    matchIndex = index;
                    break;
                }
            }
        }
        else if (selectedIndex >= 0)
        {
            for (var index = selectedIndex - 1; index >= 0; index--)
            {
                if (HangulSearchMatcher.IsMatch(
                        snapshot.Rows[index].StockName,
                        normalizedQuery))
                {
                    matchIndex = index;
                    break;
                }
            }
        }

        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            FindQuery = normalizedQuery,
            SelectedRowId = matchIndex >= 0
                ? snapshot.Rows[matchIndex].ResultId
                : snapshot.SelectedRowId,
            Detail = matchIndex >= 0 ? null : snapshot.Detail,
            RawDetail = matchIndex >= 0 ? null : snapshot.RawDetail,
            IsDetailFresh = matchIndex < 0 && snapshot.IsDetailFresh,
            IsRawDetailFresh = matchIndex < 0 && snapshot.IsRawDetailFresh,
            StockQuery = matchIndex >= 0 ? string.Empty : snapshot.StockQuery,
            StockCandidates = matchIndex >= 0
                ? Array.Empty<LegacyManualFinancialStockRow>()
                : snapshot.StockCandidates,
            StockSearchIsTruncated = matchIndex < 0 && snapshot.StockSearchIsTruncated,
            SelectedStockResultId = matchIndex >= 0
                ? null
                : snapshot.SelectedStockResultId,
            Verified = matchIndex >= 0 ? null : snapshot.Verified,
            VerifiedForRowVersion = matchIndex >= 0
                ? null
                : snapshot.VerifiedForRowVersion,
            Status = matchIndex >= 0
                ? LegacyManualFinancialWorkflowStatus.ListLoaded
                : snapshot.Status,
            LastMessage = matchIndex >= 0
                ? null
                : "No further matching stock was found."
        };
    }

    public async Task<LegacyManualFinancialWorkflowSnapshot> LoadAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string resultId,
        CancellationToken cancellationToken = default)
    {
        snapshot = RequireOwned(snapshot);
        var row = FindRow(snapshot.Rows, resultId);
        var identity = new ManualFinancialIdentity(snapshot.Screen, row.StockName);
        ManualFinancialSnapshot? validated;
        ManualFinancialRawSnapshot? validatedRaw = null;
        IReadOnlyList<LegacyManualFinancialListRow> rows;
        if (row.RawSnapshot is not null && _rawScreenService is not null)
        {
            var raw = await _rawScreenService.GetRawAsync(
                row.RawSnapshot,
                cancellationToken).ConfigureAwait(false);
            validatedRaw = ValidateRawSnapshot(raw, snapshot.Screen, "raw detail response");
            if (!EqualsIdentity(identity, validatedRaw.Record.Identity))
            {
                throw InvalidData(
                    "The raw manual-financial detail response is not correlated to its request.");
            }

            validated = validatedRaw.StrictSnapshot;
            rows = ReplaceRow(snapshot.Rows, resultId, validatedRaw);
        }
        else
        {
            var detail = await _screenService.GetAsync(
                identity,
                cancellationToken).ConfigureAwait(false);
            validated = ValidateSnapshot(detail, snapshot.Screen, "detail response");
            if (!EqualsIdentity(identity, validated.Record.Identity))
            {
                throw InvalidData(
                    "The manual-financial detail response is not correlated to its request.");
            }

            rows = ReplaceRow(snapshot.Rows, resultId, validated);
        }

        var writesQuarantined = IsQuarantined(out var reason);
        var message = reason ?? (validated is null
            ? "기존 GraphE 저장값을 원문 그대로 불러왔습니다. 송출 시에는 장면 데이터 검증을 다시 수행합니다."
            : null);
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            Rows = rows,
            SelectedRowId = resultId,
            Detail = validated,
            RawDetail = validatedRaw,
            IsDetailFresh = validated is not null,
            IsRawDetailFresh = validatedRaw is not null,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.DetailLoaded,
            WritesQuarantined = writesQuarantined,
            LastMessage = message,
            LastRequestId = null,
            LastReceipt = null
        };
    }

    public LegacyManualFinancialWorkflowSnapshot BeginCreate(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        snapshot = RequireOwned(snapshot);
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            SelectedRowId = null,
            Detail = null,
            RawDetail = null,
            IsDetailFresh = false,
            IsRawDetailFresh = false,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.ListLoaded,
            WritesQuarantined = IsQuarantined(out var reason),
            LastMessage = reason,
            LastRequestId = null,
            LastReceipt = null
        };
    }

    public async Task<LegacyManualFinancialWorkflowSnapshot> SearchStocksAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string stockName,
        CancellationToken cancellationToken = default)
    {
        snapshot = RequireOwned(snapshot);
        var normalizedName = NormalizeQuery(stockName, allowEmpty: true);
        // Form/GraphE.cs returned immediately from both the button and Enter
        // handlers when the stock-search text box was empty. Preserve the
        // complete editor/candidate state and avoid touching the provider.
        if (normalizedName.Length == 0)
        {
            return snapshot;
        }

        var result = await _stockSearchService.SearchAsync(
            normalizedName,
            MaximumStockResults,
            cancellationToken).ConfigureAwait(false);
        if (result is null ||
            !string.Equals(result.Query, normalizedName, StringComparison.Ordinal) ||
            result.Items is null || result.Items.Count > MaximumStockResults)
        {
            throw InvalidData("The stock-search response is not correlated to its request.");
        }

        var candidates = new List<LegacyManualFinancialStockRow>(result.Items.Count);
        foreach (var item in result.Items)
        {
            ValidateStockItem(item);
            candidates.Add(new LegacyManualFinancialStockRow(
                NewOpaqueId("mf-stock"),
                item with { }));
        }

        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            StockQuery = normalizedName,
            StockCandidates = Array.AsReadOnly(candidates.ToArray()),
            StockSearchIsTruncated = result.IsTruncated,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.StockCandidatesLoaded,
            WritesQuarantined = IsQuarantined(out var reason),
            LastMessage = reason,
            LastRequestId = null,
            LastReceipt = null
        };
    }

    public LegacyManualFinancialWorkflowSnapshot SelectStock(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string stockResultId)
    {
        snapshot = RequireOwned(snapshot);
        if (string.IsNullOrWhiteSpace(stockResultId))
        {
            return snapshot;
        }

        var row = FindStock(snapshot.StockCandidates, stockResultId);
        var identity = snapshot.RawDetail?.Record.Identity ??
            snapshot.Detail?.Record.Identity ??
            new ManualFinancialIdentity(snapshot.Screen, row.Item.Name);
        var candidates = snapshot.StockCandidates.Select(candidate => candidate.Item).ToArray();
        var verified = ManualFinancialCutContracts.VerifyStock(
            identity,
            candidates,
            row.Item.Market,
            row.Item.Code);
        var verifiedForRowVersion = snapshot.SelectedRowVersion;

        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            SelectedStockResultId = stockResultId,
            Verified = verified,
            VerifiedForRowVersion = verifiedForRowVersion,
            Status = LegacyManualFinancialWorkflowStatus.StockVerified,
            WritesQuarantined = IsQuarantined(out var reason),
            LastMessage = reason,
            LastRequestId = null,
            LastReceipt = null
        };
    }

    public LegacyManualFinancialCreateRequest CreateCreateRequest(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialRecord record)
    {
        snapshot = RequireWritable(snapshot);
        if (snapshot.Detail is not null)
        {
            throw new InvalidOperationException("BeginCreate must be used before creating a new record.");
        }

        var verified = snapshot.Verified ?? throw new InvalidOperationException(
            "An exact stock-master row must be verified before creating a record.");
        var validated = ValidateRecord(record, snapshot.Screen, verified, nameof(record));
        return new LegacyManualFinancialCreateRequest(
            _authority,
            snapshot.Revision,
            snapshot.WriteEpoch,
            validated,
            verified);
    }

    public LegacyManualFinancialUpdateRequest CreateUpdateRequest(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialRecord replacement)
    {
        snapshot = RequireWritable(snapshot);
        var expected = RequireFreshDetail(snapshot);
        var verified = RequireVerifiedForDetail(snapshot, expected);
        var validated = ValidateRecord(replacement, snapshot.Screen, verified, nameof(replacement));
        if (!EqualsIdentity(expected.Record.Identity, validated.Identity))
        {
            throw new ArgumentException(
                "A manual-financial update cannot rename or change screens.",
                nameof(replacement));
        }

        return new LegacyManualFinancialUpdateRequest(
            _authority,
            snapshot.Revision,
            snapshot.WriteEpoch,
            expected,
            validated,
            verified);
    }

    public LegacyManualFinancialRawUpdateRequest CreateRawUpdateRequest(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialRawRecord replacement)
    {
        snapshot = RequireWritable(snapshot);
        var expected = RequireFreshRawDetail(snapshot);
        var validated = ValidateRawRecord(replacement, snapshot.Screen, nameof(replacement));
        if (!EqualsIdentity(expected.Record.Identity, validated.Identity))
        {
            throw new ArgumentException(
                "A raw manual-financial update cannot rename or change screens.",
                nameof(replacement));
        }

        return new LegacyManualFinancialRawUpdateRequest(
            _authority,
            snapshot.Revision,
            snapshot.WriteEpoch,
            expected,
            validated);
    }

    public LegacyManualFinancialWriteRequest CreateDeleteRequest(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        snapshot = RequireWritable(snapshot);
        if (snapshot.IsRawDetailFresh && snapshot.RawDetail is not null)
        {
            return new LegacyManualFinancialRawDeleteRequest(
                _authority,
                snapshot.Revision,
                snapshot.WriteEpoch,
                snapshot.RawDetail);
        }

        var expected = RequireFreshDetail(snapshot);
        return new LegacyManualFinancialDeleteRequest(
            _authority,
            snapshot.Revision,
            snapshot.WriteEpoch,
            expected);
    }

    public LegacyManualFinancialDeleteAllRequest CreateDeleteAllRequest(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string confirmationToken)
    {
        snapshot = RequireWritable(snapshot);
        if (!string.Equals(
                confirmationToken,
                snapshot.Definition.DeleteAllConfirmationToken,
                StringComparison.Ordinal))
        {
            throw new ArgumentException(
                "The delete-all confirmation token is invalid.",
                nameof(confirmationToken));
        }

        return new LegacyManualFinancialDeleteAllRequest(
            _authority,
            snapshot.Revision,
            snapshot.WriteEpoch,
            snapshot.Definition);
    }

    public async Task<LegacyManualFinancialWorkflowSnapshot> ExecuteAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        CancellationToken cancellationToken = default)
    {
        snapshot = RequireWritable(snapshot);
        ArgumentNullException.ThrowIfNull(request);
        if (!ReferenceEquals(request.Authority, _authority) ||
            request.SourceRevision != snapshot.Revision ||
            request.SourceWriteEpoch != snapshot.WriteEpoch ||
            request.Screen != snapshot.Screen)
        {
            throw new ArgumentException(
                "The manual-financial write request is stale or belongs to another workflow state.",
                nameof(request));
        }

        cancellationToken.ThrowIfCancellationRequested();
        if (!await _writeSafety.WriteGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
        {
            throw new InvalidOperationException("Another manual-financial write is already in progress.");
        }

        try
        {
            snapshot = RequireWritable(snapshot);
            if (!_writeSafety.TryBeginWrite(
                    snapshot.WriteEpoch,
                    out var writeEpoch,
                    out var rejectionReason))
            {
                throw new InvalidOperationException(rejectionReason);
            }

            ManualFinancialMutationReceipt receipt;
            try
            {
                receipt = request switch
                {
                    LegacyManualFinancialCreateRequest create =>
                        await _screenService.CreateAsync(
                            create.Record,
                            cancellationToken).ConfigureAwait(false),
                    LegacyManualFinancialUpdateRequest update =>
                        await _screenService.UpdateAsync(
                            update.Expected,
                            update.Replacement,
                            cancellationToken).ConfigureAwait(false),
                    LegacyManualFinancialRawUpdateRequest update =>
                        await RequireRawService().UpdateRawAsync(
                            update.Expected,
                            update.Replacement,
                            cancellationToken).ConfigureAwait(false),
                    LegacyManualFinancialDeleteRequest delete =>
                        await _screenService.DeleteAsync(
                            delete.Expected,
                            cancellationToken).ConfigureAwait(false),
                    LegacyManualFinancialRawDeleteRequest delete =>
                        await RequireRawService().DeleteRawAsync(
                            delete.Expected,
                            cancellationToken).ConfigureAwait(false),
                    LegacyManualFinancialDeleteAllRequest =>
                        await _screenService.DeleteAllAsync(
                            snapshot.Screen,
                            cancellationToken).ConfigureAwait(false),
                    _ => throw new ArgumentOutOfRangeException(nameof(request))
                };
            }
            catch (ManualFinancialMutationException exception)
            {
                return exception.OutcomeUnknown
                    ? Quarantine(snapshot, request, writeEpoch, exception.Message)
                    : Rejected(snapshot, request, writeEpoch, exception.Message);
            }
            catch (Exception exception)
            {
                return Quarantine(
                    snapshot,
                    request,
                    writeEpoch,
                    $"The {request.Kind} outcome is unknown: {exception.Message}");
            }

            if (!IsCorrelatedReceipt(receipt, request))
            {
                return Quarantine(
                    snapshot,
                    request,
                    writeEpoch,
                    "The write response was malformed or not correlated; its outcome is unknown.");
            }

            return request switch
            {
                LegacyManualFinancialCreateRequest or LegacyManualFinancialUpdateRequest =>
                    await CompleteSaveAsync(
                            snapshot,
                            request,
                            receipt,
                            writeEpoch,
                            cancellationToken)
                        .ConfigureAwait(false),
                LegacyManualFinancialRawUpdateRequest rawUpdate =>
                    await CompleteRawSaveAsync(
                            snapshot,
                            rawUpdate,
                            receipt,
                            writeEpoch,
                            cancellationToken)
                        .ConfigureAwait(false),
                LegacyManualFinancialDeleteRequest or LegacyManualFinancialRawDeleteRequest =>
                    CompleteDelete(snapshot, request, receipt, writeEpoch),
                LegacyManualFinancialDeleteAllRequest =>
                    CompleteDeleteAll(snapshot, request, receipt, writeEpoch),
                _ => throw new ArgumentOutOfRangeException(nameof(request))
            };
        }
        finally
        {
            _writeSafety.WriteGate.Release();
        }
    }

    public LegacyManualFinancialPlaylistDraft MaterializePlaylistDraft(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        string actionId)
    {
        snapshot = RequireOwned(snapshot);
        if (!ScreensByActionId.TryGetValue(actionId, out var definition) ||
            definition.Screen != snapshot.Screen)
        {
            throw new ArgumentException("The manual-financial action id is invalid.", nameof(actionId));
        }

        if (IsQuarantined(out _))
        {
            throw new InvalidOperationException(
                "Playlist materialization is blocked while a write outcome is unknown.");
        }

        var identity = snapshot.SelectedIdentity ?? throw new InvalidOperationException(
            "A fresh manual-financial detail read is required.");
        var verified = RequireVerifiedForSelectedDetail(snapshot, identity);
        var raw = snapshot.IsRawDetailFresh ? snapshot.RawDetail : null;
        var detail = snapshot.IsDetailFresh ? snapshot.Detail : null;
        if (raw is null && detail is null)
        {
            throw new InvalidOperationException(
                "A fresh manual-financial detail read is required.");
        }

        var cut = ManualFinancialCutContracts.CreateSelection(identity, verified);
        return new LegacyManualFinancialPlaylistDraft(
            definition.ActionId,
            definition.BuilderKey,
            definition.Code,
            definition.Aliases,
            identity.StockName,
            definition.Label,
            "manual-financial",
            MarketName(verified.Market),
            verified.Code,
            verified.Market is StockMarket.NxtKospi or StockMarket.NxtKosdaq,
            cut,
            detail is null ? null : CloneSnapshot(detail),
            raw is null ? null : CloneRawSnapshot(raw));
    }

    private async Task<LegacyManualFinancialWorkflowSnapshot> CompleteSaveAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        ManualFinancialMutationReceipt receipt,
        long writeEpoch,
        CancellationToken cancellationToken)
    {
        var identity = new ManualFinancialIdentity(request.Screen, request.StockName);
        ManualFinancialSnapshot detail;
        try
        {
            detail = ValidateSnapshot(
                await _screenService.GetAsync(identity, cancellationToken).ConfigureAwait(false),
                request.Screen,
                "post-save read-back");
            if (!EqualsIdentity(identity, detail.Record.Identity))
            {
                throw InvalidData("The post-save read-back is not correlated to its write.");
            }
        }
        catch (Exception exception)
        {
            return snapshot with
            {
                Revision = checked(snapshot.Revision + 1),
                WriteEpoch = writeEpoch,
                SelectedRowId = null,
                Detail = null,
                RawDetail = null,
                IsDetailFresh = false,
                IsRawDetailFresh = false,
                Verified = null,
                VerifiedForRowVersion = null,
                Status = LegacyManualFinancialWorkflowStatus.ReadBackRequired,
                WritesQuarantined = false,
                LastMessage = $"The write committed, but a fresh read-back is required: {exception.Message}",
                LastRequestId = request.RequestId,
                LastReceipt = receipt
            };
        }

        var rows = UpsertRow(snapshot, detail, out var resultId);
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Rows = rows,
            SelectedRowId = resultId,
            Detail = detail,
            RawDetail = null,
            IsDetailFresh = true,
            IsRawDetailFresh = false,
            SelectedStockResultId = snapshot.SelectedStockResultId,
            Verified = request.VerifiedStock,
            VerifiedForRowVersion = detail.RowVersion,
            Status = LegacyManualFinancialWorkflowStatus.WriteCommitted,
            WritesQuarantined = false,
            LastMessage = null,
            LastRequestId = request.RequestId,
            LastReceipt = receipt
        };
    }

    private async Task<LegacyManualFinancialWorkflowSnapshot> CompleteRawSaveAsync(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialRawUpdateRequest request,
        ManualFinancialMutationReceipt receipt,
        long writeEpoch,
        CancellationToken cancellationToken)
    {
        ManualFinancialRawSnapshot raw;
        try
        {
            raw = ValidateRawSnapshot(
                await RequireRawService().GetRawAsync(
                    request.Expected,
                    cancellationToken).ConfigureAwait(false),
                request.Screen,
                "raw post-save read-back");
            if (!EqualsIdentity(request.Replacement.Identity, raw.Record.Identity))
            {
                throw InvalidData(
                    "The raw post-save read-back is not correlated to its write.");
            }
        }
        catch (Exception exception)
        {
            return snapshot with
            {
                Revision = checked(snapshot.Revision + 1),
                WriteEpoch = writeEpoch,
                SelectedRowId = null,
                Detail = null,
                RawDetail = null,
                IsDetailFresh = false,
                IsRawDetailFresh = false,
                Verified = null,
                VerifiedForRowVersion = null,
                Status = LegacyManualFinancialWorkflowStatus.ReadBackRequired,
                WritesQuarantined = false,
                LastMessage =
                    $"The write committed, but a fresh raw read-back is required: {exception.Message}",
                LastRequestId = request.RequestId,
                LastReceipt = receipt
            };
        }

        var resultId = snapshot.SelectedRowId ?? NewOpaqueId("mf-row");
        var rows = snapshot.Rows.Any(row => string.Equals(
                row.ResultId,
                resultId,
                StringComparison.Ordinal))
            ? ReplaceRow(snapshot.Rows, resultId, raw)
            : SortRows(
                snapshot.Rows.Append(new LegacyManualFinancialListRow(resultId, raw)),
                snapshot.NameSortDirection);
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Rows = rows,
            SelectedRowId = resultId,
            Detail = raw.StrictSnapshot,
            RawDetail = raw,
            IsDetailFresh = raw.StrictSnapshot is not null,
            IsRawDetailFresh = true,
            Verified = snapshot.Verified,
            VerifiedForRowVersion = snapshot.Verified is null ? null : raw.RowVersion,
            Status = LegacyManualFinancialWorkflowStatus.WriteCommitted,
            WritesQuarantined = false,
            LastMessage = raw.IsPlayoutReady
                ? null
                : "기존 GraphE 원문 값으로 저장했습니다. 송출 시 장면 데이터 검증을 다시 수행합니다.",
            LastRequestId = request.RequestId,
            LastReceipt = receipt
        };
    }

    private static LegacyManualFinancialWorkflowSnapshot CompleteDelete(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        ManualFinancialMutationReceipt receipt,
        long writeEpoch)
    {
        var selectedRowId = snapshot.SelectedRowId;
        var rows = snapshot.Rows
            .Where(row => selectedRowId is null || !string.Equals(
                row.ResultId,
                selectedRowId,
                StringComparison.Ordinal))
            .ToArray();
        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Rows = Array.AsReadOnly(rows),
            SelectedRowId = null,
            Detail = null,
            RawDetail = null,
            IsDetailFresh = false,
            IsRawDetailFresh = false,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.WriteCommitted,
            WritesQuarantined = false,
            LastMessage = null,
            LastRequestId = request.RequestId,
            LastReceipt = receipt
        };
    }

    private static LegacyManualFinancialWorkflowSnapshot CompleteDeleteAll(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        ManualFinancialMutationReceipt receipt,
        long writeEpoch) =>
        snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Rows = Array.Empty<LegacyManualFinancialListRow>(),
            ListIsTruncated = false,
            ListRejectedItemCount = 0,
            SelectedRowId = null,
            Detail = null,
            RawDetail = null,
            IsDetailFresh = false,
            IsRawDetailFresh = false,
            StockQuery = string.Empty,
            StockCandidates = Array.Empty<LegacyManualFinancialStockRow>(),
            StockSearchIsTruncated = false,
            SelectedStockResultId = null,
            Verified = null,
            VerifiedForRowVersion = null,
            Status = LegacyManualFinancialWorkflowStatus.WriteCommitted,
            WritesQuarantined = false,
            LastMessage = null,
            LastRequestId = request.RequestId,
            LastReceipt = receipt
        };

    private LegacyManualFinancialWorkflowSnapshot Rejected(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        long writeEpoch,
        string message) =>
        snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Status = LegacyManualFinancialWorkflowStatus.WriteRejected,
            WritesQuarantined = false,
            LastMessage = message,
            LastRequestId = request.RequestId,
            LastReceipt = null
        };

    private LegacyManualFinancialWorkflowSnapshot Quarantine(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        LegacyManualFinancialWriteRequest request,
        long writeEpoch,
        string message)
    {
        message = _writeSafety.Quarantine(message);

        return snapshot with
        {
            Revision = checked(snapshot.Revision + 1),
            WriteEpoch = writeEpoch,
            Status = LegacyManualFinancialWorkflowStatus.OutcomeUnknown,
            WritesQuarantined = true,
            LastMessage = message,
            LastRequestId = request.RequestId,
            LastReceipt = null
        };
    }

    private LegacyManualFinancialWorkflowSnapshot RequireOwned(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        if (!ReferenceEquals(snapshot.Authority, _authority) ||
            !ReferenceEquals(snapshot.Definition, GetScreen(snapshot.Screen)))
        {
            throw new ArgumentException(
                "The workflow snapshot belongs to another manual-financial workflow.",
                nameof(snapshot));
        }

        if (!_writeSafety.IsCurrent(snapshot.WriteEpoch))
        {
            throw new InvalidOperationException(
                "The manual-financial workflow snapshot is stale after a write attempt.");
        }

        return snapshot;
    }

    private LegacyManualFinancialWorkflowSnapshot RequireCurrentRevision(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        long expectedRevision)
    {
        snapshot = RequireOwned(snapshot);
        if (expectedRevision < 0 || snapshot.Revision != expectedRevision)
        {
            throw new InvalidOperationException(
                "The manual-financial list command belongs to a stale view generation.");
        }

        return snapshot;
    }

    private LegacyManualFinancialWorkflowSnapshot RequireWritable(
        LegacyManualFinancialWorkflowSnapshot snapshot)
    {
        snapshot = RequireOwned(snapshot);
        if (!_screenService.CanMutate)
        {
            throw new InvalidOperationException(
                "Manual-financial screens are configured for read-only access.");
        }

        if (IsQuarantined(out var reason))
        {
            throw new InvalidOperationException(
                $"Manual-financial writes are quarantined: {reason}");
        }

        return snapshot;
    }

    private bool IsQuarantined(out string? reason) =>
        _writeSafety.IsQuarantined(out reason);

    private IManualFinancialRawScreenService RequireRawService() =>
        _rawScreenService ?? throw new InvalidOperationException(
            "Raw GraphE row compatibility is not available from this data service.");

    private static ManualFinancialSnapshot RequireFreshDetail(
        LegacyManualFinancialWorkflowSnapshot snapshot) =>
        snapshot.IsDetailFresh && snapshot.Detail is not null
            ? snapshot.Detail
            : throw new InvalidOperationException(
                "A fresh manual-financial detail read is required.");

    private static ManualFinancialRawSnapshot RequireFreshRawDetail(
        LegacyManualFinancialWorkflowSnapshot snapshot) =>
        snapshot.IsRawDetailFresh && snapshot.RawDetail is not null
            ? snapshot.RawDetail
            : throw new InvalidOperationException(
                "A fresh raw manual-financial detail read is required.");

    private static VerifiedManualFinancialStock RequireVerifiedForDetail(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialSnapshot detail)
    {
        if (snapshot.Verified is null ||
            !string.Equals(
                snapshot.VerifiedForRowVersion,
                detail.RowVersion,
                StringComparison.Ordinal) ||
            !string.Equals(
                snapshot.Verified.Name,
                detail.Record.Identity.StockName,
                StringComparison.Ordinal))
        {
            throw new InvalidOperationException(
                "The selected detail requires a fresh exact stock-master verification.");
        }

        return snapshot.Verified;
    }

    private static VerifiedManualFinancialStock RequireVerifiedForSelectedDetail(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialIdentity identity)
    {
        if (snapshot.Verified is null ||
            snapshot.SelectedRowVersion is not { } rowVersion ||
            !string.Equals(
                snapshot.VerifiedForRowVersion,
                rowVersion,
                StringComparison.Ordinal) ||
            !string.Equals(
                snapshot.Verified.Name,
                identity.StockName,
                StringComparison.Ordinal))
        {
            throw new InvalidOperationException(
                "The selected detail requires a fresh exact stock-master verification.");
        }

        return snapshot.Verified;
    }

    private static LegacyManualFinancialListRow FindRow(
        IReadOnlyList<LegacyManualFinancialListRow> rows,
        string resultId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(resultId);
        return rows.SingleOrDefault(row =>
                string.Equals(row.ResultId, resultId, StringComparison.Ordinal)) ??
            throw new ArgumentException("The manual-financial result id is invalid.", nameof(resultId));
    }

    private static LegacyManualFinancialStockRow FindStock(
        IReadOnlyList<LegacyManualFinancialStockRow> rows,
        string resultId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(resultId);
        return rows.SingleOrDefault(row =>
                string.Equals(row.ResultId, resultId, StringComparison.Ordinal)) ??
            throw new ArgumentException("The stock result id is invalid.", nameof(resultId));
    }

    private static IReadOnlyList<LegacyManualFinancialListRow> ReplaceRow(
        IReadOnlyList<LegacyManualFinancialListRow> rows,
        string resultId,
        ManualFinancialSnapshot detail) =>
        Array.AsReadOnly(rows.Select(row =>
                string.Equals(row.ResultId, resultId, StringComparison.Ordinal)
                    ? new LegacyManualFinancialListRow(resultId, detail)
                    : row)
            .ToArray());

    private static IReadOnlyList<LegacyManualFinancialListRow> ReplaceRow(
        IReadOnlyList<LegacyManualFinancialListRow> rows,
        string resultId,
        ManualFinancialRawSnapshot detail) =>
        Array.AsReadOnly(rows.Select(row =>
                string.Equals(row.ResultId, resultId, StringComparison.Ordinal)
                    ? new LegacyManualFinancialListRow(resultId, detail)
                    : row)
            .ToArray());

    private static IReadOnlyList<LegacyManualFinancialListRow> UpsertRow(
        LegacyManualFinancialWorkflowSnapshot snapshot,
        ManualFinancialSnapshot detail,
        out string resultId)
    {
        var existing = snapshot.Rows.FirstOrDefault(row => string.Equals(
            row.StockName,
            detail.Record.Identity.StockName,
            StringComparison.Ordinal));
        resultId = existing?.ResultId ?? NewOpaqueId("mf-row");
        var rows = snapshot.Rows
            .Where(row => !string.Equals(
                row.StockName,
                detail.Record.Identity.StockName,
                StringComparison.Ordinal))
            .ToList();
        if (HangulSearchMatcher.IsMatch(
                detail.Record.Identity.StockName,
                snapshot.Query))
        {
            rows.Add(new LegacyManualFinancialListRow(resultId, detail));
        }

        return SortRows(rows, snapshot.NameSortDirection);
    }

    private static IReadOnlyList<LegacyManualFinancialListRow> SortRows(
        IEnumerable<LegacyManualFinancialListRow> rows,
        LegacyManualFinancialNameSortDirection direction)
    {
        if (!Enum.IsDefined(direction))
        {
            throw new ArgumentOutOfRangeException(nameof(direction));
        }

        var result = rows.ToList();
        result.Sort((left, right) =>
        {
            var compared = StringComparer.Ordinal.Compare(left.StockName, right.StockName);
            return direction == LegacyManualFinancialNameSortDirection.Ascending
                ? compared
                : -compared;
        });
        return Array.AsReadOnly(result.ToArray());
    }

    private static bool IsCorrelatedReceipt(
        ManualFinancialMutationReceipt? receipt,
        LegacyManualFinancialWriteRequest request) =>
        receipt is not null &&
        receipt.OperationId != Guid.Empty &&
        receipt.Kind == request.Kind &&
        receipt.Screen == request.Screen &&
        string.Equals(receipt.StockName, request.StockName, StringComparison.Ordinal) &&
        (request.Kind == ManualFinancialMutationKind.DeleteAll
            ? receipt.AffectedRows >= 0
            : receipt.AffectedRows == 1);

    private static ManualFinancialRawSnapshot ValidateRawSnapshot(
        ManualFinancialRawSnapshot? snapshot,
        ManualFinancialScreenKind screen,
        string source)
    {
        try
        {
            ArgumentNullException.ThrowIfNull(snapshot);
            var record = ValidateRawRecord(snapshot.Record, screen, nameof(snapshot));
            if (snapshot.RowVersion is null || snapshot.RowVersion.Length != 64 ||
                snapshot.RowVersion.Any(static character =>
                    !char.IsAsciiDigit(character) && character is not (>= 'A' and <= 'F')) ||
                snapshot.Handle is null)
            {
                throw new ArgumentException("The raw row envelope is invalid.");
            }

            ManualFinancialSnapshot? strict = null;
            if (snapshot.StrictSnapshot is not null)
            {
                strict = ValidateSnapshot(snapshot.StrictSnapshot, screen, source);
                if (!EqualsIdentity(strict.Record.Identity, record.Identity) ||
                    !string.Equals(
                        strict.RowVersion,
                        snapshot.RowVersion,
                        StringComparison.Ordinal))
                {
                    throw new ArgumentException(
                        "The strict projection is not correlated to the raw row.");
                }
            }

            return new ManualFinancialRawSnapshot(
                record,
                snapshot.RowVersion,
                snapshot.Handle,
                strict);
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            throw InvalidData($"The manual-financial {source} is invalid: {exception.Message}");
        }
    }

    private static ManualFinancialRawRecord ValidateRawRecord(
        ManualFinancialRawRecord? record,
        ManualFinancialScreenKind screen,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(record, parameterName);
        ArgumentNullException.ThrowIfNull(record.Identity, parameterName);
        if (record.Identity.Screen != screen ||
            record.Identity.StockName is null ||
            record.Identity.StockName.Length is < 1 or > 200 ||
            record.Identity.StockName != record.Identity.StockName.Trim() ||
            record.StorageValues is null)
        {
            throw new ArgumentException(
                "The raw manual-financial record identity is invalid.",
                parameterName);
        }

        var expectedCount = ManualFinancialCutContracts.Get(screen).StorageColumns.Count - 1;
        if (record.StorageValues.Count != expectedCount)
        {
            throw new ArgumentException(
                "The raw manual-financial record field count is invalid.",
                parameterName);
        }

        var values = new string[expectedCount];
        for (var index = 0; index < values.Length; index++)
        {
            var value = record.StorageValues[index];
            if (value is null || value.Length > 200 ||
                value.Any(static character =>
                    char.IsSurrogate(character) || character == '\uFFFD'))
            {
                throw new ArgumentException(
                    "A raw manual-financial field is invalid.",
                    parameterName);
            }

            values[index] = value;
        }

        return new ManualFinancialRawRecord(
            new ManualFinancialIdentity(screen, record.Identity.StockName),
            Array.AsReadOnly(values));
    }

    private static ManualFinancialSnapshot ValidateSnapshot(
        ManualFinancialSnapshot? snapshot,
        ManualFinancialScreenKind screen,
        string source)
    {
        try
        {
            ArgumentNullException.ThrowIfNull(snapshot);
            var copy = CloneSnapshot(snapshot);
            if (copy.Record.Identity.Screen != screen)
            {
                throw new ArgumentException("The response screen does not match.");
            }

            var validationStock = ValidationStock(copy.Record.Identity);
            _ = ManualFinancialCutContracts.CreateSelection(copy, validationStock);
            return copy;
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            throw InvalidData($"The manual-financial {source} is invalid: {exception.Message}");
        }
    }

    private static ManualFinancialRecord ValidateRecord(
        ManualFinancialRecord? record,
        ManualFinancialScreenKind screen,
        VerifiedManualFinancialStock stock,
        string parameterName)
    {
        ArgumentNullException.ThrowIfNull(record, parameterName);
        var copy = CloneRecord(record);
        if (copy.Identity.Screen != screen)
        {
            throw new ArgumentException(
                "The manual-financial record does not match its screen.",
                parameterName);
        }

        try
        {
            _ = ManualFinancialCutContracts.CreateSelection(
                new ManualFinancialSnapshot(copy, ValidationRowVersion),
                stock);
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            throw new ArgumentException(
                $"The manual-financial record is invalid: {exception.Message}",
                parameterName,
                exception);
        }

        return copy;
    }

    private static ManualFinancialSnapshot CloneSnapshot(ManualFinancialSnapshot snapshot) =>
        new(CloneRecord(snapshot.Record), snapshot.RowVersion);

    private static ManualFinancialRawSnapshot CloneRawSnapshot(
        ManualFinancialRawSnapshot snapshot) =>
        new(
            new ManualFinancialRawRecord(
                new ManualFinancialIdentity(
                    snapshot.Record.Identity.Screen,
                    snapshot.Record.Identity.StockName),
                Array.AsReadOnly(snapshot.Record.StorageValues.ToArray())),
            snapshot.RowVersion,
            snapshot.Handle,
            snapshot.StrictSnapshot is null
                ? null
                : CloneSnapshot(snapshot.StrictSnapshot));

    private static ManualFinancialRecord CloneRecord(ManualFinancialRecord record)
    {
        ArgumentNullException.ThrowIfNull(record);
        ArgumentNullException.ThrowIfNull(record.Identity);
        var identity = new ManualFinancialIdentity(
            record.Identity.Screen,
            record.Identity.StockName);
        return record switch
        {
            ManualRevenueCompositionRecord revenue =>
                new ManualRevenueCompositionRecord(
                    identity,
                    revenue.BaseDate,
                    Array.AsReadOnly((revenue.Slices ?? throw new ArgumentException(
                            "Revenue slices are required.",
                            nameof(record)))
                        .Select(slice => slice is null
                            ? null
                            : new ManualRevenueSlice(
                                slice.Label,
                                slice.PercentageText,
                                slice.Percentage))
                        .ToArray())),
            ManualGrowthMetricsRecord growth =>
                new ManualGrowthMetricsRecord(
                    identity,
                    CloneGrowth(growth.SalesGrowth),
                    CloneGrowth(growth.OperatingProfitGrowth),
                    CloneGrowth(growth.NetAssetGrowth),
                    CloneGrowth(growth.NetIncomeGrowth),
                    ClonePeriods(growth.Periods)),
            ManualSalesRecord sales =>
                new ManualSalesRecord(identity, CloneQuarters(sales.Quarters)),
            ManualOperatingProfitRecord profit =>
                new ManualOperatingProfitRecord(identity, CloneQuarters(profit.Quarters)),
            _ => throw new ArgumentException(
                "The manual-financial record type is invalid.",
                nameof(record))
        };
    }

    private static ManualGrowthSeriesValues CloneGrowth(ManualGrowthSeriesValues? values)
    {
        ArgumentNullException.ThrowIfNull(values);
        return new ManualGrowthSeriesValues(
            values.First,
            values.Second,
            values.Third,
            values.Fourth);
    }

    private static ManualGrowthPeriodLabels ClonePeriods(ManualGrowthPeriodLabels? periods)
    {
        ArgumentNullException.ThrowIfNull(periods);
        return new ManualGrowthPeriodLabels(
            periods.First,
            periods.Second,
            periods.Third,
            periods.Fourth);
    }

    private static IReadOnlyList<ManualQuarterValue> CloneQuarters(
        IReadOnlyList<ManualQuarterValue>? quarters)
    {
        ArgumentNullException.ThrowIfNull(quarters);
        return Array.AsReadOnly(quarters.Select(quarter =>
                quarter is null
                    ? throw new ArgumentException("A manual-financial quarter is invalid.")
                    : new ManualQuarterValue(quarter.Quarter, quarter.Value))
            .ToArray());
    }

    private static VerifiedManualFinancialStock ValidationStock(
        ManualFinancialIdentity identity)
    {
        var candidate = new StockSearchItem(
            StockMarket.Kospi,
            DataSourceKind.Oracle,
            identity.StockName,
            ValidationStockCode);
        return ManualFinancialCutContracts.VerifyStock(
            identity,
            [candidate],
            candidate.Market,
            candidate.Code);
    }

    private static void ValidateStockItem(StockSearchItem? item)
    {
        if (item is null)
        {
            throw InvalidData("The stock-search response contains a null item.");
        }

        try
        {
            var identity = new ManualFinancialIdentity(
                ManualFinancialScreenKind.Sales,
                item.Name);
            _ = ManualFinancialCutContracts.VerifyStock(
                identity,
                [item],
                item.Market,
                item.Code);
        }
        catch (Exception exception) when (exception is ArgumentException or InvalidOperationException)
        {
            throw InvalidData($"The stock-search response is invalid: {exception.Message}");
        }
    }

    private static string NormalizeQuery(string value, bool allowEmpty)
    {
        ArgumentNullException.ThrowIfNull(value);
        var normalized = value.Trim();
        if ((!allowEmpty && normalized.Length == 0) || normalized.Length > 64 ||
            normalized.Any(static character =>
                char.IsControl(character) || char.IsSurrogate(character) ||
                character == '\uFFFD'))
        {
            throw new ArgumentException("The search text is invalid.", nameof(value));
        }

        return normalized;
    }

    private static string ValidateListStockName(string value)
    {
        if (value is null || value.Length is < 1 or > 200 ||
            value != value.Trim() ||
            value.Contains('^') ||
            value.Any(static character =>
                char.IsControl(character) || char.IsSurrogate(character) ||
                character == '\uFFFD' ||
                char.GetUnicodeCategory(character) is
                    System.Globalization.UnicodeCategory.Format or
                    System.Globalization.UnicodeCategory.LineSeparator or
                    System.Globalization.UnicodeCategory.ParagraphSeparator))
        {
            throw InvalidData(
                "The manual-financial unreadable-row identity is invalid.");
        }

        return value;
    }

    private static bool EqualsIdentity(
        ManualFinancialIdentity left,
        ManualFinancialIdentity right) =>
        left.Screen == right.Screen &&
        string.Equals(left.StockName, right.StockName, StringComparison.Ordinal);

    private static string MarketName(StockMarket market) => market switch
    {
        StockMarket.Kospi => "KOSPI",
        StockMarket.Kosdaq => "KOSDAQ",
        StockMarket.NxtKospi => "NXT_KOSPI",
        StockMarket.NxtKosdaq => "NXT_KOSDAQ",
        _ => throw new ArgumentOutOfRangeException(nameof(market))
    };

    private static string NewOpaqueId(string prefix) =>
        prefix + "-" + Guid.NewGuid().ToString("N");

    private static LegacyManualFinancialWorkflowDataException InvalidData(string message) =>
        new(message);

    private static IReadOnlyList<LegacyManualFinancialScreenDefinition> CreateDefinitions() =>
        Array.AsReadOnly<LegacyManualFinancialScreenDefinition>(
        [
            new(
                ManualFinancialScreenKind.RevenueComposition,
                "revenue-composition",
                "주요매출 구성",
                "INPUT_PIE",
                "s5076",
                "5076",
                "REVENUE_COMPOSITION",
                "DELETE_ALL_INPUT_PIE"),
            new(
                ManualFinancialScreenKind.GrowthMetrics,
                "growth-metrics",
                "성장성 지표",
                "INPUT_GROW",
                "s5079",
                "5079",
                "GROWTH_METRICS",
                "DELETE_ALL_INPUT_GROW"),
            new(
                ManualFinancialScreenKind.Sales,
                "sales",
                "매출액",
                "INPUT_SELL",
                "s5080",
                "5080",
                "SALES",
                "DELETE_ALL_INPUT_SELL"),
            new(
                ManualFinancialScreenKind.OperatingProfit,
                "operating-profit",
                "영업이익",
                "INPUT_PROFIT",
                "s5081",
                "5081",
                "OPERATING_PROFIT",
                "DELETE_ALL_INPUT_PROFIT")
        ]);

    private static void ValidateDefinitions(
        IReadOnlyList<LegacyManualFinancialScreenDefinition> definitions)
    {
        if (definitions.Count != 4 ||
            definitions.Select(definition => definition.Screen).Distinct().Count() != 4 ||
            definitions.Select(definition => definition.ActionId)
                .Distinct(StringComparer.Ordinal).Count() != 4)
        {
            throw new InvalidOperationException(
                "The manual-financial screen registry is incomplete or ambiguous.");
        }

        foreach (var definition in definitions)
        {
            var core = ManualFinancialCutContracts.Get(definition.Screen);
            if (!string.Equals(core.TableName, definition.TableName, StringComparison.Ordinal) ||
                !string.Equals(core.BuilderKey, definition.BuilderKey, StringComparison.Ordinal) ||
                !string.Equals(core.CutCode, definition.Code, StringComparison.Ordinal) ||
                !string.Equals(
                    core.CanonicalGraphicType,
                    definition.CanonicalGraphicType,
                    StringComparison.Ordinal))
            {
                throw new InvalidOperationException(
                    $"The {definition.Screen} application contract differs from Core.");
            }
        }
    }
}
