using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication;

/// <summary>
/// C# authority for the original UC3 comparison workflow. Search results and fixed targets
/// are selected by opaque ids, saved bytes are parsed by the strict Core CP949 parser, and
/// an action id is the only caller-supplied value used to materialize a playlist draft.
/// </summary>
public sealed class LegacyComparisonWorkflowController
{
    private readonly ILegacyComparisonDataService _dataService;
    private readonly ILegacyComparisonPairStore _pairStore;
    private readonly LegacyComparisonUiLayout _uiLayout;
    private readonly ILegacyComparisonPairImportService? _pairImporter;
    private List<SearchResultState> _domesticResults = [];
    private List<SearchResultState> _worldResults = [];
    private List<SavedPairState> _savedPairs = [];
    private LegacyComparisonTarget? _firstTarget;
    private LegacyComparisonTarget? _secondTarget;
    private string? _selectedDomesticResultId;
    private string? _selectedWorldResultId;
    private string? _selectedFixedTargetId;
    private string? _selectedPairId;
    private string _domesticQuery = string.Empty;
    private string _worldQuery = string.Empty;
    private string _domesticError = string.Empty;
    private string _worldError = string.Empty;
    private string _statusMessage = string.Empty;
    private LegacyComparisonSearchStatus _domesticStatus = LegacyComparisonSearchStatus.Idle;
    private LegacyComparisonSearchStatus _worldStatus = LegacyComparisonSearchStatus.Idle;
    private bool _domesticIsTruncated;
    private bool _worldIsTruncated;
    private int _domesticIsolatedInvalidRowCount;
    private int _worldIsolatedInvalidRowCount;
    private int _domesticDeferredUnsafeRowCount;
    private int _worldDeferredUnsafeRowCount;
    private LegacyOperatorStatusKind _statusKind = LegacyOperatorStatusKind.Neutral;
    private LegacyComparisonImportReceipt? _lastImport;
    private long _revision;
    private long _pairSequence;
    private long _draftSequence;
    private int _domesticSearchVersion;
    private int _worldSearchVersion;
    private int _activationGeneration;
    private bool _isBusy;

    public LegacyComparisonWorkflowController(
        ILegacyComparisonDataService dataService,
        ILegacyComparisonPairStore pairStore,
        LegacyComparisonUiLayout? uiLayout = null,
        ILegacyComparisonPairImportService? pairImporter = null)
    {
        _dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
        _pairStore = pairStore ?? throw new ArgumentNullException(nameof(pairStore));
        _uiLayout = uiLayout ?? LegacyComparisonUiLayout.Default;
        _pairImporter = pairImporter;
    }

    public LegacyComparisonWorkflowSnapshot Current => CreateSnapshot();

    public bool CanImportLegacyPairs => _pairImporter is not null;

