using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyManualListScreen
{
    NetSell,
    Vi
}

public enum LegacyManualNetSellField
{
    LeftName,
    LeftAmount,
    RightName,
    RightAmount
}

public enum LegacyManualListMoveDirection
{
    Up,
    Down
}

public sealed record LegacyManualListStoreAvailability(
    bool IsAvailable,
    string Code,
    string? Message);

public sealed record LegacyManualViStoredItem(string Code, string Name);

public sealed record LegacyManualViStoreSnapshot(
    IReadOnlyList<LegacyManualViStoredItem> Items,
    string RowVersion);

public sealed record LegacyManualViWriteReceipt(
    LegacyManualViStoreSnapshot Snapshot,
    bool Persisted);

public sealed record LegacyManualImportReceipt(
    int MarkerVersion,
    string SourceSha256,
    DateTimeOffset ImportedAt,
    int NetSellRowCount,
    int ViItemCount);

public sealed class LegacyManualImportStoreException : Exception
{
    public LegacyManualImportStoreException(string code, bool outcomeUnknown)
        : base("The legacy manual-data import did not complete.")
    {
        Code = code;
        OutcomeUnknown = outcomeUnknown;
    }

    public string Code { get; }

    public bool OutcomeUnknown { get; }
}

/// <summary>
/// Native-only boundary for the operator-owned CP949 files. Implementations choose
/// every path and file name; the WebView can only select a closed screen or opaque row.
/// </summary>
public interface ILegacyManualListStore
{
    LegacyManualListStoreAvailability Availability { get; }

    Task<IReadOnlyList<S5025TrustedManualRow>> ReadNetSellAsync(
        S5025ManualAudience audience,
        CancellationToken cancellationToken = default);

    Task WriteNetSellAsync(
        S5025ManualAudience audience,
        IReadOnlyList<S5025TrustedManualRow> rows,
        CancellationToken cancellationToken = default);

    Task<LegacyManualViStoreSnapshot> ReadViAsync(
        CancellationToken cancellationToken = default);

    Task<LegacyManualViWriteReceipt> WriteViAsync(
        IReadOnlyList<LegacyManualViStoredItem> items,
        CancellationToken cancellationToken = default);

    Task<LegacyManualImportReceipt> ImportAsync(
        CancellationToken cancellationToken = default);
}

public sealed record LegacyManualNetSellRowView(
    string RowId,
    string LeftName,
    string LeftAmount,
    string RightName,
    string RightAmount);

public sealed record LegacyManualViItemView(
    string ItemId,
    int Index,
    string Name);

public sealed record LegacyManualViSearchResultView(
    string ResultId,
    string DisplayName,
    string MarketText);

public sealed record LegacyManualListsSnapshot(
    bool IsOpen,
    LegacyManualListScreen? Screen,
    S5025ManualAudience Audience,
    bool IsAvailable,
    string AvailabilityCode,
    string? AvailabilityMessage,
    bool WritesQuarantined,
    IReadOnlyList<LegacyManualNetSellRowView> NetRows,
    bool NetRowsAreFresh,
    IReadOnlyList<LegacyManualViItemView> ViItems,
    int ViPageCount,
    bool ViItemsAreFresh,
    string SearchQuery,
    IReadOnlyList<LegacyManualViSearchResultView> SearchResults,
    string StatusMessage,
    LegacyOperatorStatusKind StatusKind,
    bool CanSave,
    bool CanMaterializePlaylist)
{
    public string? SelectedSearchResultId { get; init; }

    public bool SearchIsTruncated { get; init; }
}

public sealed record LegacyManualListPlaylistDraft(
    string SceneAlias,
    string MarketText,
    string StockName,
    string GraphicType,
    string Subtype,
    string PageText,
    string RawCutLabel,
    int FadeDuration,
    LegacySceneSelection Selection);

