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

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

public enum LegacyOperatorCatalogEntity
{
    Theme,
    Expert
}

public enum LegacyOperatorCatalogMode
{
    List,
    Create,
    Edit
}

public enum LegacyOperatorCatalogStatusKind
{
    Neutral,
    Information,
    Warning,
    Error
}

public enum LegacyThemeNameAvailability
{
    NotChecked,
    Available,
    Duplicate,
    Indeterminate
}

public enum LegacyOperatorCatalogMoveDirection
{
    Up,
    Down
}

public sealed record LegacyOperatorCatalogResultView(
    string ResultId,
    string CodeLabel,
    string DisplayName,
    string Scope,
    bool IsSelected);

public sealed record LegacyOperatorCatalogDraftRowView(
    string RowId,
    string DisplayName,
    string Market,
    decimal? BuyAmount,
    int Position);

public sealed record LegacyOperatorCatalogStockResultView(
    string ResultId,
    string DisplayName,
    string Market,
    bool IsCompatible,
    bool IsSelected);

public sealed record LegacyOperatorCatalogWorkflowSnapshot(
    long Revision,
    bool IsOpen,
    LegacyOperatorCatalogEntity Entity,
    LegacyOperatorCatalogMode Mode,
    string Query,
    ThemeSession NxtSession,
    ThemeMarket NewThemeMarket,
    IReadOnlyList<LegacyOperatorCatalogResultView> Results,
    bool ResultsAreTruncated,
    string? SelectedResultId,
    string CodeLabel,
    string EditorName,
    LegacyThemeNameAvailability ThemeNameAvailability,
    string CheckedThemeName,
    IReadOnlyList<LegacyOperatorCatalogDraftRowView> DraftRows,
    string? ActiveDraftRowId,
    string StockQuery,
    IReadOnlyList<LegacyOperatorCatalogStockResultView> StockResults,
    bool StockResultsAreTruncated,
    string? SelectedStockResultId,
    bool SchemaValidated,
    bool WritesQuarantined,
    bool CanBeginCreate,
    bool CanCheckThemeName,
    bool CanSave,
    bool CanDelete,
    bool CanAddStock,
    string StatusMessage,
    LegacyOperatorCatalogStatusKind StatusKind);

internal sealed class LegacyOperatorCatalogWriteSafetyState
{
    private const string DefaultQuarantineMessage =
        "ThemeA/EList database write outcome is unknown. Restart the application and reconcile the database before another write.";

    private readonly object _sync = new();
    private long _correlationGeneration;
    private bool _writeInFlight;
    private bool _writesQuarantined;
    private string? _quarantineReason;

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

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

    internal bool TryBegin(out long generation, out string reason)
    {
        lock (_sync)
        {
            generation = _correlationGeneration;
            if (_writesQuarantined)
            {
                reason = _quarantineReason ?? DefaultQuarantineMessage;
                return false;
            }

            if (_writeInFlight)
            {
                reason = "Another ThemeA/EList write is already running.";
                return false;
            }

            reason = string.Empty;
            return true;
        }
    }

    internal bool MarkDispatched(long generation)
    {
        lock (_sync)
        {
            if (_writesQuarantined || generation != _correlationGeneration || _writeInFlight)
            {
                return false;
            }

            _writeInFlight = true;
            return true;
        }
    }

    internal bool IsCurrent(long generation)
    {
        lock (_sync)
        {
            return !_writesQuarantined && generation == _correlationGeneration;
        }
    }

    internal void Complete()
    {
        lock (_sync)
        {
            _writeInFlight = false;
        }
    }

    internal string Quarantine(string? reason = null)
    {
        lock (_sync)
        {
            _writesQuarantined = true;
            _writeInFlight = false;
            _quarantineReason ??= string.IsNullOrWhiteSpace(reason)
                ? DefaultQuarantineMessage
                : reason;
            return _quarantineReason;
        }
    }

    internal void InvalidateCorrelation()
    {
        lock (_sync)
        {
            _correlationGeneration = checked(_correlationGeneration + 1);
            if (_writeInFlight)
            {
                _writesQuarantined = true;
                _quarantineReason ??= DefaultQuarantineMessage;
                _writeInFlight = false;
            }
        }
    }
}

/// <summary>
/// C#-authoritative migration of ThemeA and EList. Web callers can select only opaque
/// catalog, stock, and draft-row ids. Database identities, provider routing, item codes,
/// optimistic identities, and mutation ordering remain inside this workflow. Each explicit
/// save/delete intent dispatches at most one transaction. An uncertain result latches the
/// process-lifetime write quarantine and is never retried.
/// </summary>
public sealed class LegacyOperatorCatalogWorkflowController
{
    public const int MaximumCatalogResults = LegacyThemeSelectionService.MaximumResults;
    public const int MaximumExpertCatalogResults = LegacyExpertSelectionService.MaximumResults;
    public const int MaximumStockResults = LegacyStockSearchService.MaximumResults;
    public const string ThemeNameDuplicateMessage = "이미 사용중인 테마명입니다.";
    public const string ThemeNameAvailableMessage = "사용 가능한 테마명입니다.";
    public const string ThemeSaveSuccessMessage = "ThemeA를 저장했습니다.";
    public const string ExpertSaveSuccessMessage = "EList를 저장했습니다.";
    public const string ThemeDeleteSuccessMessage = "ThemeA를 삭제했습니다.";
    public const string ExpertDeleteSuccessMessage = "EList를 삭제했습니다.";

    private const decimal MaximumWebSafeInteger = 9_007_199_254_740_991m;
    private const string OutcomeUnknownMessage =
        "ThemeA/EList 저장 결과를 확인할 수 없어 현재 프로세스의 추가 DB 쓰기를 차단했습니다. DB를 대조한 뒤 프로그램을 다시 시작하세요.";

    private static readonly LegacyOperatorCatalogWriteSafetyState ProcessWriteSafety = new();

    private readonly IThemeSelectionService _themeSelectionService;
    private readonly IExpertSelectionService _expertSelectionService;
    private readonly IThemeCatalogPersistenceService _themePersistenceService;
    private readonly IExpertCatalogPersistenceService _expertPersistenceService;
    private readonly IOperatorCatalogStockSearchService _stockSearchService;
    private readonly IStockMasterIdentityValidationService _identityValidationService;
    private readonly IOperatorCatalogSchemaValidationService _schemaValidationService;
    private readonly LegacyOperatorCatalogWriteSafetyState _writeSafety;

    private readonly List<CatalogResultState> _results = [];
    private readonly List<DraftRowState> _draftRows = [];
    private readonly List<StockResultState> _stockResults = [];
    private long _revision;
    private bool _isOpen;
    private bool _schemaValidated;
    private bool _resultsAreTruncated;
    private bool _stockResultsAreTruncated;
    private LegacyOperatorCatalogEntity _entity = LegacyOperatorCatalogEntity.Theme;
    private LegacyOperatorCatalogMode _mode = LegacyOperatorCatalogMode.List;
    private ThemeSession _nxtSession = ThemeSession.PreMarket;
    private ThemeMarket _newThemeMarket = ThemeMarket.Krx;
    private string _query = string.Empty;
    private string _stockQuery = string.Empty;
    private string _codeLabel = string.Empty;
    private string _editorName = string.Empty;
    private LegacyThemeNameAvailability _themeNameAvailability =
        LegacyThemeNameAvailability.NotChecked;
    private string _checkedThemeName = string.Empty;
    private string? _selectedResultId;
    private string? _selectedStockResultId;
    private string? _activeDraftRowId;
    private ThemeSelectionIdentity? _expectedThemeIdentity;
    private ExpertSelectionIdentity? _expectedExpertIdentity;
    private string _statusMessage = string.Empty;
    private LegacyOperatorCatalogStatusKind _statusKind = LegacyOperatorCatalogStatusKind.Neutral;