    /// <summary>
    /// Recreates UC3's per-control state before its saved CP949 pairs are loaded for
    /// a new tab activation. Search and pair-load generations invalidate completions
    /// that belong to the discarded control instance.
    /// </summary>
    public LegacyComparisonWorkflowSnapshot ResetForTabActivation()
    {
        Interlocked.Increment(ref _activationGeneration);
        Interlocked.Increment(ref _domesticSearchVersion);
        Interlocked.Increment(ref _worldSearchVersion);
        _domesticResults = [];
        _worldResults = [];
        _savedPairs = [];
        _firstTarget = null;
        _secondTarget = null;
        _selectedDomesticResultId = null;
        _selectedWorldResultId = null;
        _selectedFixedTargetId = null;
        _selectedPairId = null;
        _domesticQuery = string.Empty;
        _worldQuery = string.Empty;
        _domesticError = string.Empty;
        _worldError = string.Empty;
        _statusMessage = string.Empty;
        _domesticStatus = LegacyComparisonSearchStatus.Idle;
        _worldStatus = LegacyComparisonSearchStatus.Idle;
        _domesticIsTruncated = false;
        _worldIsTruncated = false;
        _domesticIsolatedInvalidRowCount = 0;
        _worldIsolatedInvalidRowCount = 0;
        _domesticDeferredUnsafeRowCount = 0;
        _worldDeferredUnsafeRowCount = 0;
        _statusKind = LegacyOperatorStatusKind.Neutral;
        _lastImport = null;
        _isBusy = false;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyComparisonWorkflowSnapshot> SearchDomesticAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        var normalizedQuery = NormalizeSearchQuery(query);
        var version = Interlocked.Increment(ref _domesticSearchVersion);
        _domesticQuery = normalizedQuery;
        _domesticStatus = LegacyComparisonSearchStatus.Loading;
        _domesticError = string.Empty;
        _domesticResults = [];
        _domesticIsTruncated = false;
        _domesticIsolatedInvalidRowCount = 0;
        _domesticDeferredUnsafeRowCount = 0;
        _selectedDomesticResultId = null;
        Touch();

        try
        {
            var search = await SearchDomesticWithDiagnosticsAsync(
                normalizedQuery,
                cancellationToken).ConfigureAwait(false);
            if (version != Volatile.Read(ref _domesticSearchVersion))
            {
                return CreateSnapshot();
            }

            ValidateDiagnostics(
                search,
                LegacyComparisonWorkflowCatalog.DomesticMaximumResults);
            _domesticResults = ValidateSearchResults(
                search.Items,
                LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
                domestic: true,
                version);
            _domesticIsTruncated = search.IsTruncated;
            _domesticIsolatedInvalidRowCount = search.IsolatedInvalidRowCount;
            _domesticDeferredUnsafeRowCount = search.DeferredUnsafeRowCount;
            _domesticStatus = LegacyComparisonSearchStatus.Ready;
            _domesticError = string.Empty;
            Touch();
            return CreateSnapshot();
        }
        catch (OperationCanceledException)
        {
            if (version == Volatile.Read(ref _domesticSearchVersion))
            {
                _domesticStatus = LegacyComparisonSearchStatus.Idle;
                _domesticError = string.Empty;
                Touch();
            }

            throw;
        }
        catch
        {
            if (version == Volatile.Read(ref _domesticSearchVersion))
            {
                _domesticResults = [];
                _domesticIsTruncated = false;
                _domesticIsolatedInvalidRowCount = 0;
                _domesticDeferredUnsafeRowCount = 0;
                _domesticStatus = LegacyComparisonSearchStatus.Error;
                _domesticError = "국내·NXT 종목 검색 결과를 안전하게 확인하지 못했습니다.";
                Touch();
            }

            throw;
        }
    }

    public async Task<LegacyComparisonWorkflowSnapshot> SearchWorldAsync(
        string query,
        CancellationToken cancellationToken = default)
    {
        // UC3's blank world query loaded the complete US/TW universe, so blank remains valid.
        var normalizedQuery = NormalizeSearchQuery(query);
        var version = Interlocked.Increment(ref _worldSearchVersion);
        _worldQuery = normalizedQuery;
        _worldStatus = LegacyComparisonSearchStatus.Loading;
        _worldError = string.Empty;
        _worldResults = [];
        _worldIsTruncated = false;
        _worldIsolatedInvalidRowCount = 0;
        _worldDeferredUnsafeRowCount = 0;
        _selectedWorldResultId = null;
        Touch();

        try
        {
            var search = await SearchWorldWithDiagnosticsAsync(
                normalizedQuery,
                cancellationToken).ConfigureAwait(false);
            if (version != Volatile.Read(ref _worldSearchVersion))
            {
                return CreateSnapshot();
            }

            ValidateDiagnostics(
                search,
                LegacyComparisonWorkflowCatalog.WorldMaximumResults);
            _worldResults = ValidateSearchResults(
                search.Items,
                LegacyComparisonWorkflowCatalog.WorldMaximumResults,
                domestic: false,
                version);
            _worldIsTruncated = search.IsTruncated;
            _worldIsolatedInvalidRowCount = search.IsolatedInvalidRowCount;
            _worldDeferredUnsafeRowCount = search.DeferredUnsafeRowCount;
            _worldStatus = LegacyComparisonSearchStatus.Ready;
            _worldError = string.Empty;
            Touch();
            return CreateSnapshot();
        }
        catch (OperationCanceledException)
        {
            if (version == Volatile.Read(ref _worldSearchVersion))
            {
                _worldStatus = LegacyComparisonSearchStatus.Idle;
                _worldError = string.Empty;
                Touch();
            }

            throw;
        }
        catch
        {
            if (version == Volatile.Read(ref _worldSearchVersion))
            {
                _worldResults = [];
                _worldIsTruncated = false;
                _worldIsolatedInvalidRowCount = 0;
                _worldDeferredUnsafeRowCount = 0;
                _worldStatus = LegacyComparisonSearchStatus.Error;
                _worldError = "미국·대만 종목 검색 결과를 안전하게 확인하지 못했습니다.";
                Touch();
            }

            throw;
        }
    }