/// <summary>
/// Authoritative FSell/VIList workflow. Draft rows, stock identities, file versions,
/// save verification and playlist materialization remain in C#; browser messages are
/// limited to input intent and opaque identifiers.
/// </summary>
public sealed class LegacyManualListsWorkflow
{
    public const int NetSellRowCount = 5;
    public const int MaximumViItems = 100;
    public const int ViPageSize = 5;
    public const int MaximumPageCount = 20;
    public const int MaximumNetSellValueLength = 256;
    public const int MaximumSearchLength = 100;
    public const int MaximumSearchResults = 10_000;

    private const string ViSnapshotSubject = "@MBN_VI_SNAPSHOT_V1";
    private const string OutcomeUnknownMessage =
        "저장 결과를 확인할 수 없어 수동 목록 쓰기를 격리했습니다. 앱을 다시 시작하고 파일을 확인하세요.";

    private readonly ILegacyManualListStore _store;
    private readonly ILegacyStockLookup _stockLookup;
    private readonly List<S5025TrustedManualRow> _netRows = CreateEmptyNetRows();
    private readonly List<ViDraftItem> _viItems = [];
    private readonly List<ViSearchResult> _searchResults = [];
    private LegacyManualListScreen? _screen;
    private S5025ManualAudience _audience = S5025ManualAudience.Individual;
    private bool _isOpen;
    private bool _netRowsAreFresh;
    private bool _viItemsAreFresh;
    private bool _writesQuarantined;
    private string? _viVersion;
    private string _searchQuery = string.Empty;
    private string? _selectedSearchResultId;
    private bool _searchResultsAreTruncated;
    private string _statusMessage = string.Empty;
    private LegacyOperatorStatusKind _statusKind;
    private long _opaqueSequence;

    public LegacyManualListsWorkflow(
        ILegacyManualListStore store,
        ILegacyStockLookup stockLookup)
    {
        _store = store ?? throw new ArgumentNullException(nameof(store));
        _stockLookup = stockLookup ?? throw new ArgumentNullException(nameof(stockLookup));
    }

    public LegacyManualListsSnapshot Current => CreateSnapshot();

    public async Task<LegacyManualListsSnapshot> OpenNetSellAsync(
        S5025ManualAudience audience,
        CancellationToken cancellationToken = default)
    {
        if (!Enum.IsDefined(audience))
        {
            throw new ArgumentOutOfRangeException(nameof(audience));
        }

        _isOpen = true;
        _screen = LegacyManualListScreen.NetSell;
        _audience = audience;
        _searchResults.Clear();
        _searchQuery = string.Empty;
        _selectedSearchResultId = null;
        _searchResultsAreTruncated = false;
        await ReadNetSellAsync(cancellationToken).ConfigureAwait(false);
        return Current;
    }

    public async Task<LegacyManualListsSnapshot> OpenViAsync(
        CancellationToken cancellationToken = default)
    {
        _isOpen = true;
        _screen = LegacyManualListScreen.Vi;
        _searchResults.Clear();
        _searchQuery = string.Empty;
        _selectedSearchResultId = null;
        _searchResultsAreTruncated = false;
        await ReadViAsync(cancellationToken).ConfigureAwait(false);
        return Current;
    }

    public LegacyManualListsSnapshot Close()
    {
        _isOpen = false;
        _screen = null;
        _searchResults.Clear();
        _searchQuery = string.Empty;
        _selectedSearchResultId = null;
        _searchResultsAreTruncated = false;
        SetStatus(string.Empty, LegacyOperatorStatusKind.Neutral);
        return Current;
    }

    public async Task<LegacyManualListsSnapshot> RefreshAsync(
        CancellationToken cancellationToken = default)
    {
        if (_screen == LegacyManualListScreen.NetSell)
        {
            await ReadNetSellAsync(cancellationToken).ConfigureAwait(false);
        }
        else if (_screen == LegacyManualListScreen.Vi)
        {
            await ReadViAsync(cancellationToken).ConfigureAwait(false);
        }

        return Current;
    }

