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

namespace MBN_STOCK_WEBVIEW;

public sealed partial class MainWindow
{
    private const string OperatorDataDirectoryEnvironmentVariable =
        "MBN_STOCK_OPERATOR_DATA_DIRECTORY";
    private const int MaximumManualValueLength = 256;
    private const string ManualOperatorCorrelationQuarantineMessage =
        "An operator-data file write result could not be confirmed. Restart the app and reconcile the trusted files before another write.";

    private readonly SemaphoreSlim _manualOperatorDataGate = new(1, 1);
    private S5025TrustedManualFileStore? _manualNetSellStore;
    private ViTrustedManualFileStore? _viManualListStore;
    private LegacyManualOperatorDataImporter? _legacyManualOperatorDataImporter;
    private string? _manualOperatorDataInitializationError;
    private string? _manualOperatorDataInitializationCode;
    private int _manualOperatorWriteInFlight;
    private int _manualOperatorWriteQuarantined;
    private int _manualOperatorBrowserGeneration;
    private string? _manualOperatorWriteQuarantineMessage =
        ManualOperatorCorrelationQuarantineMessage;

    private void InitializeManualOperatorDataRuntime()
    {
        try
        {
            var configuredDirectory = Environment.GetEnvironmentVariable(
                OperatorDataDirectoryEnvironmentVariable);
            if (!string.IsNullOrWhiteSpace(configuredDirectory))
            {
                _manualOperatorDataInitializationCode = "UNSAFE_CONFIGURATION";
                _manualOperatorDataInitializationError =
                    "외부 수동 데이터 경로는 TOCTOU 안전성을 보장할 수 없어 비활성화되었습니다. 앱 전용 로컬 운영 데이터 폴더를 사용하세요.";
                return;
            }

            var directory = TrustedManualDirectory.PreparePrivateWritableDirectory(Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                "MBN_STOCK_WEBVIEW",
                "OperatorData"));
            var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(directory);
            if (!inspection.IsReady)
            {
                (_manualOperatorDataInitializationCode,
                    _manualOperatorDataInitializationError) = inspection.State switch
                {
                    LegacyManualOperatorDataRuntimeState.InterruptedImport => (
                        "IMPORT_INTERRUPTED",
                        "이전 원본 수동 데이터 가져오기가 중단되었습니다. FSell/VI 파일은 열지 않았습니다. 다시 시작하기 전에 진행 표식, 완료 표식, 데이터 파일 4개를 확인하세요."),
                    _ => (
                        "IMPORTED_STATE_INVALID",
                        "원본 수동 데이터 완료 표식과 파일 4개가 검증된 한 세트를 이루지 않습니다. FSell/VI 파일은 열지 않았습니다. 다시 시작하기 전에 앱 전용 운영 데이터 폴더를 확인하세요.")
                };
                return;
            }

            _manualNetSellStore = new S5025TrustedManualFileStore(directory);
            _viManualListStore = new ViTrustedManualFileStore(directory);
            _legacyManualOperatorDataImporter = new LegacyManualOperatorDataImporter(
                directory,
                new FixedLegacyManualOperatorDataSource());
        }
        catch
        {
            _legacyManualOperatorDataImporter?.Dispose();
            _viManualListStore?.Dispose();
            _manualNetSellStore?.Dispose();
            _legacyManualOperatorDataImporter = null;
            _viManualListStore = null;
            _manualNetSellStore = null;
            _manualOperatorDataInitializationCode = "INITIALIZATION_FAILED";
            _manualOperatorDataInitializationError =
                "수동 송출 데이터 저장소를 초기화하지 못했습니다. 로컬 운영 데이터 설정을 확인하세요.";
        }
    }

    private void HandleManualOperatorStatusRequest(JsonElement payload)
    {
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId") ||
            !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId))
        {
            PostMessage("manual-operator-data-error", new
            {
                requestId = SafeManualOperatorRequestId(payload),
                operation = "status",
                code = "INVALID_REQUEST",
                message = "올바르지 않은 수동 데이터 상태 요청입니다.",
                retryable = false,
                outcomeUnknown = false
            });
            return;
        }

        PostMessage("manual-operator-data-status", new
        {
            requestId,
            available = _manualNetSellStore is not null && _viManualListStore is not null,
            code = _manualOperatorDataInitializationCode ?? "READY",
            writeQuarantined = IsManualOperatorWriteQuarantined(),
            message = IsManualOperatorWriteQuarantined()
                ? GetOperatorMutationQuarantineMessage(
                    _manualOperatorWriteQuarantineMessage ??
                        ManualOperatorCorrelationQuarantineMessage)
                : _manualOperatorDataInitializationError
        });
    }

    private async Task HandleLegacyManualOperatorImportAsync(JsonElement payload)
    {
        const string operation = "import-legacy-manual-data";
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId") ||
            !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId))
        {
            PostManualOperatorError(
                SafeManualOperatorRequestId(payload),
                operation,
                "INVALID_REQUEST",
                "올바르지 않은 원본 수동 데이터 가져오기 요청입니다.",
                retryable: false,
                outcomeUnknown: false);
            return;
        }

        var importer = _legacyManualOperatorDataImporter;
        if (importer is null || _manualNetSellStore is null || _viManualListStore is null)
        {
            PostManualOperatorUnavailable(requestId, operation);
            return;
        }

        if (IsManualOperatorWriteQuarantined())
        {
            PostManualOperatorQuarantine(requestId, operation);
            return;
        }

        if (!await TryEnterManualMutationGatesAsync(requestId, operation))
        {
            return;
        }

        var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
        Interlocked.Exchange(ref _manualOperatorWriteInFlight, 1);
        try
        {
            var result = await importer.ImportAsync(_lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                QuarantineManualOperatorWrites(
                    "원본 수동 데이터 가져오기 중 화면 상관관계가 사라졌습니다. 가져오기 marker와 4개 파일을 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
                return;
            }

            PostMessage("legacy-manual-operator-import-result", new
            {
                requestId,
                markerVersion = result.MarkerVersion,
                sourceSha256 = result.SourceSha256,
                importedAt = result.ImportedAt,
                netSellRowCount = result.NetSellRowCount,
                viItemCount = result.ViItemCount
            });
        }
        catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
        {
            // A pre-commit cancellation creates nothing. Once commit begins the
            // importer completes or rolls back without observing cancellation.
        }
        catch (LegacyManualOperatorImportException exception)
        {
            if (exception.OutcomeUnknown)
            {
                QuarantineManualOperatorWrites(
                    "원본 수동 데이터 가져오기를 안전하게 되돌렸는지 확인할 수 없습니다. 4개 파일과 marker를 직접 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
                PostManualOperatorQuarantine(requestId, operation);
                return;
            }

            var (code, message) = exception.Failure switch
            {
                LegacyManualOperatorImportFailure.SourceUnavailable => (
                    "SOURCE_UNAVAILABLE",
                    "원본 프로젝트의 FSell/VI 데이터 4개 파일을 찾을 수 없습니다."),
                LegacyManualOperatorImportFailure.SourceInvalid => (
                    "SOURCE_INVALID",
                    "원본 FSell/VI 데이터가 CP949 원본 형식 또는 크기 제한을 충족하지 않습니다."),
                LegacyManualOperatorImportFailure.AlreadyImported => (
                    "ALREADY_IMPORTED",
                    "이 앱 전용 폴더에는 이미 완료 marker가 있어 다시 가져오지 않습니다."),
                LegacyManualOperatorImportFailure.DestinationNotEmpty => (
                    "DESTINATION_NOT_EMPTY",
                    "앱 전용 폴더에 기존 FSell/VI 데이터가 있어 아무 파일도 덮어쓰지 않았습니다."),
                LegacyManualOperatorImportFailure.InterruptedImport => (
                    "IMPORT_INTERRUPTED",
                    "이전 가져오기의 진행 표식이 남아 있습니다. 완료 표식과 파일 4개를 확인하기 전에는 다시 시작하거나 반복하지 마세요."),
                _ => (
                    "IMPORT_FAILED",
                    "원본 수동 데이터 가져오기에 실패했고 생성된 파일은 되돌렸습니다. 자동으로 재시도하지 않습니다.")
            };
            PostManualOperatorError(
                requestId,
                operation,
                code,
                message,
                retryable: false,
                outcomeUnknown: false);
        }
        catch
        {
            QuarantineManualOperatorWrites(
                "원본 수동 데이터 가져오기 결과를 확인할 수 없습니다. 4개 파일과 marker를 직접 확인하고 앱을 다시 시작하기 전까지 작업을 반복하지 마세요.");
            PostManualOperatorQuarantine(requestId, operation);
        }
        finally
        {
            Interlocked.Exchange(ref _manualOperatorWriteInFlight, 0);
            ExitManualMutationGates();
        }
    }

    private async Task HandleManualNetSellReadAsync(JsonElement payload)
    {
        if (!TryParseAudienceRequest(payload, out var requestId, out var audience))
        {
            PostManualOperatorError(
                requestId,
                "read-net-sell",
                "INVALID_REQUEST",
                "올바르지 않은 수동 순매도 조회 요청입니다.",
                retryable: false,
                outcomeUnknown: false);
            return;
        }

        var store = _manualNetSellStore;
        if (store is null)
        {
            PostManualOperatorUnavailable(requestId, "read-net-sell");
            return;
        }

        var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
        await _manualOperatorDataGate.WaitAsync(_lifetimeCancellation.Token);
        try
        {
            var rows = await store.ReadAsync(audience, _lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                return;
            }
            PostMessage("manual-net-sell-data", new
            {
                requestId,
                audience = ToWireValue(audience),
                rows = rows.Select(row => new
                {
                    row.LeftName,
                    row.LeftAmount,
                    row.RightName,
                    row.RightAmount
                })
            });
        }
        catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
        {
            // Window shutdown owns cancellation and no stale browser response is posted.
        }
        catch (S5025TrustedManualDataException)
        {
            PostManualOperatorError(
                requestId,
                "read-net-sell",
                "DATA_INVALID",
                "수동 순매도 데이터 파일이 없거나 형식이 올바르지 않습니다.",
                retryable: false,
                outcomeUnknown: false);
        }
        finally
        {
            _manualOperatorDataGate.Release();
        }
    }

    private async Task HandleManualNetSellWriteAsync(JsonElement payload)
    {
        if (!TryParseManualNetSellWrite(
                payload,
                out var requestId,
                out var audience,
                out var rows))
        {
            PostManualOperatorError(
                requestId,
                "save-net-sell",
                "INVALID_REQUEST",
                "수동 순매도 입력은 5행의 검증된 값이어야 합니다.",
                retryable: false,
                outcomeUnknown: false);
            return;
        }

        var store = _manualNetSellStore;
        if (store is null)
        {
            PostManualOperatorUnavailable(requestId, "save-net-sell");
            return;
        }

        if (IsManualOperatorWriteQuarantined())
        {
            PostManualOperatorQuarantine(requestId, "save-net-sell");
            return;
        }

        if (!await TryEnterManualMutationGatesAsync(requestId, "save-net-sell"))
        {
            return;
        }

        var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
        if (!CanStartOperatorMutation(IsManualOperatorWriteQuarantined()))
        {
            ExitManualMutationGates();
            PostManualOperatorError(
                requestId,
                "save-net-sell",
                "PLAYOUT_ACTIVE",
                "Operator data cannot be changed unless playout is verifiably idle.",
                retryable: true,
                outcomeUnknown: false);
            return;
        }
        Interlocked.Exchange(ref _manualOperatorWriteInFlight, 1);
        try
        {
            await store.WriteAsync(audience, rows, _lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                QuarantineManualOperatorWrites(
                    "수동 순매도 저장 중 화면 상관관계가 사라졌습니다. 저장 파일을 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                return;
            }
            PostManualNetSellWriteResult(requestId, audience, recovered: false);
        }
        catch (Exception exception) when (exception is
            S5025TrustedManualDataException or OperationCanceledException)
        {
            if (_lifetimeCancellation.IsCancellationRequested)
            {
                return;
            }

            try
            {
                var persisted = await store.ReadAsync(audience, CancellationToken.None);
                if (persisted.SequenceEqual(rows))
                {
                    if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
                    {
                        QuarantineManualOperatorWrites(
                            "수동 순매도 저장 중 화면 상관관계가 사라졌습니다. 저장 파일을 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                        return;
                    }
                    PostManualNetSellWriteResult(requestId, audience, recovered: true);
                    return;
                }

                QuarantineManualOperatorWrites(
                    "수동 순매도 저장 중 파일 내용이 달라져 결과를 확정할 수 없습니다. 파일을 직접 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                PostManualOperatorQuarantine(requestId, "save-net-sell");
            }
            catch
            {
                QuarantineManualOperatorWrites(
                    "수동 순매도 저장 결과를 확인할 수 없습니다. 파일을 직접 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                PostManualOperatorQuarantine(requestId, "save-net-sell");
            }
        }
        finally
        {
            Interlocked.Exchange(ref _manualOperatorWriteInFlight, 0);
            ExitManualMutationGates();
        }
    }

    private async Task HandleViManualListReadAsync(JsonElement payload)
    {
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId") ||
            !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out var requestId))
        {
            PostManualOperatorError(
                SafeManualOperatorRequestId(payload),
                "read-vi",
                "INVALID_REQUEST",
                "올바르지 않은 VI 수동 목록 조회 요청입니다.",
                retryable: false,
                outcomeUnknown: false);
            return;
        }

        var store = _viManualListStore;
        if (store is null)
        {
            PostManualOperatorUnavailable(requestId, "read-vi");
            return;
        }

        var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
        await _manualOperatorDataGate.WaitAsync(_lifetimeCancellation.Token);
        try
        {
            var snapshot = await store.ReadSnapshotAsync(_lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                return;
            }
            PostMessage("vi-manual-list-data", new
            {
                requestId,
                itemCount = snapshot.Items.Count,
                pageCount = snapshot.Items.Count == 0 ? 0 : (snapshot.Items.Count + 4) / 5,
                items = snapshot.Items,
                version = snapshot.RowVersion
            });
        }
        catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
        {
            // Window shutdown owns cancellation.
        }
        catch (ViTrustedManualDataException)
        {
            PostManualOperatorError(
                requestId,
                "read-vi",
                "DATA_INVALID",
                "VI 수동 목록 파일 형식이 올바르지 않습니다.",
                retryable: false,
                outcomeUnknown: false);
        }
        finally
        {
            _manualOperatorDataGate.Release();
        }
    }

    private async Task HandleViManualListWriteAsync(JsonElement payload)
    {
        if (!TryParseViWrite(
                payload,
                out var requestId,
                out var expectedVersion,
                out var items))
        {
            PostManualOperatorError(
                requestId,
                "save-vi",
                "INVALID_REQUEST",
                $"VI 수동 목록은 최대 {ViTrustedManualFileStore.MaximumItemCount}개의 검증된 종목이어야 합니다.",
                retryable: false,
                outcomeUnknown: false);
            return;
        }

        var store = _viManualListStore;
        if (store is null)
        {
            PostManualOperatorUnavailable(requestId, "save-vi");
            return;
        }

        if (IsManualOperatorWriteQuarantined())
        {
            PostManualOperatorQuarantine(requestId, "save-vi");
            return;
        }

        if (!await TryEnterManualMutationGatesAsync(requestId, "save-vi"))
        {
            return;
        }

        var browserGeneration = Volatile.Read(ref _manualOperatorBrowserGeneration);
        if (!CanStartOperatorMutation(IsManualOperatorWriteQuarantined()))
        {
            ExitManualMutationGates();
            PostManualOperatorError(
                requestId,
                "save-vi",
                "PLAYOUT_ACTIVE",
                "Operator data cannot be changed unless playout is verifiably idle.",
                retryable: true,
                outcomeUnknown: false);
            return;
        }
        var mutationStarted = false;
        try
        {
            var currentSnapshot = await store.ReadSnapshotAsync(_lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                return;
            }

            if (!string.Equals(
                    currentSnapshot.RowVersion,
                    expectedVersion,
                    StringComparison.Ordinal))
            {
                PostManualOperatorError(
                    requestId,
                    "save-vi",
                    "STALE_DATA",
                    "The VI manual list changed after it was read. Reload it before saving.",
                    retryable: false,
                    outcomeUnknown: false);
                return;
            }

            Interlocked.Exchange(ref _manualOperatorWriteInFlight, 1);
            mutationStarted = true;
            var result = await store.WriteAsync(items, _lifetimeCancellation.Token);
            if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
            {
                QuarantineManualOperatorWrites(
                    "VI 수동 목록 저장 중 화면 상관관계가 사라졌습니다. 저장 파일을 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                return;
            }
            PostViWriteResult(requestId, result, recovered: false);
        }
        catch (Exception exception) when (exception is
            ViTrustedManualDataException or OperationCanceledException)
        {
            if (_lifetimeCancellation.IsCancellationRequested)
            {
                return;
            }

            if (!mutationStarted)
            {
                PostManualOperatorError(
                    requestId,
                    "save-vi",
                    "DATA_INVALID",
                    "The current VI manual list could not be verified before saving.",
                    retryable: false,
                    outcomeUnknown: false);
                return;
            }

            try
            {
                var persisted = await store.ReadSnapshotAsync(CancellationToken.None);
                if (persisted.Items.SequenceEqual(items))
                {
                    if (browserGeneration != Volatile.Read(ref _manualOperatorBrowserGeneration))
                    {
                        QuarantineManualOperatorWrites(
                            "VI 수동 목록 저장 중 화면 상관관계가 사라졌습니다. 저장 파일을 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                        return;
                    }
                    PostViWriteResult(
                        requestId,
                        new ViTrustedManualWriteResult(persisted, Persisted: items.Count != 0),
                        recovered: true);
                    return;
                }

                QuarantineManualOperatorWrites(
                    "VI 수동 목록 저장 중 파일 내용이 달라져 결과를 확정할 수 없습니다. 파일을 직접 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                PostManualOperatorQuarantine(requestId, "save-vi");
            }
            catch
            {
                QuarantineManualOperatorWrites(
                    "VI 수동 목록 저장 결과를 확인할 수 없습니다. 파일을 직접 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
                PostManualOperatorQuarantine(requestId, "save-vi");
            }
        }
        finally
        {
            Interlocked.Exchange(ref _manualOperatorWriteInFlight, 0);
            ExitManualMutationGates();
        }
    }

    private async Task<bool> TryEnterManualMutationGatesAsync(
        string requestId,
        string operation)
    {
        if (!await _playoutCommandGate.WaitAsync(0))
        {
            PostManualOperatorError(
                requestId,
                operation,
                "PLAYOUT_BUSY",
                "송출 명령을 처리하는 동안 수동 데이터를 저장할 수 없습니다.",
                retryable: true,
                outcomeUnknown: false);
            return false;
        }

        if (!await _manualOperatorDataGate.WaitAsync(0))
        {
            _playoutCommandGate.Release();
            PostManualOperatorError(
                requestId,
                operation,
                "DATA_BUSY",
                "다른 수동 데이터 작업이 진행 중입니다.",
                retryable: true,
                outcomeUnknown: false);
            return false;
        }

        if (!CanStartOperatorMutation(IsManualOperatorWriteQuarantined()))
        {
            ExitManualMutationGates();
            PostManualOperatorError(
                requestId,
                operation,
                "PLAYOUT_ACTIVE",
                "PREPARE 또는 송출 중에는 수동 데이터를 변경할 수 없습니다. TAKE OUT 후 저장하세요.",
                retryable: true,
                outcomeUnknown: false);
            return false;
        }

        return true;
    }

    private void ExitManualMutationGates()
    {
        _manualOperatorDataGate.Release();
        _playoutCommandGate.Release();
    }

    private static bool TryParseAudienceRequest(
        JsonElement payload,
        out string requestId,
        out S5025ManualAudience audience)
    {
        requestId = SafeManualOperatorRequestId(payload);
        audience = default;
        return payload.ValueKind == JsonValueKind.Object &&
            HasOnlyProperties(payload, "requestId", "audience") &&
            TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) &&
            TryParseAudience(GetString(payload, "audience"), out audience);
    }

    private static string SafeManualOperatorRequestId(JsonElement payload) =>
        payload.ValueKind == JsonValueKind.Object &&
        TryGetSafeToken(
            payload,
            "requestId",
            MaximumPlayoutRequestIdLength,
            out var requestId)
            ? requestId
            : string.Empty;

    private static bool TryParseManualNetSellWrite(
        JsonElement payload,
        out string requestId,
        out S5025ManualAudience audience,
        out IReadOnlyList<S5025TrustedManualRow> rows)
    {
        requestId = SafeManualOperatorRequestId(payload);
        audience = default;
        rows = Array.Empty<S5025TrustedManualRow>();
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId", "audience", "rows") ||
            !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) ||
            !TryParseAudience(GetString(payload, "audience"), out audience) ||
            !payload.TryGetProperty("rows", out var rowsValue) ||
            rowsValue.ValueKind != JsonValueKind.Array ||
            rowsValue.GetArrayLength() != 5)
        {
            return false;
        }

        var parsed = new List<S5025TrustedManualRow>(5);
        foreach (var row in rowsValue.EnumerateArray())
        {
            if (row.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(
                    row,
                    "leftName",
                    "leftAmount",
                    "rightName",
                    "rightAmount") ||
                !TryGetManualValue(row, "leftName", out var leftName) ||
                !TryGetManualValue(row, "leftAmount", out var leftAmount) ||
                !TryGetManualValue(row, "rightName", out var rightName) ||
                !TryGetManualValue(row, "rightAmount", out var rightAmount))
            {
                return false;
            }

            parsed.Add(new S5025TrustedManualRow(
                leftName,
                leftAmount,
                rightName,
                rightAmount));
        }

        rows = parsed;
        return true;
    }

    private static bool TryParseViWrite(
        JsonElement payload,
        out string requestId,
        out string expectedVersion,
        out IReadOnlyList<ViTrustedManualItem> items)
    {
        requestId = SafeManualOperatorRequestId(payload);
        expectedVersion = string.Empty;
        items = Array.Empty<ViTrustedManualItem>();
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId", "expectedVersion", "items") ||
            !TryGetSafeToken(payload, "requestId", MaximumPlayoutRequestIdLength, out requestId) ||
            !TryGetViExpectedVersion(payload, out expectedVersion) ||
            !payload.TryGetProperty("items", out var itemsValue) ||
            itemsValue.ValueKind != JsonValueKind.Array ||
            itemsValue.GetArrayLength() > ViTrustedManualFileStore.MaximumItemCount)
        {
            return false;
        }

        var parsed = new List<ViTrustedManualItem>(itemsValue.GetArrayLength());
        foreach (var item in itemsValue.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(item, "code", "name") ||
                !TryGetManualValue(item, "code", out var code, allowEmpty: false, maximumLength: 64) ||
                !TryGetManualValue(item, "name", out var name, allowEmpty: false, maximumLength: 200) ||
                !string.Equals(code, code.Trim(), StringComparison.Ordinal) ||
                !string.Equals(name, name.Trim(), StringComparison.Ordinal) ||
                name.Contains(','))
            {
                return false;
            }

            parsed.Add(new ViTrustedManualItem(code, name));
        }

        items = parsed;
        return true;
    }

    private static bool TryGetViExpectedVersion(
        JsonElement payload,
        out string expectedVersion)
    {
        expectedVersion = string.Empty;
        if (payload.ValueKind != JsonValueKind.Object ||
            !payload.TryGetProperty("expectedVersion", out var value) ||
            value.ValueKind != JsonValueKind.String)
        {
            return false;
        }

        expectedVersion = value.GetString() ?? string.Empty;
        return TrustedViManualReference.IsRowVersion(expectedVersion);
    }

    private static bool TryGetManualValue(
        JsonElement payload,
        string propertyName,
        out string value,
        bool allowEmpty = true,
        int maximumLength = MaximumManualValueLength)
    {
        value = GetString(payload, propertyName) ?? string.Empty;
        return payload.TryGetProperty(propertyName, out var element) &&
            element.ValueKind == JsonValueKind.String &&
            (allowEmpty || value.Length > 0) &&
            value.Length <= maximumLength &&
            !value.Contains('^') &&
            !value.Any(char.IsControl);
    }

    private static bool TryParseAudience(
        string? value,
        out S5025ManualAudience audience)
    {
        audience = value switch
        {
            "INDIVIDUAL" => S5025ManualAudience.Individual,
            "FOREIGN" => S5025ManualAudience.Foreign,
            "INSTITUTION" => S5025ManualAudience.Institution,
            _ => default
        };
        return value is "INDIVIDUAL" or "FOREIGN" or "INSTITUTION";
    }

    private static string ToWireValue(S5025ManualAudience audience) => audience switch
    {
        S5025ManualAudience.Individual => "INDIVIDUAL",
        S5025ManualAudience.Foreign => "FOREIGN",
        S5025ManualAudience.Institution => "INSTITUTION",
        _ => throw new ArgumentOutOfRangeException(nameof(audience))
    };

    private void PostManualNetSellWriteResult(
        string requestId,
        S5025ManualAudience audience,
        bool recovered) =>
        PostMessage("manual-net-sell-save-result", new
        {
            requestId,
            audience = ToWireValue(audience),
            saved = true,
            recovered
        });

    private void PostViWriteResult(
        string requestId,
        ViTrustedManualWriteResult result,
        bool recovered) =>
        PostMessage("vi-manual-list-save-result", new
        {
            requestId,
            saved = true,
            recovered,
            persisted = result.Persisted,
            itemCount = result.Snapshot.Items.Count,
            pageCount = result.Snapshot.Items.Count == 0
                ? 0
                : (result.Snapshot.Items.Count + 4) / 5,
            version = result.Snapshot.RowVersion
        });

    private void PostManualOperatorUnavailable(string requestId, string operation) =>
        PostManualOperatorError(
            requestId,
            operation,
            _manualOperatorDataInitializationCode ?? "DATA_UNAVAILABLE",
            _manualOperatorDataInitializationError ??
                "수동 송출 데이터 저장소를 사용할 수 없습니다.",
            retryable: false,
            outcomeUnknown: false);

    private void PostManualOperatorQuarantine(string requestId, string operation) =>
        PostManualOperatorError(
            requestId,
            operation,
            "OUTCOME_UNKNOWN",
            _manualOperatorWriteQuarantineMessage ??
                "이전 저장 결과가 불명확하여 앱을 다시 시작하기 전까지 저장할 수 없습니다.",
            retryable: false,
            outcomeUnknown: true);

    private void PostManualOperatorError(
        string requestId,
        string operation,
        string code,
        string message,
        bool retryable,
        bool outcomeUnknown) =>
        PostMessage("manual-operator-data-error", new
        {
            requestId,
            operation,
            code,
            message,
            retryable,
            outcomeUnknown
        });

    private bool IsManualOperatorWriteQuarantined() =>
        Volatile.Read(ref _manualOperatorWriteQuarantined) != 0 ||
        IsOperatorMutationQuarantined();

    private void QuarantineManualOperatorWrites(string message)
    {
        LatchOperatorMutationQuarantine(message);
        _manualOperatorWriteQuarantineMessage = message;
        Interlocked.Exchange(ref _manualOperatorWriteQuarantined, 1);
    }

    private void ShutdownManualOperatorDataRuntime()
    {
        InvalidateManualOperatorBrowserRequests();
        var netSellStore = Interlocked.Exchange(ref _manualNetSellStore, null);
        var viStore = Interlocked.Exchange(ref _viManualListStore, null);
        var importer = Interlocked.Exchange(ref _legacyManualOperatorDataImporter, null);
        if (netSellStore is null && viStore is null && importer is null)
        {
            return;
        }

        _ = DisposeManualOperatorStoresWhenIdleAsync(netSellStore, viStore, importer);
    }

    private async Task DisposeManualOperatorStoresWhenIdleAsync(
        S5025TrustedManualFileStore? netSellStore,
        ViTrustedManualFileStore? viStore,
        LegacyManualOperatorDataImporter? importer)
    {
        await _manualOperatorDataGate.WaitAsync().ConfigureAwait(false);
        try
        {
            importer?.Dispose();
            viStore?.Dispose();
            netSellStore?.Dispose();
        }
        finally
        {
            _manualOperatorDataGate.Release();
        }
    }

    private void InvalidateManualOperatorBrowserRequests()
    {
        Interlocked.Increment(ref _manualOperatorBrowserGeneration);
        if (Volatile.Read(ref _manualOperatorWriteInFlight) != 0)
        {
            QuarantineManualOperatorWrites(
                "수동 데이터 저장 중 화면 상관관계가 사라졌습니다. 저장 파일을 확인하고 앱을 다시 시작하기 전까지 저장을 반복하지 마세요.");
        }
    }
}