    public LegacyComparisonWorkflowSnapshot SelectDomesticResult(string selectionId) =>
        SelectSearchResult(_domesticResults, selectionId, domestic: true);

    public LegacyComparisonWorkflowSnapshot SelectWorldResult(string selectionId) =>
        SelectSearchResult(_worldResults, selectionId, domestic: false);

    public LegacyComparisonWorkflowSnapshot SelectFixedTarget(string targetId)
    {
        ArgumentNullException.ThrowIfNull(targetId);
        if (LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId) is not null)
        {
            _selectedFixedTargetId = targetId;
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyComparisonWorkflowSnapshot ChooseDomesticResult(string selectionId) =>
        ChooseSearchResult(_domesticResults, selectionId, domestic: true);

    public LegacyComparisonWorkflowSnapshot ChooseWorldResult(string selectionId) =>
        ChooseSearchResult(_worldResults, selectionId, domestic: false);

    public LegacyComparisonWorkflowSnapshot ChooseFixedTarget(string targetId)
    {
        ArgumentNullException.ThrowIfNull(targetId);
        var target = LegacyComparisonWorkflowCatalog.GetFixedTarget(targetId);
        if (target is not null)
        {
            _selectedFixedTargetId = targetId;
            RotateDraft(target);
            Touch();
        }

        return CreateSnapshot();
    }

    public LegacyComparisonWorkflowSnapshot SwapDraftTargets()
    {
        (_firstTarget, _secondTarget) = (_secondTarget, _firstTarget);
        Touch();
        return CreateSnapshot();
    }

    public LegacyComparisonWorkflowSnapshot ClearDraftTargets()
    {
        _firstTarget = null;
        _secondTarget = null;
        Touch();
        return CreateSnapshot();
    }

    public async Task<LegacyComparisonWorkflowSnapshot> LoadSavedPairsAsync(
        CancellationToken cancellationToken = default)
    {
        var activationGeneration = Volatile.Read(ref _activationGeneration);
        BeginBusy();
        try
        {
            var source = await _pairStore.ReadCp949Async(cancellationToken).ConfigureAwait(false);
            var candidate = new List<SavedPairState>();
            if (source is { Length: > 0 })
            {
                var bytes = source.ToArray();
                var rows = LegacyComparisonFileParser.Parse(bytes);
                foreach (var row in rows)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    var firstReference = new LegacyComparisonStoredTargetReference(
                        row.First.Name,
                        row.First.Market);
                    var secondReference = new LegacyComparisonStoredTargetReference(
                        row.Second.Name,
                        row.Second.Market);
                    var first = await ResolveStoredTargetAsync(
                        firstReference,
                        cancellationToken).ConfigureAwait(false);
                    var second = await ResolveStoredTargetAsync(
                        secondReference,
                        cancellationToken).ConfigureAwait(false);
                    candidate.Add(new SavedPairState(
                        $"comparison-pair-{row.RowNumber:D8}",
                        first,
                        second));
                }
            }

            if (activationGeneration != Volatile.Read(ref _activationGeneration))
            {
                return CreateSnapshot();
            }

            _savedPairs = candidate;
            _pairSequence = Math.Max(_pairSequence, candidate.Count);
            _selectedPairId = candidate.FirstOrDefault()?.RowId;
            _statusMessage = candidate.Count == 0
                ? "저장된 종목 비교쌍이 없습니다."
                : $"저장된 종목 비교쌍 {candidate.Count}개를 불러왔습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch
        {
            if (activationGeneration != Volatile.Read(ref _activationGeneration))
            {
                return CreateSnapshot();
            }

            _statusMessage = "CP949 종목비교.dat를 안전하게 불러오지 못했습니다.";
            _statusKind = LegacyOperatorStatusKind.Error;
            throw;
        }
        finally
        {
            if (activationGeneration == Volatile.Read(ref _activationGeneration))
            {
                EndBusy();
            }
        }

        return CreateSnapshot();
    }

    /// <summary>
    /// Resolves every row from the original application's fixed, read-only file before
    /// crossing the local write boundary. Existing operator rows keep their exact order;
    /// only ordered pairs that have not already been saved are appended.
    /// </summary>
    public async Task<LegacyComparisonWorkflowSnapshot> ImportLegacyPairsAsync(
        CancellationToken cancellationToken = default)
    {
        if (_pairImporter is null)
        {
            throw new InvalidOperationException(
                "원본 비교쌍 가져오기가 현재 데이터베이스 구성에 연결되지 않았습니다.");
        }

        BeginBusy();
        try
        {
            var resolved = await _pairImporter.ImportAsync(cancellationToken)
                .ConfigureAwait(false);
            var importedPairs = ValidateImportedPairs(resolved);

            var signatures = new HashSet<string>(
                _savedPairs.Select(static pair =>
                    LegacyComparisonWorkflowCatalog.PairSignature(pair.First, pair.Second)),
                StringComparer.Ordinal);
            var candidate = new List<SavedPairState>(_savedPairs);
            var nextSequence = _pairSequence;
            foreach (var pair in importedPairs)
            {
                cancellationToken.ThrowIfCancellationRequested();
                var signature = LegacyComparisonWorkflowCatalog.PairSignature(
                    pair.First,
                    pair.Second);
                if (!signatures.Add(signature))
                {
                    continue;
                }

                candidate.Add(new SavedPairState(
                    $"comparison-pair-{++nextSequence:D8}",
                    pair.First,
                    pair.Second));
            }

            if (candidate.Count > LegacyComparisonFileParser.MaximumRows)
            {
                throw new InvalidOperationException(
                    "원본 비교쌍을 병합하면 저장 가능한 최대 행 수를 초과합니다.");
            }

            var addedCount = candidate.Count - _savedPairs.Count;
            if (addedCount > 0)
            {
                await WritePairsAsync(candidate, cancellationToken).ConfigureAwait(false);
                _savedPairs = candidate;
                _pairSequence = nextSequence;
                _selectedPairId ??= candidate.FirstOrDefault()?.RowId;
            }

            _lastImport = new LegacyComparisonImportReceipt(
                resolved.SourceSha256,
                resolved.SourceRowCount,
                addedCount,
                resolved.SourceRowCount - addedCount,
                candidate.Count,
                resolved.RetrievedAt);
            _statusMessage = addedCount == 0
                ? $"원본 비교쌍 {resolved.SourceRowCount}개가 모두 이미 저장되어 있습니다."
                : $"원본 비교쌍 {resolved.SourceRowCount}개를 확인해 새 비교쌍 {addedCount}개를 추가했습니다.";
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch
        {
            _statusMessage =
                "원본 비교쌍을 모두 확인하거나 로컬 파일에 원자적으로 병합하지 못해 변경하지 않았습니다.";
            _statusKind = LegacyOperatorStatusKind.Error;
            throw;
        }
        finally
        {
            EndBusy();
        }

        return CreateSnapshot();
    }

    public async Task<LegacyComparisonWorkflowSnapshot> AddDraftPairAsync(
        CancellationToken cancellationToken = default)
    {
        if (_firstTarget is null || _secondTarget is null)
        {
            throw new InvalidOperationException("Select two comparison targets first.");
        }

        if (!_firstTarget.CanSelect || !_secondTarget.CanSelect)
        {
            throw new InvalidOperationException(
                "A deferred comparison row cannot be saved as a comparison pair.");
        }

        if (_savedPairs.Count >= LegacyComparisonFileParser.MaximumRows)
        {
            throw new InvalidOperationException("The saved comparison pair limit was reached.");
        }

        var rowId = $"comparison-pair-{++_pairSequence:D8}";
        var candidate = new List<SavedPairState>(_savedPairs)
        {
            new(rowId, _firstTarget, _secondTarget)
        };
        return await CommitPairsAsync(
            candidate,
            rowId,
            "종목 비교쌍을 저장했습니다.",
            cancellationToken).ConfigureAwait(false);
    }

    public LegacyComparisonWorkflowSnapshot SelectSavedPair(string rowId)
    {
        ArgumentNullException.ThrowIfNull(rowId);
        if (_savedPairs.Any(pair => string.Equals(pair.RowId, rowId, StringComparison.Ordinal)))
        {
            _selectedPairId = rowId;
            Touch();
        }

        return CreateSnapshot();
    }

    public async Task<LegacyComparisonWorkflowSnapshot> MoveSelectedPairAsync(
        LegacyComparisonPairMoveDirection direction,
        CancellationToken cancellationToken = default)
    {
        if (!Enum.IsDefined(direction))
        {
            throw new ArgumentOutOfRangeException(nameof(direction));
        }

        var index = SelectedPairIndex();
        var destination = index + (direction == LegacyComparisonPairMoveDirection.Up ? -1 : 1);
        if (index < 0 || destination < 0 || destination >= _savedPairs.Count)
        {
            return CreateSnapshot();
        }

        var candidate = new List<SavedPairState>(_savedPairs);
        (candidate[index], candidate[destination]) = (candidate[destination], candidate[index]);
        return await CommitPairsAsync(
            candidate,
            _selectedPairId,
            "종목 비교쌍 순서를 변경했습니다.",
            cancellationToken).ConfigureAwait(false);
    }

    public async Task<LegacyComparisonWorkflowSnapshot> DeleteSelectedPairAsync(
        CancellationToken cancellationToken = default)
    {
        var index = SelectedPairIndex();
        if (index < 0)
        {
            return CreateSnapshot();
        }

        var candidate = new List<SavedPairState>(_savedPairs);
        candidate.RemoveAt(index);
        var nextSelection = candidate.Count == 0
            ? null
            : candidate[Math.Min(index, candidate.Count - 1)].RowId;
        return await CommitPairsAsync(
            candidate,
            nextSelection,
            "선택한 종목 비교쌍을 삭제했습니다.",
            cancellationToken).ConfigureAwait(false);
    }

    public Task<LegacyComparisonWorkflowSnapshot> DeleteAllPairsAsync(
        CancellationToken cancellationToken = default) =>
        _savedPairs.Count == 0
            ? Task.FromResult(CreateSnapshot())
            : CommitPairsAsync(
                [],
                null,
                "종목 비교쌍을 모두 삭제했습니다.",
                cancellationToken);

    public LegacyGenericPlaylistDraft MaterializeSelectedAction(string actionId)
    {
        ArgumentNullException.ThrowIfNull(actionId);
        if (_isBusy)
        {
            throw new InvalidOperationException("The comparison workflow is busy.");
        }

        var index = SelectedPairIndex();
        if (index < 0)
        {
            throw new InvalidOperationException("Select a saved comparison pair first.");
        }

        var pair = _savedPairs[index];
        var draftId = $"comparison-draft-{++_draftSequence:D8}";
        return LegacyComparisonWorkflowCatalog.Materialize(
            actionId,
            pair.First,
            pair.Second,
            draftId);
    }

    private LegacyComparisonWorkflowSnapshot ChooseSearchResult(
        IReadOnlyList<SearchResultState> results,
        string selectionId,
        bool domestic)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        var result = results.FirstOrDefault(item =>
            string.Equals(item.SelectionId, selectionId, StringComparison.Ordinal));
        if (result is not null)
        {
            if (!result.Target.CanSelect)
            {
                _statusMessage = result.Target.UnavailableReason ??
                    "이 비교 대상은 선택할 수 없습니다.";
                _statusKind = LegacyOperatorStatusKind.Warning;
                Touch();
                return CreateSnapshot();
            }

            if (domestic)
            {
                _selectedDomesticResultId = selectionId;
            }
            else
            {
                _selectedWorldResultId = selectionId;
            }

            RotateDraft(result.Target);
            Touch();
        }

        return CreateSnapshot();
    }

    private LegacyComparisonWorkflowSnapshot SelectSearchResult(
        IReadOnlyList<SearchResultState> results,
        string selectionId,
        bool domestic)
    {
        ArgumentNullException.ThrowIfNull(selectionId);
        var result = results.FirstOrDefault(item => string.Equals(
            item.SelectionId,
            selectionId,
            StringComparison.Ordinal));
        if (result is not null)
        {
            if (!result.Target.CanSelect)
            {
                _statusMessage = result.Target.UnavailableReason ??
                    "이 비교 대상은 선택할 수 없습니다.";
                _statusKind = LegacyOperatorStatusKind.Warning;
                Touch();
                return CreateSnapshot();
            }

            if (domestic)
            {
                _selectedDomesticResultId = selectionId;
            }
            else
            {
                _selectedWorldResultId = selectionId;
            }

            Touch();
        }

        return CreateSnapshot();
    }

    private void RotateDraft(LegacyComparisonTarget selected)
    {
        _secondTarget = _firstTarget;
        _firstTarget = selected;
    }

    private async Task<LegacyComparisonTarget> ResolveStoredTargetAsync(
        LegacyComparisonStoredTargetReference stored,
        CancellationToken cancellationToken)
    {
        var fixedTarget = LegacyComparisonWorkflowCatalog.TryResolveFixedStoredTarget(stored);
        var resolved = fixedTarget ?? await _dataService.ResolveStoredTargetAsync(
            stored,
            cancellationToken).ConfigureAwait(false);
        if (resolved is null)
        {
            throw new InvalidOperationException("A stored comparison target no longer resolves.");
        }

        resolved = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(resolved);
        if (!resolved.CanSelect)
        {
            throw new InvalidOperationException(
                "A stored comparison target resolves only to a deferred unsafe row.");
        }
        if (!LegacyComparisonWorkflowCatalog.MatchesStoredReference(resolved, stored))
        {
            throw new InvalidOperationException("A stored comparison target resolved to a different identity.");
        }

        return resolved;
    }

    private async Task<LegacyComparisonWorkflowSnapshot> CommitPairsAsync(
        IReadOnlyList<SavedPairState> candidate,
        string? nextSelection,
        string successMessage,
        CancellationToken cancellationToken)
    {
        BeginBusy();
        try
        {
            await WritePairsAsync(candidate, cancellationToken).ConfigureAwait(false);

            _savedPairs = candidate.ToList();
            _selectedPairId = nextSelection;
            _statusMessage = successMessage;
            _statusKind = LegacyOperatorStatusKind.Information;
        }
        catch
        {
            _statusMessage = "종목 비교쌍 파일 쓰기를 완료하지 못해 변경을 적용하지 않았습니다.";
            _statusKind = LegacyOperatorStatusKind.Error;
            throw;
        }
        finally
        {
            EndBusy();
        }

        return CreateSnapshot();
    }

    private async Task WritePairsAsync(
        IReadOnlyList<SavedPairState> candidate,
        CancellationToken cancellationToken)
    {
        var writeRows = candidate.Select((pair, index) =>
        {
            var first = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.First);
            var second = LegacyComparisonWorkflowCatalog.ToStoredReference(pair.Second);
            return new LegacyComparisonPairWriteRow(
                index + 1,
                first.Name,
                first.Market,
                second.Name,
                second.Market);
        }).ToArray();
        await _pairStore.WriteCp949Async(
            Array.AsReadOnly(writeRows),
            cancellationToken).ConfigureAwait(false);
    }