    public LegacyManualListsSnapshot SetNetSellCell(
        string rowId,
        LegacyManualNetSellField field,
        string value)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        ArgumentNullException.ThrowIfNull(value);
        if (_screen != LegacyManualListScreen.NetSell ||
            !TryParseNetRowId(rowId, out var index) ||
            !IsSafeNetValue(value) ||
            !Enum.IsDefined(field))
        {
            SetStatus("수동 순매도 입력값을 확인하세요.", LegacyOperatorStatusKind.Warning);
            return Current;
        }

        var row = _netRows[index];
        _netRows[index] = field switch
        {
            LegacyManualNetSellField.LeftName => row with { LeftName = value },
            LegacyManualNetSellField.LeftAmount => row with { LeftAmount = value },
            LegacyManualNetSellField.RightName => row with { RightName = value },
            LegacyManualNetSellField.RightAmount => row with { RightAmount = value },
            _ => row
        };
        _netRowsAreFresh = false;
        SetStatus("변경 내용을 저장한 뒤 재조회해야 컷을 추가할 수 있습니다.",
            LegacyOperatorStatusKind.Information);
        return Current;
    }

    public async Task<LegacyManualListsSnapshot> SaveNetSellAsync(
        CancellationToken cancellationToken = default)
    {
        if (!CanWrite(LegacyManualListScreen.NetSell))
        {
            return Current;
        }

        var expected = _netRows.ToArray();
        try
        {
            await _store.WriteNetSellAsync(_audience, expected, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            return await RecoverNetSellWriteAsync(expected, cancellationToken)
                .ConfigureAwait(false);
        }

        return await VerifyNetSellWriteAsync(expected, recovered: false, cancellationToken)
            .ConfigureAwait(false);
    }

    public async Task<LegacyManualListsSnapshot> SearchViAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(query);
        _searchQuery = query.Trim();
        _searchResults.Clear();
        _selectedSearchResultId = null;
        _searchResultsAreTruncated = false;
        if (_screen != LegacyManualListScreen.Vi ||
            _searchQuery.Length > MaximumSearchLength)
        {
            SetStatus("검색어를 확인하세요.", LegacyOperatorStatusKind.Warning);
            return Current;
        }

        try
        {
            var data = await _stockLookup.SearchAsync(_searchQuery, cancellationToken)
                .ConfigureAwait(false);
            foreach (var displayName in data.DisplayNames)
            {
                var identity = data.ResolveLastByDisplayName(displayName);
                if (identity is not null)
                {
                    if (_searchResults.Count == MaximumSearchResults)
                    {
                        _searchResultsAreTruncated = true;
                        break;
                    }

                    _searchResults.Add(new ViSearchResult(
                        NextOpaqueId("manual-vi-result"),
                        identity));
                }
            }

            SetStatus(
                _searchResultsAreTruncated
                    ? $"검색 결과가 {MaximumSearchResults:N0}건을 초과해 일부만 표시합니다."
                    : _searchResults.Count == 0
                    ? "검색 결과가 없습니다."
                    : $"{_searchResults.Count}개 종목을 찾았습니다.",
                _searchResultsAreTruncated
                    ? LegacyOperatorStatusKind.Warning
                    : LegacyOperatorStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            SetStatus("종목 검색을 완료하지 못했습니다.", LegacyOperatorStatusKind.Error);
        }

        return Current;
    }

    public LegacyManualListsSnapshot SelectViSearchResult(string resultId)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        if (_screen == LegacyManualListScreen.Vi && _searchResults.Any(item =>
                string.Equals(item.ResultId, resultId, StringComparison.Ordinal)))
        {
            _selectedSearchResultId = resultId;
        }
        else
        {
            SetStatus(
                "The selected stock is no longer in the current VI search generation.",
                LegacyOperatorStatusKind.Warning);
        }

        return Current;
    }

    public LegacyManualListsSnapshot AddViSearchResult(string resultId)
    {
        ArgumentNullException.ThrowIfNull(resultId);
        var result = _searchResults.SingleOrDefault(item =>
            string.Equals(item.ResultId, resultId, StringComparison.Ordinal));
        if (_screen != LegacyManualListScreen.Vi ||
            result is null ||
            _viItems.Count >= MaximumViItems)
        {
            SetStatus("선택한 종목을 VI 목록에 추가할 수 없습니다.",
                LegacyOperatorStatusKind.Warning);
            return Current;
        }

        _selectedSearchResultId = resultId;

        var prefix = result.Identity.Market is
            LegacyDomesticStockMarket.Kospi or LegacyDomesticStockMarket.NxtKospi
                ? "P"
                : "D";
        var item = new LegacyManualViStoredItem(
            prefix + result.Identity.StockCode,
            result.Identity.DisplayName);
        if (!IsValidViItem(item))
        {
            SetStatus("선택한 종목 정보가 VI 파일 형식에 맞지 않습니다.",
                LegacyOperatorStatusKind.Warning);
            return Current;
        }

        _viItems.Add(new ViDraftItem(NextOpaqueId("manual-vi-item"), item));
        _viItemsAreFresh = false;
        SetStatus("VI 목록을 저장한 뒤 재조회해야 컷을 추가할 수 있습니다.",
            LegacyOperatorStatusKind.Information);
        return Current;
    }

    public LegacyManualListsSnapshot MoveViItem(
        string itemId,
        LegacyManualListMoveDirection direction)
    {
        var index = FindViItem(itemId);
        var target = direction == LegacyManualListMoveDirection.Up ? index - 1 : index + 1;
        if (_screen != LegacyManualListScreen.Vi ||
            index < 0 || target < 0 || target >= _viItems.Count ||
            !Enum.IsDefined(direction))
        {
            return Current;
        }

        (_viItems[index], _viItems[target]) = (_viItems[target], _viItems[index]);
        _viItemsAreFresh = false;
        SetStatus("VI 순서를 변경했습니다. 저장 후 재조회하세요.",
            LegacyOperatorStatusKind.Information);
        return Current;
    }

    public LegacyManualListsSnapshot DeleteViItem(string itemId)
    {
        var index = FindViItem(itemId);
        if (_screen == LegacyManualListScreen.Vi && index >= 0)
        {
            _viItems.RemoveAt(index);
            _viItemsAreFresh = false;
            SetStatus("VI 종목을 삭제했습니다. 저장 후 재조회하세요.",
                LegacyOperatorStatusKind.Information);
        }

        return Current;
    }

    public LegacyManualListsSnapshot DeleteAllViItems()
    {
        if (_screen == LegacyManualListScreen.Vi && _viItems.Count > 0)
        {
            _viItems.Clear();
            _viItemsAreFresh = false;
            SetStatus("VI 목록을 비웠습니다. 원본과 같이 빈 목록 저장은 기존 파일을 유지합니다.",
                LegacyOperatorStatusKind.Information);
        }

        return Current;
    }

    public async Task<LegacyManualListsSnapshot> SaveViAsync(
        CancellationToken cancellationToken = default)
    {
        if (!CanWrite(LegacyManualListScreen.Vi) ||
            !TrustedViManualReference.IsRowVersion(_viVersion))
        {
            return Current;
        }

        var expectedVersion = _viVersion;
        var expectedItems = _viItems.Select(item => item.Stored).ToArray();
        LegacyManualViStoreSnapshot preflight;
        try
        {
            preflight = await _store.ReadViAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            SetStatus("VI 저장 전 파일 버전을 확인하지 못했습니다. 저장하지 않았습니다.",
                LegacyOperatorStatusKind.Error);
            return Current;
        }

        if (!string.Equals(preflight.RowVersion, expectedVersion, StringComparison.Ordinal))
        {
            SetStatus("VI 파일이 다른 작업에서 변경되었습니다. 재조회 후 다시 편집하세요.",
                LegacyOperatorStatusKind.Warning);
            return Current;
        }

        LegacyManualViWriteReceipt receipt;
        try
        {
            receipt = await _store.WriteViAsync(expectedItems, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            return await RecoverViWriteAsync(expectedItems, cancellationToken)
                .ConfigureAwait(false);
        }

        return await VerifyViWriteAsync(
            expectedItems,
            receipt,
            recovered: false,
            cancellationToken).ConfigureAwait(false);
    }

    public async Task<LegacyManualListsSnapshot> ImportAsync(
        CancellationToken cancellationToken = default)
    {
        if (!_store.Availability.IsAvailable || _writesQuarantined)
        {
            return Current;
        }

        try
        {
            var receipt = await _store.ImportAsync(cancellationToken).ConfigureAwait(false);
            SetStatus(
                $"원본 수동 데이터를 가져왔습니다. 순매도 {receipt.NetSellRowCount}행, VI {receipt.ViItemCount}종목.",
                LegacyOperatorStatusKind.Information);
            return await RefreshAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (LegacyManualImportStoreException exception)
        {
            if (exception.OutcomeUnknown)
            {
                QuarantineWrites();
            }
            else
            {
                SetStatus(ImportFailureMessage(exception.Code), LegacyOperatorStatusKind.Warning);
            }
        }
        catch
        {
            SetStatus("원본 수동 데이터 가져오기를 완료하지 못했습니다.",
                LegacyOperatorStatusKind.Error);
        }

        return Current;
    }

    public LegacyManualListPlaylistDraft CreateNetSellPlaylistDraft()
    {
        if (_screen != LegacyManualListScreen.NetSell || !_netRowsAreFresh)
        {
            throw new InvalidOperationException(
                "The net-sell rows must be saved and re-read before materialization.");
        }

        var (groupCode, label) = _audience switch
        {
            S5025ManualAudience.Individual => ("INDIVIDUAL", "개인"),
            S5025ManualAudience.Foreign => ("FOREIGN", "외국인"),
            S5025ManualAudience.Institution => ("INSTITUTION", "기관"),
            _ => throw new InvalidOperationException("The manual audience is invalid.")
        };
        return new LegacyManualListPlaylistDraft(
            "5025",
            "지수",
            $"{label} 순매도 상위(수동)",
            "순매도 상위",
            "순매도 상위",
            "1/1",
            $"{label} 순매도 상위(수동)",
            6,
            new LegacySceneSelection(
                groupCode,
                "INDEX",
                "MANUAL_NET_SELL",
                "MANUAL_NET_SELL",
                string.Empty));
    }

    public LegacyManualListPlaylistDraft CreateViPlaylistDraft()
    {
        if (_screen != LegacyManualListScreen.Vi ||
            !_viItemsAreFresh ||
            _viItems.Count == 0 ||
            !TrustedViManualReference.IsRowVersion(_viVersion))
        {
            throw new InvalidOperationException(
                "The VI rows must be saved and re-read before materialization.");
        }

        var names = string.Join(",", _viItems.Select(item => item.Stored.Name));
        var pageCount = (_viItems.Count + ViPageSize - 1) / ViPageSize;
        var rowVersion = _viVersion ?? throw new InvalidOperationException(
            "The trusted VI row version is unavailable.");
        return new LegacyManualListPlaylistDraft(
            "5074",
            "지수",
            names,
            "5단 표그래프",
            "VI 발동(수동)",
            $"1/{pageCount}",
            "5단 표그래프_VI 발동(수동)",
            6,
            new LegacySceneSelection(
                "PAGED_VI",
                ViSnapshotSubject,
                "INPUT_ORDER",
                "CURRENT",
                rowVersion));
    }

    private async Task ReadNetSellAsync(CancellationToken cancellationToken)
    {
        _netRowsAreFresh = false;
        if (!EnsureAvailable())
        {
            return;
        }

        try
        {
            var rows = await _store.ReadNetSellAsync(_audience, cancellationToken)
                .ConfigureAwait(false);
            if (rows.Count != NetSellRowCount || rows.Any(row => !IsValidNetRow(row)))
            {
                throw new InvalidDataException("The FSell file shape is invalid.");
            }

            _netRows.Clear();
            _netRows.AddRange(rows);
            _netRowsAreFresh = true;
            SetStatus("수동 순매도 5행을 조회했습니다.", LegacyOperatorStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            _netRows.Clear();
            _netRows.AddRange(CreateEmptyNetRows());
            SetStatus("수동 순매도 파일을 읽지 못했습니다. 필요하면 원본 가져오기를 실행하세요.",
                LegacyOperatorStatusKind.Error);
        }
    }

    private async Task ReadViAsync(CancellationToken cancellationToken)
    {
        _viItemsAreFresh = false;
        _viVersion = null;
        if (!EnsureAvailable())
        {
            return;
        }

        try
        {
            var snapshot = await _store.ReadViAsync(cancellationToken).ConfigureAwait(false);
            SetViSnapshot(snapshot);
            SetStatus($"VI 수동 목록 {snapshot.Items.Count}종목을 조회했습니다.",
                LegacyOperatorStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            _viItems.Clear();
            SetStatus("VI 수동 목록 파일을 읽지 못했습니다. 필요하면 원본 가져오기를 실행하세요.",
                LegacyOperatorStatusKind.Error);
        }
    }

    private async Task<LegacyManualListsSnapshot> RecoverNetSellWriteAsync(
        IReadOnlyList<S5025TrustedManualRow> expected,
        CancellationToken cancellationToken)
    {
        try
        {
            var actual = await _store.ReadNetSellAsync(_audience, cancellationToken)
                .ConfigureAwait(false);
            if (actual.SequenceEqual(expected))
            {
                SetNetRows(actual);
                SetStatus("저장 응답은 실패했지만 재조회로 동일한 5행을 확인했습니다.",
                    LegacyOperatorStatusKind.Warning);
                return Current;
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
        }

        QuarantineWrites();
        return Current;
    }

    private async Task<LegacyManualListsSnapshot> VerifyNetSellWriteAsync(
        IReadOnlyList<S5025TrustedManualRow> expected,
        bool recovered,
        CancellationToken cancellationToken)
    {
        try
        {
            var actual = await _store.ReadNetSellAsync(_audience, cancellationToken)
                .ConfigureAwait(false);
            if (actual.SequenceEqual(expected))
            {
                SetNetRows(actual);
                SetStatus(
                    recovered
                        ? "재조회로 수동 순매도 저장을 확인했습니다."
                        : "저장 후 재조회한 5행이 입력값과 일치합니다.",
                    LegacyOperatorStatusKind.Information);
                return Current;
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
        }

        QuarantineWrites();
        return Current;
    }

    private async Task<LegacyManualListsSnapshot> RecoverViWriteAsync(
        IReadOnlyList<LegacyManualViStoredItem> expected,
        CancellationToken cancellationToken)
    {
        try
        {
            var actual = await _store.ReadViAsync(cancellationToken).ConfigureAwait(false);
            if (expected.Count > 0 && actual.Items.SequenceEqual(expected))
            {
                SetViSnapshot(actual);
                SetStatus("저장 응답은 실패했지만 재조회로 같은 VI 목록을 확인했습니다.",
                    LegacyOperatorStatusKind.Warning);
                return Current;
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
        }

        QuarantineWrites();
        return Current;
    }

    private async Task<LegacyManualListsSnapshot> VerifyViWriteAsync(
        IReadOnlyList<LegacyManualViStoredItem> expected,
        LegacyManualViWriteReceipt receipt,
        bool recovered,
        CancellationToken cancellationToken)
    {
        try
        {
            var actual = await _store.ReadViAsync(cancellationToken).ConfigureAwait(false);
            var receiptMatches = actual.Items.SequenceEqual(receipt.Snapshot.Items) &&
                string.Equals(actual.RowVersion, receipt.Snapshot.RowVersion,
                    StringComparison.Ordinal);
            var expectedMatches = !receipt.Persisted || actual.Items.SequenceEqual(expected);
            if (receiptMatches && expectedMatches &&
                TrustedViManualReference.IsRowVersion(actual.RowVersion))
            {
                SetViSnapshot(actual);
                SetStatus(
                    receipt.Persisted
                        ? recovered
                            ? "재조회로 VI 저장을 확인했습니다."
                            : "VI 저장 후 재조회한 목록과 버전이 일치합니다."
                        : "빈 목록은 원본 동작과 같이 파일을 덮어쓰지 않아 기존 목록을 다시 표시합니다.",
                    LegacyOperatorStatusKind.Information);
                return Current;
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
        }

        QuarantineWrites();
        return Current;
    }

    private bool CanWrite(LegacyManualListScreen requiredScreen)
    {
        if (_screen != requiredScreen || !_isOpen || !EnsureAvailable() || _writesQuarantined)
        {
            return false;
        }

        return true;
    }

    private bool EnsureAvailable()
    {
        if (_store.Availability.IsAvailable)
        {
            return true;
        }

        SetStatus(
            _store.Availability.Message ?? "수동 목록 저장소를 사용할 수 없습니다.",
            LegacyOperatorStatusKind.Error);
        return false;
    }

    private void SetNetRows(IEnumerable<S5025TrustedManualRow> rows)
    {
        _netRows.Clear();
        _netRows.AddRange(rows);
        _netRowsAreFresh = true;
    }

    private void SetViSnapshot(LegacyManualViStoreSnapshot snapshot)
    {
        if (snapshot.Items.Count > MaximumViItems ||
            snapshot.Items.Any(item => !IsValidViItem(item)) ||
            !TrustedViManualReference.IsRowVersion(snapshot.RowVersion))
        {
            throw new InvalidDataException("The VI snapshot is invalid.");
        }

        _viItems.Clear();
        _viItems.AddRange(snapshot.Items.Select(item =>
            new ViDraftItem(NextOpaqueId("manual-vi-item"), item)));
        _viVersion = snapshot.RowVersion;
        _viItemsAreFresh = true;
    }

    private LegacyManualListsSnapshot CreateSnapshot()
    {
        var availability = _store.Availability;
        var netRows = _netRows.Select((row, index) => new LegacyManualNetSellRowView(
            NetRowId(index),
            row.LeftName,
            row.LeftAmount,
            row.RightName,
            row.RightAmount)).ToArray();
        var viItems = _viItems.Select((item, index) => new LegacyManualViItemView(
            item.ItemId,
            index + 1,
            item.Stored.Name)).ToArray();
        var search = _searchResults.Select(item => new LegacyManualViSearchResultView(
            item.ResultId,
            item.Identity.DisplayName,
            item.Identity.MarketText)).ToArray();
        var canSave = _isOpen && availability.IsAvailable && !_writesQuarantined &&
            (_screen == LegacyManualListScreen.NetSell ||
                (_screen == LegacyManualListScreen.Vi &&
                    TrustedViManualReference.IsRowVersion(_viVersion)));
        var canMaterialize = _screen switch
        {
            LegacyManualListScreen.NetSell => _netRowsAreFresh,
            LegacyManualListScreen.Vi => _viItemsAreFresh && _viItems.Count > 0 &&
                TrustedViManualReference.IsRowVersion(_viVersion),
            _ => false
        };
        return new LegacyManualListsSnapshot(
            _isOpen,
            _screen,
            _audience,
            availability.IsAvailable,
            availability.Code,
            availability.Message,
            _writesQuarantined,
            Array.AsReadOnly(netRows),
            _netRowsAreFresh,
            Array.AsReadOnly(viItems),
            (_viItems.Count + ViPageSize - 1) / ViPageSize,
            _viItemsAreFresh,
            _searchQuery,
            Array.AsReadOnly(search),
            _statusMessage,
            _statusKind,
            canSave,
            canMaterialize)
        {
            SelectedSearchResultId = _selectedSearchResultId,
            SearchIsTruncated = _searchResultsAreTruncated
        };
    }

    private static List<S5025TrustedManualRow> CreateEmptyNetRows() =>
        Enumerable.Range(0, NetSellRowCount)
            .Select(_ => new S5025TrustedManualRow(string.Empty, string.Empty, string.Empty, string.Empty))
            .ToList();

    private static bool IsValidNetRow(S5025TrustedManualRow row) =>
        row is not null &&
        IsSafeNetValue(row.LeftName) &&
        IsSafeNetValue(row.LeftAmount) &&
        IsSafeNetValue(row.RightName) &&
        IsSafeNetValue(row.RightAmount);

    private static bool IsSafeNetValue(string? value) =>
        value is not null &&
        value.Length <= MaximumNetSellValueLength &&
        !value.Contains('^') &&
        !value.Any(character => char.IsControl(character) || character == '\uFFFD');

    private static bool IsValidViItem(LegacyManualViStoredItem item) =>
        item is not null &&
        IsSafeViText(item.Code, 64) &&
        IsSafeViText(item.Name, 200) &&
        !item.Name.Contains(',');

    private static bool IsSafeViText(string? value, int maximumLength) =>
        value is { Length: > 0 } &&
        value.Length <= maximumLength &&
        string.Equals(value, value.Trim(), StringComparison.Ordinal) &&
        !value.Contains('^') &&
        !value.Any(character => char.IsControl(character) || character == '\uFFFD');

    private static string NetRowId(int index) => $"manual-net-row-{index + 1}";

    private static bool TryParseNetRowId(string rowId, out int index)
    {
        index = Array.FindIndex(
            Enumerable.Range(0, NetSellRowCount).Select(NetRowId).ToArray(),
            value => string.Equals(value, rowId, StringComparison.Ordinal));
        return index >= 0;
    }

    private int FindViItem(string itemId) => _viItems.FindIndex(item =>
        string.Equals(item.ItemId, itemId, StringComparison.Ordinal));

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

    private void SetStatus(string message, LegacyOperatorStatusKind kind)
    {
        _statusMessage = message;
        _statusKind = kind;
    }

    private void QuarantineWrites()
    {
        _writesQuarantined = true;
        _netRowsAreFresh = false;
        _viItemsAreFresh = false;
        SetStatus(OutcomeUnknownMessage, LegacyOperatorStatusKind.Error);
    }

    private static string ImportFailureMessage(string code) => code switch
    {
        "SOURCE_UNAVAILABLE" => "원본 수동 데이터 파일을 찾을 수 없습니다.",
        "SOURCE_INVALID" => "원본 수동 데이터 파일 형식이 올바르지 않습니다.",
        "ALREADY_IMPORTED" => "원본 수동 데이터는 이미 한 번 가져왔습니다.",
        "DESTINATION_NOT_EMPTY" => "현재 저장소에 파일이 있어 원본 데이터를 덮어쓰지 않았습니다.",
        "INTERRUPTED_IMPORT" => "이전 가져오기 작업이 중단된 상태입니다. 파일을 먼저 확인하세요.",
        _ => "원본 수동 데이터 가져오기를 완료하지 못했습니다."
    };

    private sealed record ViDraftItem(string ItemId, LegacyManualViStoredItem Stored);

    private sealed record ViSearchResult(string ResultId, LegacyStockIdentity Identity);
}