    public LegacyOperatorCatalogWorkflowController(
        IThemeSelectionService themeSelectionService,
        IExpertSelectionService expertSelectionService,
        IThemeCatalogPersistenceService themePersistenceService,
        IExpertCatalogPersistenceService expertPersistenceService,
        IOperatorCatalogStockSearchService stockSearchService,
        IStockMasterIdentityValidationService identityValidationService,
        IOperatorCatalogSchemaValidationService schemaValidationService)
        : this(
            themeSelectionService,
            expertSelectionService,
            themePersistenceService,
            expertPersistenceService,
            stockSearchService,
            identityValidationService,
            schemaValidationService,
            ProcessWriteSafety)
    {
    }

    internal LegacyOperatorCatalogWorkflowController(
        IThemeSelectionService themeSelectionService,
        IExpertSelectionService expertSelectionService,
        IThemeCatalogPersistenceService themePersistenceService,
        IExpertCatalogPersistenceService expertPersistenceService,
        IOperatorCatalogStockSearchService stockSearchService,
        IStockMasterIdentityValidationService identityValidationService,
        IOperatorCatalogSchemaValidationService schemaValidationService,
        LegacyOperatorCatalogWriteSafetyState writeSafety)
    {
        _themeSelectionService = themeSelectionService ??
            throw new ArgumentNullException(nameof(themeSelectionService));
        _expertSelectionService = expertSelectionService ??
            throw new ArgumentNullException(nameof(expertSelectionService));
        _themePersistenceService = themePersistenceService ??
            throw new ArgumentNullException(nameof(themePersistenceService));
        _expertPersistenceService = expertPersistenceService ??
            throw new ArgumentNullException(nameof(expertPersistenceService));
        _stockSearchService = stockSearchService ??
            throw new ArgumentNullException(nameof(stockSearchService));
        _identityValidationService = identityValidationService ??
            throw new ArgumentNullException(nameof(identityValidationService));
        _schemaValidationService = schemaValidationService ??
            throw new ArgumentNullException(nameof(schemaValidationService));
        _writeSafety = writeSafety ?? throw new ArgumentNullException(nameof(writeSafety));
    }