    private static IReadOnlyList<(LegacyComparisonTarget First, LegacyComparisonTarget Second)>
        ValidateImportedPairs(LegacyComparisonImportResult? result)
    {
        if (result is null || result.SourceRowCount is <= 0 or > LegacyComparisonFileParser.MaximumRows ||
            result.Pairs is null || result.Pairs.Count != result.SourceRowCount ||
            string.IsNullOrEmpty(result.SourceSha256) || result.SourceSha256.Length != 64 ||
            result.SourceSha256.Any(static value =>
                value is not (>= '0' and <= '9') and not (>= 'A' and <= 'F')))
        {
            throw new InvalidOperationException("The resolved comparison import is invalid.");
        }

        var pairs = new List<(LegacyComparisonTarget First, LegacyComparisonTarget Second)>(
            result.Pairs.Count);
        for (var index = 0; index < result.Pairs.Count; index++)
        {
            var pair = result.Pairs[index] ??
                throw new InvalidOperationException("The resolved comparison import is invalid.");
            if (pair.RowNumber != index + 1)
            {
                throw new InvalidOperationException("The resolved comparison import is invalid.");
            }

            pairs.Add((
                ToCanonicalImportedTarget(pair.First),
                ToCanonicalImportedTarget(pair.Second)));
        }

        return pairs.AsReadOnly();
    }

    private static LegacyComparisonTarget ToCanonicalImportedTarget(
        LegacyComparisonImportTarget? target)
    {
        if (target is null)
        {
            throw new InvalidOperationException("The resolved comparison target is invalid.");
        }

        LegacyComparisonTarget canonical = target.Kind switch
        {
            LegacyComparisonImportTargetKind.MarketTarget
                when target.MarketTarget is not null &&
                     target.Market is null && target.StockName is null &&
                     target.StockCode is null && target.InputName is null &&
                     target.Symbol is null && target.Nation is null =>
                LegacyComparisonWorkflowCatalog.FixedTargets.SingleOrDefault(item =>
                    string.Equals(
                        item.MarketTarget,
                        target.MarketTarget,
                        StringComparison.Ordinal)) ??
                throw new InvalidOperationException(
                    "The resolved comparison market target is unknown."),
            LegacyComparisonImportTargetKind.KrxStock
                when target.MarketTarget is null && target.Market is not null &&
                     target.StockName is not null && target.StockCode is not null &&
                     target.InputName is null && target.Symbol is null &&
                     target.Nation is null =>
                LegacyComparisonTarget.CreateKrxStock(
                    ParseImportedDomesticMarket(target.Market),
                    target.StockName,
                    target.StockCode),
            LegacyComparisonImportTargetKind.NxtStock
                when target.MarketTarget is null && target.Market is not null &&
                     target.StockName is not null && target.StockCode is not null &&
                     target.InputName is null && target.Symbol is null &&
                     target.Nation is null =>
                LegacyComparisonTarget.CreateNxtStock(
                    ParseImportedDomesticMarket(target.Market),
                    target.StockName,
                    target.StockCode),
            LegacyComparisonImportTargetKind.WorldStock
                when target.MarketTarget is null && target.Market is null &&
                     target.StockName is null && target.StockCode is null &&
                     target.InputName is not null && target.Symbol is not null &&
                     target.Nation is not null =>
                LegacyComparisonTarget.CreateWorldStock(
                    target.InputName,
                    target.Symbol,
                    target.Nation),
            _ => throw new InvalidOperationException(
                "The resolved comparison target shape is invalid.")
        };

        canonical = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(canonical);
        if (!canonical.CanSelect)
        {
            throw new InvalidOperationException(
                "The resolved comparison target cannot be stored safely.");
        }

        return canonical;
    }