    public LegacyOperatorCatalogWorkflowSnapshot Current => CreateSnapshot();

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> OpenAsync(
        LegacyOperatorCatalogEntity entity,
        CancellationToken cancellationToken = default)
    {
        if (_isOpen && _mode != LegacyOperatorCatalogMode.List)
        {
            throw new InvalidOperationException(
                "Close the active legacy child dialog before switching catalogs.");
        }

        _isOpen = true;
        _entity = entity;
        _mode = LegacyOperatorCatalogMode.List;
        _query = string.Empty;
        ClearEditor();
        ClearStatus();

        try
        {
            if (!_schemaValidated)
            {
                var validation = await _schemaValidationService.ValidateAsync(cancellationToken)
                    .ConfigureAwait(false);
                _schemaValidated = validation.ValidatedSources.Contains(DataSourceKind.Oracle) &&
                    validation.ValidatedSources.Contains(DataSourceKind.MariaDb);
                if (!_schemaValidated)
                {
                    throw new OperatorCatalogSchemaException(
                        DataSourceKind.Oracle,
                        "The complete ThemeA/EList schema was not validated.");
                }
            }

            await LoadCatalogListCoreAsync(cancellationToken).ConfigureAwait(false);
            SetStatus(
                entity == LegacyOperatorCatalogEntity.Theme
                    ? "ThemeA 목록을 불러왔습니다."
                    : "EList 목록을 불러왔습니다.",
                LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            _schemaValidated = false;
            _results.Clear();
            _resultsAreTruncated = false;
            SetStatus(
                "ThemeA/EList 스키마 또는 목록을 확인하지 못했습니다. DB 연결을 확인하세요.",
                LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Opens the ThemeA editor for the exact identity already selected by UC4.
    /// The browser never receives or sends this database identity; it remains
    /// correlated inside the native workflows from selection through mutation.
    /// </summary>
    public async Task<LegacyOperatorCatalogWorkflowSnapshot> OpenThemeForEditAsync(
        ThemeSelectionIdentity identity,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(identity);
        await OpenAsync(LegacyOperatorCatalogEntity.Theme, cancellationToken)
            .ConfigureAwait(false);

        var session = identity.Market == ThemeMarket.Nxt
            ? identity.Session
            : ThemeSession.PreMarket;
        await SearchAsync(identity.ThemeTitle, session, cancellationToken)
            .ConfigureAwait(false);

        var selected = _results.SingleOrDefault(row => row.ThemeIdentity == identity);
        if (selected is null)
        {
            ClearEditor();
            SetStatus(
                "선택한 테마의 현재 DB identity를 다시 확인하지 못해 편집을 시작하지 않았습니다.",
                LegacyOperatorCatalogStatusKind.Error);
            Touch();
            return CreateSnapshot();
        }

        return await SelectAsync(selected.ResultId, cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// Opens the EList editor for the exact identity already selected by UC6.
    /// </summary>
    public async Task<LegacyOperatorCatalogWorkflowSnapshot> OpenExpertForEditAsync(
        ExpertSelectionIdentity identity,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(identity);
        await OpenAsync(LegacyOperatorCatalogEntity.Expert, cancellationToken)
            .ConfigureAwait(false);
        await SearchAsync(identity.ExpertName, ThemeSession.PreMarket, cancellationToken)
            .ConfigureAwait(false);

        var selected = _results.SingleOrDefault(row => row.ExpertIdentity == identity);
        if (selected is null)
        {
            ClearEditor();
            SetStatus(
                "선택한 전문가의 현재 DB identity를 다시 확인하지 못해 편집을 시작하지 않았습니다.",
                LegacyOperatorCatalogStatusKind.Error);
            Touch();
            return CreateSnapshot();
        }

        return await SelectAsync(selected.ResultId, cancellationToken).ConfigureAwait(false);
    }

    public LegacyOperatorCatalogWorkflowSnapshot Close()
    {
        _isOpen = false;
        _mode = LegacyOperatorCatalogMode.List;
        ClearEditor();
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> SearchAsync(
        string query,
        ThemeSession nxtSession,
        CancellationToken cancellationToken = default)
    {
        EnsureCatalogListMode();
        _query = ValidateQuery(query);
        if (nxtSession is not (ThemeSession.PreMarket or ThemeSession.AfterMarket))
        {
            throw new ArgumentOutOfRangeException(nameof(nxtSession));
        }

        _nxtSession = nxtSession;
        _mode = LegacyOperatorCatalogMode.List;
        ClearEditor();
        try
        {
            await LoadCatalogListCoreAsync(cancellationToken).ConfigureAwait(false);
            SetStatus("목록 검색을 완료했습니다.", LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            _results.Clear();
            _resultsAreTruncated = false;
            SetStatus("목록을 검색하지 못했습니다.", LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> SelectAsync(
        string resultId,
        CancellationToken cancellationToken = default)
    {
        EnsureCatalogListMode();
        var selected = FindResult(resultId);
        try
        {
            _draftRows.Clear();
            if (selected.ThemeIdentity is { } themeIdentity)
            {
                var stored = await _themePersistenceService.ReadStoredAsync(
                    themeIdentity,
                    cancellationToken).ConfigureAwait(false);
                if (stored.IsTruncated || stored.Identity != themeIdentity)
                {
                    throw new ThemeSelectionDataException(
                        "A complete, correlated stored ThemeA definition is required for editing.");
                }

                foreach (var item in stored.Items.OrderBy(static item => item.InputIndex))
                {
                    _draftRows.Add(DraftRowState.ForTheme(
                        NewOpaqueId("catalog-draft"),
                        item.ItemCode,
                        item.ItemName,
                        requiresCurrentIdentityValidation: false));
                }

                _expectedThemeIdentity = themeIdentity;
                _expectedExpertIdentity = null;
                _codeLabel = themeIdentity.ThemeCode;
                _editorName = themeIdentity.ThemeTitle;
                _newThemeMarket = themeIdentity.Market;
            }
            else if (selected.ExpertIdentity is { } expertIdentity)
            {
                var preview = await _expertSelectionService.PreviewAsync(
                    expertIdentity,
                    LegacyExpertSelectionService.MaximumPreviewItems,
                    cancellationToken).ConfigureAwait(false);
                if (preview.IsTruncated || preview.Identity != expertIdentity)
                {
                    throw new ExpertSelectionDataException(
                        "A complete, correlated EList preview is required for editing.");
                }

                foreach (var item in preview.Items.OrderBy(static item => item.PlayIndex))
                {
                    _draftRows.Add(DraftRowState.ForExpert(
                        NewOpaqueId("catalog-draft"),
                        item.StockCode,
                        item.StockName,
                        item.BuyAmount));
                }

                _expectedThemeIdentity = null;
                _expectedExpertIdentity = expertIdentity;
                _codeLabel = expertIdentity.ExpertCode;
                _editorName = expertIdentity.ExpertName;
            }
            else
            {
                throw new InvalidOperationException("The catalog result has no trusted identity.");
            }

            _activeDraftRowId = _draftRows.FirstOrDefault()?.RowId;
            _selectedResultId = resultId;
            _mode = LegacyOperatorCatalogMode.Edit;
            ResetThemeNameAvailability();
            ResetStockSearch();
            SetStatus("편집할 항목을 불러왔습니다.", LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            ClearEditor();
            SetStatus(
                "전체 구성 데이터를 다시 읽지 못해 편집을 시작하지 않았습니다.",
                LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> BeginCreateThemeAsync(
        ThemeMarket market,
        CancellationToken cancellationToken = default)
    {
        EnsureOpenEntity(LegacyOperatorCatalogEntity.Theme);
        EnsureCatalogListMode();
        if (!CanMutateCurrentEntity())
        {
            return RejectUnavailableWrite();
        }

        try
        {
            _codeLabel = await _themePersistenceService.SuggestNextCodeAsync(
                market,
                cancellationToken).ConfigureAwait(false);
            _newThemeMarket = market;
            _mode = LegacyOperatorCatalogMode.Create;
            _editorName = string.Empty;
            _selectedResultId = null;
            _expectedThemeIdentity = null;
            _expectedExpertIdentity = null;
            _draftRows.Clear();
            _activeDraftRowId = null;
            ResetThemeNameAvailability();
            ResetStockSearch();
            SetStatus("새 ThemeA 항목을 입력하세요.", LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            ClearEditor();
            SetStatus("새 ThemeA 코드를 만들지 못했습니다.", LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    /// <summary>
    /// Changes the market of the legacy ThemeA create dialog without closing and
    /// recreating it. The next code is resolved before any correlated editor state
    /// is committed, so a failed read cannot leave a mixed market/code/draft state.
    /// </summary>
    public async Task<LegacyOperatorCatalogWorkflowSnapshot> ChangeCreateThemeMarketAsync(
        ThemeMarket market,
        string editorName,
        CancellationToken cancellationToken = default)
    {
        EnsureOpenEntity(LegacyOperatorCatalogEntity.Theme);
        if (_mode != LegacyOperatorCatalogMode.Create)
        {
            throw new InvalidOperationException(
                "The ThemeA market can be changed only while creating a new item.");
        }

        if (market is not (ThemeMarket.Krx or ThemeMarket.Nxt))
        {
            throw new ArgumentOutOfRangeException(nameof(market));
        }

        var validatedEditorName = ValidateOptionalEditorText(editorName, 128);
        if (!CanMutateCurrentEntity())
        {
            return RejectUnavailableWrite();
        }

        if (market == _newThemeMarket)
        {
            _editorName = validatedEditorName;
            ResetThemeNameAvailability();
            SetStatus(
                "현재 ThemeA 시장을 유지했습니다.",
                LegacyOperatorCatalogStatusKind.Information);
            Touch();
            return CreateSnapshot();
        }

        try
        {
            // Keep this read ahead of every editor mutation. A provider failure must
            // preserve the old market, code, rows, selection, and editor name exactly.
            var nextCode = ValidateRequiredText(
                await _themePersistenceService.SuggestNextCodeAsync(
                    market,
                    cancellationToken).ConfigureAwait(false),
                64);
            cancellationToken.ThrowIfCancellationRequested();

            var removedCount = _draftRows.RemoveAll(row =>
                !IsDraftRowCompatible(row, market));
            _newThemeMarket = market;
            _codeLabel = nextCode;
            _editorName = validatedEditorName;
            _expectedThemeIdentity = null;
            _expectedExpertIdentity = null;
            if (_activeDraftRowId is not null && !_draftRows.Any(row =>
                    string.Equals(row.RowId, _activeDraftRowId, StringComparison.Ordinal)))
            {
                _activeDraftRowId = _draftRows.FirstOrDefault()?.RowId;
            }

            if (_selectedStockResultId is not null)
            {
                var selectedStock = _stockResults.SingleOrDefault(row => string.Equals(
                    row.ResultId,
                    _selectedStockResultId,
                    StringComparison.Ordinal));
                if (selectedStock is null || !IsCompatible(selectedStock.Item, market))
                {
                    _selectedStockResultId = null;
                }
            }

            ResetThemeNameAvailability();
            if (removedCount > 0)
            {
                SetStatus(
                    $"ThemeA 시장을 {ThemeMarketDisplayName(market)}로 변경하고 새 시장과 맞지 않는 초안 종목 {removedCount}개를 제외했습니다.",
                    LegacyOperatorCatalogStatusKind.Warning);
            }
            else
            {
                SetStatus(
                    $"ThemeA 시장을 {ThemeMarketDisplayName(market)}로 변경했습니다.",
                    LegacyOperatorCatalogStatusKind.Information);
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            SetStatus(
                "새 시장의 ThemeA 코드를 확인하지 못해 시장과 초안을 변경하지 않았습니다.",
                LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> CheckThemeNameAvailabilityAsync(
        string editorName,
        CancellationToken cancellationToken = default)
    {
        EnsureOpenEntity(LegacyOperatorCatalogEntity.Theme);
        EnsureEditing();
        _editorName = ValidateRequiredText(editorName, 128);
        _checkedThemeName = _editorName;
        _themeNameAvailability = LegacyThemeNameAvailability.Indeterminate;

        try
        {
            var duplicate = await _themePersistenceService.ThemeNameExistsAsync(
                _newThemeMarket,
                _editorName,
                cancellationToken).ConfigureAwait(false);

            if (duplicate)
            {
                _themeNameAvailability = LegacyThemeNameAvailability.Duplicate;
                SetStatus(
                    ThemeNameDuplicateMessage,
                    LegacyOperatorCatalogStatusKind.Warning);
            }
            else
            {
                _themeNameAvailability = LegacyThemeNameAvailability.Available;
                SetStatus(
                    ThemeNameAvailableMessage,
                    LegacyOperatorCatalogStatusKind.Information);
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            SetStatus(
                "테마명 중복 여부를 확인하지 못했습니다. DB 연결을 확인하세요.",
                LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> BeginCreateExpertAsync(
        CancellationToken cancellationToken = default)
    {
        EnsureOpenEntity(LegacyOperatorCatalogEntity.Expert);
        EnsureCatalogListMode();
        if (!CanMutateCurrentEntity())
        {
            return RejectUnavailableWrite();
        }

        try
        {
            _codeLabel = await _expertPersistenceService.SuggestNextCodeAsync(cancellationToken)
                .ConfigureAwait(false);
            _mode = LegacyOperatorCatalogMode.Create;
            _editorName = string.Empty;
            _selectedResultId = null;
            _expectedThemeIdentity = null;
            _expectedExpertIdentity = null;
            _draftRows.Clear();
            _activeDraftRowId = null;
            ResetStockSearch();
            SetStatus("새 EList 항목을 입력하세요.", LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            ClearEditor();
            SetStatus("새 EList 코드를 만들지 못했습니다.", LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> SearchStocksAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        EnsureDraftRowsEditable();
        _stockQuery = ValidateOptionalStockQuery(query);
        _selectedStockResultId = null;
        try
        {
            var result = _stockQuery.Length == 0
                ? await _stockSearchService.SearchAllAsync(
                    MaximumStockResults,
                    cancellationToken).ConfigureAwait(false)
                : await _stockSearchService.SearchAsync(
                    _stockQuery,
                    MaximumStockResults,
                    cancellationToken).ConfigureAwait(false);
            if (result is null ||
                !string.Equals(result.Query, _stockQuery, StringComparison.Ordinal) ||
                result.Items is null || result.Items.Count > MaximumStockResults)
            {
                throw new InvalidDataException(
                    "The operator-catalog stock result is not correlated or bounded.");
            }

            _stockResults.Clear();
            _stockResults.AddRange(result.Items.Select(item => new StockResultState(
                NewOpaqueId("catalog-stock"),
                item)));
            _stockResultsAreTruncated = result.IsTruncated;
            SetStatus("종목 검색을 완료했습니다.", LegacyOperatorCatalogStatusKind.Information);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            _stockResults.Clear();
            _stockResultsAreTruncated = false;
            SetStatus("종목 master를 검색하지 못했습니다.", LegacyOperatorCatalogStatusKind.Error);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot SelectStock(string resultId)
    {
        EnsureDraftRowsEditable();
        var stock = FindStock(resultId);
        if (!IsCompatible(stock.Item))
        {
            SetStatus("현재 편집 시장과 일치하지 않는 종목입니다.", LegacyOperatorCatalogStatusKind.Warning);
        }
        else
        {
            _selectedStockResultId = resultId;
            SetStatus("추가할 종목을 선택했습니다.", LegacyOperatorCatalogStatusKind.Information);
        }

        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot AddSelectedStock(decimal? buyAmount)
    {
        EnsureDraftRowsEditable();
        var selected = _selectedStockResultId is null
            ? throw new InvalidOperationException("Select a stock-master result first.")
            : FindStock(_selectedStockResultId);
        if (!IsCompatible(selected.Item))
        {
            throw new InvalidOperationException("The selected stock is incompatible with this catalog.");
        }

        DraftRowState row;
        if (_entity == LegacyOperatorCatalogEntity.Theme)
        {
            if (_draftRows.Count >= LegacyThemeCatalogPersistenceService.MaximumItems)
            {
                throw new InvalidOperationException("ThemeA item limit reached.");
            }

            var itemCode = ThemeItemCode(selected.Item);
            var itemName = ThemeStoredName(selected.Item);
            row = DraftRowState.ForTheme(
                NewOpaqueId("catalog-draft"),
                itemCode,
                itemName,
                requiresCurrentIdentityValidation: true);
        }
        else
        {
            if (_draftRows.Count >= LegacyExpertCatalogPersistenceService.MaximumRecommendations)
            {
                throw new InvalidOperationException("EList recommendation limit reached.");
            }

            row = DraftRowState.ForExpert(
                NewOpaqueId("catalog-draft"),
                selected.Item.Code,
                selected.Item.Name,
                buyAmount: null);
        }

        _draftRows.Add(row);
        _activeDraftRowId = row.RowId;
        _selectedStockResultId = null;
        SetStatus("종목을 편집 목록에 추가했습니다.", LegacyOperatorCatalogStatusKind.Information);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot MoveDraftRow(
        string rowId,
        LegacyOperatorCatalogMoveDirection direction)
    {
        EnsureDraftRowsEditable();
        var index = FindDraftIndex(rowId);
        var target = direction == LegacyOperatorCatalogMoveDirection.Up ? index - 1 : index + 1;
        var activeChanged = !string.Equals(
            _activeDraftRowId,
            rowId,
            StringComparison.Ordinal);
        _activeDraftRowId = rowId;
        if (target >= 0 && target < _draftRows.Count)
        {
            (_draftRows[index], _draftRows[target]) = (_draftRows[target], _draftRows[index]);
            SetStatus("표시 순서를 변경했습니다.", LegacyOperatorCatalogStatusKind.Information);
            Touch();
        }
        else if (activeChanged)
        {
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot ReorderDraftRow(
        string sourceRowId,
        string targetRowId,
        LegacyDragDropPosition position)
    {
        ArgumentNullException.ThrowIfNull(sourceRowId);
        ArgumentNullException.ThrowIfNull(targetRowId);
        EnsureDraftRowsEditable();

        var sourceIndex = _draftRows.FindIndex(row =>
            string.Equals(row.RowId, sourceRowId, StringComparison.Ordinal));
        var targetIndex = _draftRows.FindIndex(row =>
            string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
        if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
        {
            return CreateSnapshot();
        }

        var activeChanged = !string.Equals(
            _activeDraftRowId,
            sourceRowId,
            StringComparison.Ordinal);
        _activeDraftRowId = sourceRowId;

        var candidate = _draftRows.ToList();
        var movingRow = candidate[sourceIndex];
        candidate.RemoveAt(sourceIndex);
        var remainingTargetIndex = candidate.FindIndex(row =>
            string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
        var insertionIndex = position switch
        {
            LegacyDragDropPosition.Before => remainingTargetIndex,
            LegacyDragDropPosition.After => remainingTargetIndex + 1,
            _ => throw new ArgumentOutOfRangeException(nameof(position), position, null)
        };
        candidate.Insert(insertionIndex, movingRow);

        if (_draftRows.Select(static row => row.RowId).SequenceEqual(
                candidate.Select(static row => row.RowId),
                StringComparer.Ordinal))
        {
            if (activeChanged)
            {
                Touch();
            }
            return CreateSnapshot();
        }

        _draftRows.Clear();
        _draftRows.AddRange(candidate);
        SetStatus("표시 순서를 변경했습니다.", LegacyOperatorCatalogStatusKind.Information);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot SetDraftBuyAmount(
        string rowId,
        decimal? buyAmount)
    {
        EnsureDraftRowsEditable();
        if (_entity != LegacyOperatorCatalogEntity.Expert)
        {
            throw new InvalidOperationException(
                "Only EList recommendation rows have a buy amount.");
        }

        if (buyAmount is not null &&
            (decimal.Truncate(buyAmount.Value) != buyAmount.Value ||
             buyAmount.Value <= 0 || buyAmount.Value > MaximumWebSafeInteger))
        {
            throw new ArgumentOutOfRangeException(nameof(buyAmount));
        }

        var index = FindDraftIndex(rowId);
        _draftRows[index] = _draftRows[index] with { BuyAmount = buyAmount };
        _activeDraftRowId = rowId;
        SetStatus("추천 종목의 매수가를 변경했습니다. 저장을 눌러 DB에 반영하세요.",
            LegacyOperatorCatalogStatusKind.Information);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot RemoveDraftRow(string rowId)
    {
        EnsureDraftRowsEditable();
        var index = FindDraftIndex(rowId);
        _draftRows.RemoveAt(index);
        _activeDraftRowId = _draftRows.Count == 0
            ? null
            : _draftRows[Math.Min(index, _draftRows.Count - 1)].RowId;
        SetStatus("편집 목록에서 종목을 삭제했습니다.", LegacyOperatorCatalogStatusKind.Information);
        Touch();
        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot SelectDraftRow(string rowId)
    {
        EnsureDraftRowsEditable();
        var canonicalRowId = _draftRows[FindDraftIndex(rowId)].RowId;
        if (!string.Equals(_activeDraftRowId, canonicalRowId, StringComparison.Ordinal))
        {
            _activeDraftRowId = canonicalRowId;
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyOperatorCatalogWorkflowSnapshot RemoveActiveDraftRow()
    {
        EnsureDraftRowsEditable();
        if (_activeDraftRowId is null)
        {
            return CreateSnapshot();
        }

        return RemoveDraftRow(_activeDraftRowId);
    }

    public LegacyOperatorCatalogWorkflowSnapshot ClearThemeDraftRows()
    {
        EnsureOpenEntity(LegacyOperatorCatalogEntity.Theme);
        EnsureEditing();
        if (_draftRows.Count == 0)
        {
            return CreateSnapshot();
        }

        _draftRows.Clear();
        _activeDraftRowId = null;
        SetStatus("모든 구성 종목을 편집 목록에서 삭제했습니다.",
            LegacyOperatorCatalogStatusKind.Information);
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyOperatorCatalogWorkflowSnapshot> SaveAsync(
        string editorName,
        CancellationToken cancellationToken = default)
    {
        EnsureEditing();

        // UC6 silently ignored Save when the selected expert had no recommendation rows.
        // This check intentionally precedes name validation and does not touch revision/status.
        if (_entity == LegacyOperatorCatalogEntity.Expert &&
            _mode == LegacyOperatorCatalogMode.Edit &&
            _draftRows.Count == 0)
        {
            return CreateSnapshot();
        }

        if (!CanMutateCurrentEntity())
        {
            return RejectUnavailableWrite();
        }

        _editorName = _entity switch
        {
            LegacyOperatorCatalogEntity.Expert when _mode == LegacyOperatorCatalogMode.Edit =>
                (_expectedExpertIdentity ?? throw new InvalidOperationException(
                    "The trusted EList edit identity is missing.")).ExpertName,
            LegacyOperatorCatalogEntity.Theme when _mode == LegacyOperatorCatalogMode.Edit =>
                ValidateExistingThemeEditorName(
                    editorName,
                    _expectedThemeIdentity ?? throw new InvalidOperationException(
                        "The trusted ThemeA edit identity is missing.")),
            _ => ValidateRequiredText(editorName, 128)
        };

        if (_entity == LegacyOperatorCatalogEntity.Theme)
        {
            var items = _draftRows.Select((row, index) =>
                new ThemeCatalogItem(index, row.Code, row.Name)).ToArray();
            if (_mode == LegacyOperatorCatalogMode.Create)
            {
                var definition = new ThemeCatalogDefinition(
                    _newThemeMarket,
                    _codeLabel,
                    _editorName,
                    Array.AsReadOnly(items));
                return await ExecuteWriteAsync(
                    ThemeSaveSuccessMessage,
                    async token =>
                    {
                        if (await _themePersistenceService.ThemeNameExistsAsync(
                                _newThemeMarket,
                                _editorName,
                                token).ConfigureAwait(false))
                        {
                            throw new ThemeNameDuplicateException();
                        }

                        await _identityValidationService.ValidateThemeItemsAsync(items, token)
                            .ConfigureAwait(false);
                    },
                    token => _themePersistenceService.CreateAsync(definition, token),
                    cancellationToken).ConfigureAwait(false);
            }

            var expected = _expectedThemeIdentity ?? throw new InvalidOperationException(
                "The trusted ThemeA edit identity is missing.");
            var newItems = _draftRows
                .Where(static row => row.RequiresCurrentIdentityValidation)
                .Select((row, index) => new ThemeCatalogItem(index, row.Code, row.Name))
                .ToArray();
            return await ExecuteWriteAsync(
                ThemeSaveSuccessMessage,
                token => newItems.Length == 0
                    ? Task.CompletedTask
                    : _identityValidationService.ValidateThemeItemsAsync(newItems, token),
                token => _themePersistenceService.ReplaceAsync(
                    expected,
                    _editorName,
                    items,
                    token),
                cancellationToken).ConfigureAwait(false);
        }

        if (_draftRows.Any(static row => row.BuyAmount is null))
        {
            SetStatus("매수가를 입력하세요.", LegacyOperatorCatalogStatusKind.Warning);
            Touch();
            return CreateSnapshot();
        }

        var recommendations = _draftRows.Select((row, index) =>
            new ExpertCatalogRecommendation(
                index,
                row.Code,
                row.Name,
                row.BuyAmount!.Value)).ToArray();
        if (_mode == LegacyOperatorCatalogMode.Create)
        {
            if (recommendations.Length != 0)
            {
                throw new InvalidOperationException(
                    "The legacy EList create dialog cannot contain recommendations.");
            }

            var definition = new ExpertCatalogDefinition(
                new ExpertSelectionIdentity(_codeLabel, _editorName),
                Array.AsReadOnly(recommendations));
            return await ExecuteWriteAsync(
                ExpertSaveSuccessMessage,
                static _ => Task.CompletedTask,
                token => _expertPersistenceService.CreateAsync(definition, token),
                cancellationToken).ConfigureAwait(false);
        }

        var expectedExpert = _expectedExpertIdentity ?? throw new InvalidOperationException(
            "The trusted EList edit identity is missing.");
        return await ExecuteWriteAsync(
            ExpertSaveSuccessMessage,
            token => _identityValidationService.ValidateExpertRecommendationsAsync(
                recommendations,
                token),
            token => _expertPersistenceService.ReplaceAsync(
                expectedExpert,
                _editorName,
                recommendations,
                token),
            cancellationToken).ConfigureAwait(false);
    }

    public Task<LegacyOperatorCatalogWorkflowSnapshot> DeleteAsync(
        CancellationToken cancellationToken = default)
    {
        EnsureEditing();
        if (_mode != LegacyOperatorCatalogMode.Edit)
        {
            throw new InvalidOperationException("Only a freshly loaded catalog row can be deleted.");
        }

        if (_entity == LegacyOperatorCatalogEntity.Theme)
        {
            var expected = _expectedThemeIdentity ?? throw new InvalidOperationException(
                "The trusted ThemeA delete identity is missing.");
            return ExecuteWriteAsync(
                ThemeDeleteSuccessMessage,
                validation: null,
                token => _themePersistenceService.DeleteAsync(expected, token),
                cancellationToken);
        }

        var expectedExpert = _expectedExpertIdentity ?? throw new InvalidOperationException(
            "The trusted EList delete identity is missing.");
        return ExecuteWriteAsync(
            ExpertDeleteSuccessMessage,
            validation: null,
            token => _expertPersistenceService.DeleteAsync(expectedExpert, token),
            cancellationToken);
    }

    public LegacyOperatorCatalogWorkflowSnapshot InvalidateBrowserCorrelation()
    {
        _writeSafety.InvalidateCorrelation();
        if (_writeSafety.IsQuarantined(out var reason))
        {
            SetStatus(reason ?? OutcomeUnknownMessage, LegacyOperatorCatalogStatusKind.Error);
            Touch();
        }

        return CreateSnapshot();
    }

    private async Task<LegacyOperatorCatalogWorkflowSnapshot> ExecuteWriteAsync(
        string successMessage,
        Func<CancellationToken, Task>? validation,
        Func<CancellationToken, Task<OperatorCatalogMutationReceipt>> mutation,
        CancellationToken cancellationToken)
    {
        if (!CanMutateCurrentEntity())
        {
            return RejectUnavailableWrite();
        }

        if (!await _writeSafety.WriteGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
        {
            SetStatus("다른 ThemeA/EList 쓰기가 진행 중입니다.", LegacyOperatorCatalogStatusKind.Warning);
            Touch();
            return CreateSnapshot();
        }

        var dispatched = false;
        try
        {
            if (!_writeSafety.TryBegin(out var generation, out var reason))
            {
                SetStatus(reason, LegacyOperatorCatalogStatusKind.Error);
                Touch();
                return CreateSnapshot();
            }

            if (validation is not null)
            {
                await validation(cancellationToken).ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();
            }

            if (!_writeSafety.IsCurrent(generation) || !_writeSafety.MarkDispatched(generation))
            {
                SetStatus(
                    "브라우저 상관관계가 변경되어 DB transaction을 시작하지 않았습니다.",
                    LegacyOperatorCatalogStatusKind.Warning);
                Touch();
                return CreateSnapshot();
            }

            dispatched = true;
            var receipt = await mutation(cancellationToken).ConfigureAwait(false);
            if (receipt.OperationId == Guid.Empty || !_writeSafety.IsCurrent(generation))
            {
                throw new OperatorCatalogMutationException(
                    OutcomeUnknownMessage,
                    outcomeUnknown: true);
            }

            _writeSafety.Complete();
            dispatched = false;
            ClearEditor();
            try
            {
                await LoadCatalogListCoreAsync(cancellationToken).ConfigureAwait(false);
                SetStatus(successMessage, LegacyOperatorCatalogStatusKind.Information);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                SetStatus(
                    $"{successMessage} 목록 새로고침은 취소되었습니다.",
                    LegacyOperatorCatalogStatusKind.Warning);
            }
            catch
            {
                SetStatus(
                    $"{successMessage} 다만 목록을 새로 읽지 못했으므로 다시 검색하세요.",
                    LegacyOperatorCatalogStatusKind.Warning);
            }
        }
        catch (OperatorCatalogConflictException)
        {
            _writeSafety.Complete();
            dispatched = false;
            SetStatus(
                "DB 내용이 바뀌었거나 코드/이름이 중복되어 transaction이 rollback되었습니다. 목록을 다시 읽으세요.",
                LegacyOperatorCatalogStatusKind.Warning);
        }
        catch (ThemeNameDuplicateException) when (!dispatched)
        {
            _writeSafety.Complete();
            _checkedThemeName = _editorName;
            _themeNameAvailability = LegacyThemeNameAvailability.Duplicate;
            SetStatus(ThemeNameDuplicateMessage, LegacyOperatorCatalogStatusKind.Warning);
        }
        catch (StockMasterIdentityDataException) when (!dispatched)
        {
            _writeSafety.Complete();
            dispatched = false;
            SetStatus(
                "모든 종목은 현재 stock master의 유일한 코드·이름과 일치해야 합니다.",
                LegacyOperatorCatalogStatusKind.Error);
        }
        catch (OperatorCatalogMutationException exception)
        {
            if (exception.OutcomeUnknown)
            {
                var quarantine = _writeSafety.Quarantine(OutcomeUnknownMessage);
                SetStatus(quarantine, LegacyOperatorCatalogStatusKind.Error);
            }
            else
            {
                _writeSafety.Complete();
                SetStatus(
                    "DB transaction이 rollback되었으며 자동으로 재시도하지 않았습니다.",
                    LegacyOperatorCatalogStatusKind.Error);
            }

            dispatched = false;
        }
        catch (OperationCanceledException) when (dispatched)
        {
            dispatched = false;
            SetStatus(
                _writeSafety.Quarantine(OutcomeUnknownMessage),
                LegacyOperatorCatalogStatusKind.Error);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            _writeSafety.Complete();
            throw;
        }
        catch
        {
            if (dispatched)
            {
                SetStatus(
                    _writeSafety.Quarantine(OutcomeUnknownMessage),
                    LegacyOperatorCatalogStatusKind.Error);
            }
            else
            {
                _writeSafety.Complete();
                SetStatus(
                    "DB transaction 시작 전에 요청이 실패했으며 자동으로 재시도하지 않았습니다.",
                    LegacyOperatorCatalogStatusKind.Error);
            }

            dispatched = false;
        }
        finally
        {
            if (dispatched)
            {
                _writeSafety.Quarantine(OutcomeUnknownMessage);
            }

            _writeSafety.WriteGate.Release();
            Touch();
        }

        return CreateSnapshot();
    }

    private async Task LoadCatalogListCoreAsync(CancellationToken cancellationToken)
    {
        _results.Clear();
        _selectedResultId = null;
        if (_entity == LegacyOperatorCatalogEntity.Theme)
        {
            var result = await _themeSelectionService.SearchAsync(
                _query,
                _nxtSession,
                MaximumCatalogResults,
                cancellationToken).ConfigureAwait(false);
            _query = result.Query;
            _resultsAreTruncated = result.IsTruncated;
            _results.AddRange(result.Items.Select(item => CatalogResultState.ForTheme(
                NewOpaqueId("catalog-result"),
                item.Identity,
                item.Source)));
        }
        else
        {
            var result = await _expertSelectionService.SearchAsync(
                _query,
                MaximumExpertCatalogResults,
                cancellationToken).ConfigureAwait(false);
            _query = result.Query;
            _resultsAreTruncated = result.IsTruncated;
            _results.AddRange(result.Items.Select(item => CatalogResultState.ForExpert(
                NewOpaqueId("catalog-result"),
                item.Identity,
                item.Source)));
        }
    }

    private LegacyOperatorCatalogWorkflowSnapshot CreateSnapshot()
    {
        var quarantined = _writeSafety.IsQuarantined(out var quarantineReason);
        var canMutate = CanMutateCurrentEntity() && !quarantined;
        var resultViews = _results.Select(row => new LegacyOperatorCatalogResultView(
            row.ResultId,
            row.CodeLabel,
            row.DisplayName,
            row.Scope,
            string.Equals(row.ResultId, _selectedResultId, StringComparison.Ordinal))).ToArray();
        var draftViews = _draftRows.Select((row, index) =>
            new LegacyOperatorCatalogDraftRowView(
                row.RowId,
                _entity == LegacyOperatorCatalogEntity.Theme &&
                    _newThemeMarket == ThemeMarket.Nxt &&
                    !row.Name.EndsWith("(NXT)", StringComparison.Ordinal)
                    ? row.Name + "(NXT)"
                    : row.Name,
                row.MarketLabel,
                row.BuyAmount,
                index)).ToArray();
        var stockViews = _stockResults.Select(row =>
            new LegacyOperatorCatalogStockResultView(
                row.ResultId,
                row.Item.Name,
                StockMarketId(row.Item.Market),
                IsCompatible(row.Item),
                string.Equals(
                    row.ResultId,
                    _selectedStockResultId,
                    StringComparison.Ordinal))).ToArray();

        return new LegacyOperatorCatalogWorkflowSnapshot(
            _revision,
            _isOpen,
            _entity,
            _mode,
            _query,
            _nxtSession,
            _newThemeMarket,
            Array.AsReadOnly(resultViews),
            _resultsAreTruncated,
            _selectedResultId,
            _codeLabel,
            _editorName,
            _themeNameAvailability,
            _checkedThemeName,
            Array.AsReadOnly(draftViews),
            _activeDraftRowId,
            _stockQuery,
            Array.AsReadOnly(stockViews),
            _stockResultsAreTruncated,
            _selectedStockResultId,
            _schemaValidated,
            quarantined,
            canMutate && _mode == LegacyOperatorCatalogMode.List,
            _schemaValidated && _isOpen &&
                _entity == LegacyOperatorCatalogEntity.Theme &&
                _mode is LegacyOperatorCatalogMode.Create or LegacyOperatorCatalogMode.Edit,
            canMutate &&
                (_mode is LegacyOperatorCatalogMode.Create or LegacyOperatorCatalogMode.Edit),
            canMutate && _mode == LegacyOperatorCatalogMode.Edit,
            canMutate &&
                (_mode is LegacyOperatorCatalogMode.Create or LegacyOperatorCatalogMode.Edit) &&
                (_entity != LegacyOperatorCatalogEntity.Expert ||
                    _mode == LegacyOperatorCatalogMode.Edit) &&
                _selectedStockResultId is not null,
            quarantined && string.IsNullOrWhiteSpace(_statusMessage)
                ? quarantineReason ?? OutcomeUnknownMessage
                : _statusMessage,
            quarantined ? LegacyOperatorCatalogStatusKind.Error : _statusKind);
    }

    private bool CanMutateCurrentEntity() =>
        _schemaValidated && !_writeSafety.IsQuarantined(out _) &&
        (_entity == LegacyOperatorCatalogEntity.Theme
            ? _themePersistenceService.CanMutate
            : _expertPersistenceService.CanMutate);

    private LegacyOperatorCatalogWorkflowSnapshot RejectUnavailableWrite()
    {
        SetStatus(
            _writeSafety.IsQuarantined(out var reason)
                ? reason ?? OutcomeUnknownMessage
                : "ThemeA/EList DB 쓰기를 사용할 수 없습니다.",
            LegacyOperatorCatalogStatusKind.Error);
        Touch();
        return CreateSnapshot();
    }

    private void ClearEditor()
    {
        _mode = LegacyOperatorCatalogMode.List;
        _selectedResultId = null;
        _expectedThemeIdentity = null;
        _expectedExpertIdentity = null;
        _codeLabel = string.Empty;
        _editorName = string.Empty;
        _draftRows.Clear();
        _activeDraftRowId = null;
        ResetThemeNameAvailability();
        ResetStockSearch();
    }

    private void ResetThemeNameAvailability()
    {
        _themeNameAvailability = LegacyThemeNameAvailability.NotChecked;
        _checkedThemeName = string.Empty;
    }

    private void ResetStockSearch()
    {
        _stockQuery = string.Empty;
        _stockResults.Clear();
        _stockResultsAreTruncated = false;
        _selectedStockResultId = null;
    }

    private void EnsureOpen()
    {
        if (!_isOpen)
        {
            throw new InvalidOperationException("The ThemeA/EList workspace is not open.");
        }
    }

    private void EnsureOpenEntity(LegacyOperatorCatalogEntity entity)
    {
        EnsureOpen();
        if (_entity != entity)
        {
            throw new InvalidOperationException("The requested catalog entity is not active.");
        }
    }

    private void EnsureEditing()
    {
        EnsureOpen();
        if (_mode == LegacyOperatorCatalogMode.List)
        {
            throw new InvalidOperationException("Start a new or existing catalog edit first.");
        }
    }

    private void EnsureCatalogListMode()
    {
        EnsureOpen();
        if (_mode != LegacyOperatorCatalogMode.List)
        {
            throw new InvalidOperationException(
                "The legacy parent catalog is disabled while its child dialog is open.");
        }
    }

    private void EnsureDraftRowsEditable()
    {
        EnsureEditing();
        if (_entity == LegacyOperatorCatalogEntity.Expert &&
            _mode != LegacyOperatorCatalogMode.Edit)
        {
            throw new InvalidOperationException(
                "The legacy EList create dialog only accepts an expert name.");
        }
    }

    private CatalogResultState FindResult(string resultId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(resultId);
        return _results.SingleOrDefault(row =>
                string.Equals(row.ResultId, resultId, StringComparison.Ordinal)) ??
            throw new ArgumentException("The opaque catalog result id is invalid.", nameof(resultId));
    }

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

    private int FindDraftIndex(string rowId)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(rowId);
        var index = _draftRows.FindIndex(row =>
            string.Equals(row.RowId, rowId, StringComparison.Ordinal));
        return index >= 0
            ? index
            : throw new ArgumentException("The opaque draft row id is invalid.", nameof(rowId));
    }

    private bool IsCompatible(StockSearchItem item) => _entity switch
    {
        LegacyOperatorCatalogEntity.Expert => true,
        LegacyOperatorCatalogEntity.Theme => IsCompatible(item, _newThemeMarket),
        _ => false
    };

    private static bool IsCompatible(StockSearchItem item, ThemeMarket market) => market switch
    {
        ThemeMarket.Krx => item.Market is StockMarket.Kospi or StockMarket.Kosdaq,
        ThemeMarket.Nxt => item.Market is StockMarket.NxtKospi or StockMarket.NxtKosdaq,
        _ => false
    };

    private static bool IsDraftRowCompatible(DraftRowState row, ThemeMarket market) =>
        row.Code.Length > 0 && (market switch
        {
            ThemeMarket.Krx => row.Code[0] is 'P' or 'D',
            ThemeMarket.Nxt => row.Code[0] is 'X' or 'T',
            _ => false
        });

    private static string ThemeMarketDisplayName(ThemeMarket market) => market switch
    {
        ThemeMarket.Krx => "KRX",
        ThemeMarket.Nxt => "NXT",
        _ => throw new ArgumentOutOfRangeException(nameof(market))
    };

    private static string ThemeItemCode(StockSearchItem item) => item.Market switch
    {
        StockMarket.Kospi => "P" + item.Code,
        StockMarket.Kosdaq => "D" + item.Code,
        StockMarket.NxtKospi => "X" + item.Code,
        StockMarket.NxtKosdaq => "T" + item.Code,
        _ => throw new ArgumentOutOfRangeException(nameof(item))
    };

    private static string ThemeStoredName(StockSearchItem item)
    {
        if (item.Market is not (StockMarket.NxtKospi or StockMarket.NxtKosdaq))
        {
            return item.Name;
        }

        const string suffix = "(NXT)";
        if (!item.Name.EndsWith(suffix, StringComparison.Ordinal))
        {
            throw new InvalidOperationException("An NXT stock display name is invalid.");
        }

        return ValidateRequiredText(item.Name[..^suffix.Length], 200);
    }

    private static string ValidateQuery(string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        var normalized = value.Trim();
        if (normalized.Length > 64 || !IsSafeText(normalized, allowEmpty: true))
        {
            throw new ArgumentException("A safe catalog query is required.", nameof(value));
        }

        return normalized;
    }

    private static string ValidateRequiredText(string value, int maximumLength)
    {
        ArgumentNullException.ThrowIfNull(value);
        if (value.Length is < 1 || value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            !IsSafeText(value, allowEmpty: false))
        {
            throw new ArgumentException("A safe canonical text value is required.", nameof(value));
        }

        return value;
    }

    private static string ValidateOptionalEditorText(string value, int maximumLength)
    {
        ArgumentNullException.ThrowIfNull(value);
        if (value.Length > maximumLength ||
            !string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
            !IsSafeText(value, allowEmpty: true))
        {
            throw new ArgumentException("A safe canonical editor value is required.", nameof(value));
        }

        return value;
    }

    private static string ValidateOptionalStockQuery(string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        var normalized = value.Trim();
        if (value.Length > LegacyStockSearchService.MaximumQueryLength ||
            normalized.Length > LegacyStockSearchService.MaximumQueryLength ||
            !IsSafeText(value, allowEmpty: true) ||
            !IsSafeText(normalized, allowEmpty: true))
        {
            throw new ArgumentException(
                "A safe operator-catalog stock query is required.",
                nameof(value));
        }

        return normalized;
    }

    private static string ValidateExistingThemeEditorName(
        string value,
        ThemeSelectionIdentity expected)
    {
        if (!string.Equals(value, expected.ThemeTitle, StringComparison.Ordinal))
        {
            return ValidateRequiredText(value, 128);
        }

        ArgumentNullException.ThrowIfNull(value);
        if (value.Length is < 1 or > 128 || string.IsNullOrWhiteSpace(value) ||
            !IsSafeText(value, allowEmpty: false))
        {
            throw new ArgumentException("A safe stored theme name is required.", nameof(value));
        }

        return value;
    }

    private static bool IsSafeText(string value, bool allowEmpty)
    {
        if (!allowEmpty && value.Length == 0)
        {
            return false;
        }

        foreach (var character in value)
        {
            var category = CharUnicodeInfo.GetUnicodeCategory(character);
            if (char.IsControl(character) || char.IsSurrogate(character) ||
                character == '\uFFFD' || category is UnicodeCategory.Format or
                    UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator)
            {
                return false;
            }
        }

        return true;
    }

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

    private void ClearStatus()
    {
        _statusMessage = string.Empty;
        _statusKind = LegacyOperatorCatalogStatusKind.Neutral;
    }

    private void Touch() => _revision = checked(_revision + 1);

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

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

    private sealed record CatalogResultState(
        string ResultId,
        string CodeLabel,
        string DisplayName,
        string Scope,
        ThemeSelectionIdentity? ThemeIdentity,
        ExpertSelectionIdentity? ExpertIdentity)
    {
        internal static CatalogResultState ForTheme(
            string resultId,
            ThemeSelectionIdentity identity,
            DataSourceKind source) =>
            new(
                resultId,
                identity.ThemeCode,
                identity.ThemeTitle,
                identity.Market == ThemeMarket.Krx ? "KRX / Oracle" : "NXT / MariaDB",
                identity,
                null);

        internal static CatalogResultState ForExpert(
            string resultId,
            ExpertSelectionIdentity identity,
            DataSourceKind source) =>
            new(
                resultId,
                identity.ExpertCode,
                identity.ExpertName,
                source == DataSourceKind.Oracle ? "Oracle" : "MariaDB",
                null,
                identity);
    }

    private sealed record DraftRowState(
        string RowId,
        string Code,
        string Name,
        string MarketLabel,
        decimal? BuyAmount,
        bool RequiresCurrentIdentityValidation)
    {
        internal static DraftRowState ForTheme(
            string rowId,
            string code,
            string name,
            bool requiresCurrentIdentityValidation) =>
            new(
                rowId,
                code,
                name,
                ThemeMarketLabel(code),
                null,
                requiresCurrentIdentityValidation);

        internal static DraftRowState ForExpert(
            string rowId,
            string code,
            string name,
            decimal? buyAmount) =>
            new(rowId, code, name, "종목", buyAmount, false);

        private static string ThemeMarketLabel(string code) => code.Length > 0
            ? code[0] switch
            {
                'P' => "KOSPI",
                'D' => "KOSDAQ",
                'X' => "NXT KOSPI",
                'T' => "NXT KOSDAQ",
                _ => "종목"
            }
            : "종목";
    }

    private sealed record StockResultState(string ResultId, StockSearchItem Item);

    private sealed class ThemeNameDuplicateException : Exception
    {
    }
}