    private static LegacyComparisonDomesticMarket ParseImportedDomesticMarket(string market) =>
        market switch
        {
            "kospi" => LegacyComparisonDomesticMarket.Kospi,
            "kosdaq" => LegacyComparisonDomesticMarket.Kosdaq,
            _ => throw new InvalidOperationException(
                "The resolved comparison domestic market is invalid.")
        };

    private static List<SearchResultState> ValidateSearchResults(
        IReadOnlyList<LegacyComparisonTarget>? results,
        int maximumResults,
        bool domestic,
        int version)
    {
        if (results is null || results.Count > maximumResults)
        {
            throw new InvalidOperationException("The comparison search result count is invalid.");
        }

        var validated = new List<SearchResultState>(results.Count);
        var identities = new HashSet<LegacyComparisonTarget>();
        for (var index = 0; index < results.Count; index++)
        {
            var target = LegacyComparisonWorkflowCatalog.RequireCanonicalTarget(results[index]);
            var allowedKind = domestic
                ? target.Kind is LegacyComparisonTargetKind.KrxStock or LegacyComparisonTargetKind.NxtStock
                : target.Kind == LegacyComparisonTargetKind.WorldStock;
            if (!allowedKind || !identities.Add(target))
            {
                throw new InvalidOperationException("The comparison search result identity is invalid.");
            }

            validated.Add(new SearchResultState(
                $"comparison-{(domestic ? "domestic" : "world")}-{version:D4}-{index + 1:D4}",
                target));
        }

        return validated;
    }

    private async Task<LegacyComparisonSearchData> SearchDomesticWithDiagnosticsAsync(
        string query,
        CancellationToken cancellationToken)
    {
        if (_dataService is ILegacyComparisonDataDiagnosticsService diagnostics)
        {
            return await diagnostics.SearchDomesticWithDiagnosticsAsync(
                query,
                LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
                cancellationToken).ConfigureAwait(false);
        }

        var items = await _dataService.SearchDomesticAsync(
            query,
            LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
            cancellationToken).ConfigureAwait(false);
        return new LegacyComparisonSearchData(items, false, 0);
    }

    private async Task<LegacyComparisonSearchData> SearchWorldWithDiagnosticsAsync(
        string query,
        CancellationToken cancellationToken)
    {
        if (_dataService is ILegacyComparisonDataDiagnosticsService diagnostics)
        {
            return await diagnostics.SearchWorldWithDiagnosticsAsync(
                query,
                LegacyComparisonWorkflowCatalog.WorldMaximumResults,
                cancellationToken).ConfigureAwait(false);
        }

        var items = await _dataService.SearchWorldAsync(
            query,
            LegacyComparisonWorkflowCatalog.WorldMaximumResults,
            cancellationToken).ConfigureAwait(false);
        return new LegacyComparisonSearchData(items, false, 0);
    }

    private static void ValidateDiagnostics(
        LegacyComparisonSearchData? search,
        int maximumResults)
    {
        if (search is null || search.Items is null ||
            search.Items.Count > maximumResults ||
            search.IsolatedInvalidRowCount is < 0 ||
            search.IsolatedInvalidRowCount > maximumResults ||
            search.DeferredUnsafeRowCount is < 0 ||
            search.DeferredUnsafeRowCount > search.Items.Count ||
            search.DeferredUnsafeRowCount != search.Items.Count(item => !item.CanSelect) ||
            search.IsolatedInvalidRowCount > 0 && !search.IsTruncated)
        {
            throw new InvalidOperationException(
                "The comparison search diagnostics are invalid.");
        }
    }

    private LegacyComparisonWorkflowSnapshot CreateSnapshot()
    {
        var selectedPair = SelectedPairIndex() is var selectedIndex && selectedIndex >= 0
            ? _savedPairs[selectedIndex]
            : null;
        var domesticResults = _domesticResults
            .Select(result => LegacyComparisonWorkflowCatalog.ToView(
                result.Target,
                result.SelectionId))
            .ToArray();
        var worldResults = _worldResults
            .Select(result => LegacyComparisonWorkflowCatalog.ToView(
                result.Target,
                result.SelectionId))
            .ToArray();
        var fixedTargets = _uiLayout.FixedTargets
            .Select(target => LegacyComparisonWorkflowCatalog.ToView(target, target.TargetId))
            .ToArray();
        var savedPairs = _savedPairs.Select((pair, index) =>
            new LegacyComparisonSavedPairView(
                pair.RowId,
                index + 1,
                $"{pair.First.DisplayName} ↔ {pair.Second.DisplayName}",
                LegacyComparisonWorkflowCatalog.ToView(pair.First, $"{pair.RowId}-first"),
                LegacyComparisonWorkflowCatalog.ToView(pair.Second, $"{pair.RowId}-second"),
                string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal)))
            .ToArray();
        return new LegacyComparisonWorkflowSnapshot(
            _revision,
            _isBusy,
            new LegacyComparisonSearchSnapshot(
                _domesticQuery,
                _domesticStatus,
                Array.AsReadOnly(domesticResults),
                _selectedDomesticResultId,
                domesticResults.Length,
                _domesticError)
            {
                IsTruncated = _domesticIsTruncated,
                IsolatedInvalidRowCount = _domesticIsolatedInvalidRowCount,
                DeferredUnsafeRowCount = _domesticDeferredUnsafeRowCount
            },
            new LegacyComparisonSearchSnapshot(
                _worldQuery,
                _worldStatus,
                Array.AsReadOnly(worldResults),
                _selectedWorldResultId,
                worldResults.Length,
                _worldError)
            {
                IsTruncated = _worldIsTruncated,
                IsolatedInvalidRowCount = _worldIsolatedInvalidRowCount,
                DeferredUnsafeRowCount = _worldDeferredUnsafeRowCount
            },
            Array.AsReadOnly(fixedTargets),
            _selectedFixedTargetId,
            _firstTarget is null
                ? null
                : LegacyComparisonWorkflowCatalog.ToView(_firstTarget, "comparison-draft-first"),
            _secondTarget is null
                ? null
                : LegacyComparisonWorkflowCatalog.ToView(_secondTarget, "comparison-draft-second"),
            Array.AsReadOnly(savedPairs),
            _selectedPairId,
            _uiLayout.CreateActionViews(
                selectedPair?.First,
                selectedPair?.Second),
            _statusMessage,
            _statusKind)
        {
            CanImportLegacyPairs = CanImportLegacyPairs,
            LastImport = _lastImport
        };
    }

    private int SelectedPairIndex() => _selectedPairId is null
        ? -1
        : _savedPairs.FindIndex(pair =>
            string.Equals(pair.RowId, _selectedPairId, StringComparison.Ordinal));

    private static string NormalizeSearchQuery(string query)
    {
        ArgumentNullException.ThrowIfNull(query);
        var normalized = query.Trim();
        if (normalized.Length > 64 ||
            normalized.Any(static character => char.IsControl(character) || char.IsSurrogate(character)))
        {
            throw new ArgumentException("A comparison query must be at most 64 safe characters.", nameof(query));
        }

        return normalized;
    }

    private void BeginBusy()
    {
        if (_isBusy)
        {
            throw new InvalidOperationException("The comparison workflow is already busy.");
        }

        _isBusy = true;
        Touch();
    }

    private void EndBusy()
    {
        _isBusy = false;
        Touch();
    }

    private void Touch() => _revision++;

    private sealed record SearchResultState(
        string SelectionId,
        LegacyComparisonTarget Target);

    private sealed record SavedPairState(
        string RowId,
        LegacyComparisonTarget First,
        LegacyComparisonTarget Second);
}
