// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "indexedstrategy.h"

#include <QDir>
#include <QThread>
#include <QElapsedTimer>

#include <lucene++/LuceneHeaders.h>
#include <lucene++/BooleanQuery.h>
#include <lucene++/QueryWrapperFilter.h>
#include <lucene++/WildcardQuery.h>
#include <lucene++/MapFieldSelector.h>

#include <dfm-search/field_names.h>
#include <dfm-search/timerangefilter.h>
#include <dfm-search/ocrtextsearchapi.h>

#include "utils/cancellablecollector.h"
#include "utils/contenthighlighter.h"
#include "utils/lucenequeryutils.h"
#include "utils/lucene_cancellation_compat.h"
#include "utils/timerangeutils.h"

using namespace Lucene;

DFM_SEARCH_BEGIN_NS

OcrTextIndexedStrategy::OcrTextIndexedStrategy(const SearchOptions &options, QObject *parent)
    : OcrTextBaseStrategy(options, parent)
{
    initializeIndexing();
}

OcrTextIndexedStrategy::~OcrTextIndexedStrategy() = default;

void OcrTextIndexedStrategy::initializeIndexing()
{
    // Get OCR text index directory
    m_indexDir = Global::ocrTextIndexDirectory();

    // Check if index directory exists
    if (!QDir(m_indexDir).exists()) {
        qWarning() << "OCR text index directory does not exist:" << m_indexDir;
    }
}

void OcrTextIndexedStrategy::search(const SearchQuery &query)
{
    m_results.clear();

    try {
        // Perform OCR text index search
        performOcrTextSearch(query);
    } catch (const std::exception &e) {
        qWarning() << "OCR Text Index Search Exception:" << e.what();
        emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException));
    }
}

Lucene::QueryPtr OcrTextIndexedStrategy::buildLuceneQuery(const SearchQuery &query)
{
    try {
        m_keywords.clear();
        OcrTextOptionsAPI optAPI(m_options);
        bool mixedAndEnabled = optAPI.isFilenameOcrContentMixedAndSearchEnabled();

        Lucene::QueryPtr mainQuery;
        if (query.type() == SearchQuery::Type::Simple) {
            mainQuery = buildSimpleOcrContentsQuery(query);
        } else if (query.type() == SearchQuery::Type::Boolean) {
            if (query.subQueries().isEmpty()) {
                // For an empty boolean query, match nothing.
                mainQuery = newLucene<Lucene::BooleanQuery>();
            } else {
                // Determine which logic path to take for boolean queries
                if (mixedAndEnabled && query.booleanOperator() == SearchQuery::BooleanOperator::AND) {
                    // New "advanced" AND logic for ocr_contents/filename
                    mainQuery = buildAdvancedAndQuery(query);
                } else {
                    // "Standard" ocr_contents-only logic
                    mainQuery = buildStandardBooleanOcrContentsQuery(query);
                }
            }
        } else {
            qWarning() << "Unknown SearchQuery type encountered.";
            mainQuery = newLucene<Lucene::BooleanQuery>();   // Should not happen
        }

        // Add filename keyword query (before filters, so it replaces empty content query correctly)
        QString filenameKw = optAPI.filenameKeyword();
        if (!filenameKw.isEmpty()) {
            Lucene::QueryPtr filenameQuery = LuceneQueryUtils::buildNGramSearchQuery(
                    QString::fromWCharArray(LuceneFieldNames::OcrText::kFilename),
                    filenameKw);

            if (filenameQuery) {
                // Check if content keywords are effectively empty
                bool noContentKeywords = (query.type() == SearchQuery::Type::Simple)
                        ? query.keyword().isEmpty()
                        : (query.subQueries().isEmpty()
                           || std::all_of(query.subQueries().cbegin(), query.subQueries().cend(),
                                          [](const auto &sq) { return sq.keyword().isEmpty(); }));

                if (noContentKeywords) {
                    // Filename-only search: replace empty content query with filename query
                    mainQuery = filenameQuery;
                } else if (mainQuery) {
                    // Both content and filename: AND combination
                    BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                    finalQuery->add(mainQuery, BooleanClause::MUST);
                    finalQuery->add(filenameQuery, BooleanClause::MUST);
                    mainQuery = finalQuery;
                }
                m_keywords.append(filenameKw);
            }
        }

        // Add path prefix query optimization
        QStringList searchPathsList = m_options.searchPaths();
        if (mainQuery) {
            QueryPtr pathPrefixQuery = LuceneQueryUtils::buildMultiPathPrefixQuery(
                    searchPathsList,
                    QString::fromWCharArray(LuceneFieldNames::OcrText::kAncestorPaths));
            if (pathPrefixQuery) {
                BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                finalQuery->add(mainQuery, BooleanClause::MUST);
                finalQuery->add(pathPrefixQuery, BooleanClause::MUST);
                qInfo() << "Using multi-path prefix query for OCR text search optimization:" << searchPathsList;
                mainQuery = finalQuery;
            }
        }

        // Add excluded paths filter (pushed down to query layer)
        if (mainQuery) {
            const QStringList &excludedPaths = m_options.searchExcludedPaths();
            if (!excludedPaths.isEmpty()) {
                QueryPtr excludedQuery = LuceneQueryUtils::buildMultiPathPrefixQuery(
                        excludedPaths,
                        QString::fromWCharArray(LuceneFieldNames::OcrText::kAncestorPaths));
                if (excludedQuery) {
                    BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                    finalQuery->add(mainQuery, BooleanClause::MUST);
                    finalQuery->add(excludedQuery, BooleanClause::MUST_NOT);
                    mainQuery = finalQuery;
                }
            }
        }

        // Add hidden file filter (pushed down to query layer)
        if (mainQuery && !m_options.includeHidden()) {
            QueryPtr hiddenQuery = Lucene::newLucene<Lucene::TermQuery>(
                    Lucene::newLucene<Lucene::Term>(
                            LuceneFieldNames::OcrText::kIsHidden,
                            L"Y"));
            BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
            finalQuery->add(mainQuery, BooleanClause::MUST);
            finalQuery->add(hiddenQuery, BooleanClause::MUST_NOT);
            mainQuery = finalQuery;
        }

        // When hiddenOnly is true, only return hidden files (MUST kIsHidden:Y)
        if (mainQuery && m_options.hiddenOnly()) {
            QueryPtr hiddenOnlyQuery = Lucene::newLucene<Lucene::TermQuery>(
                    Lucene::newLucene<Lucene::Term>(
                            LuceneFieldNames::OcrText::kIsHidden,
                            L"Y"));
            BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
            finalQuery->add(mainQuery, BooleanClause::MUST);
            finalQuery->add(hiddenOnlyQuery, BooleanClause::MUST);
            mainQuery = finalQuery;
        }

        // Add time range filter query
        if (m_options.hasTimeRangeFilter()) {
            TimeRangeFilter filter = m_options.timeRangeFilter();
            QueryPtr timeQuery = TimeRangeUtils::buildTimeRangeFilterQuery(
                    filter,
                    LuceneFieldNames::OcrText::kBirthTime,
                    LuceneFieldNames::OcrText::kModifyTime);

            if (timeQuery) {
                if (mainQuery) {
                    BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                    finalQuery->add(mainQuery, BooleanClause::MUST);
                    finalQuery->add(timeQuery, BooleanClause::MUST);
                    mainQuery = finalQuery;
                } else {
                    // Time filter alone is a valid query
                    mainQuery = timeQuery;
                }
            }
        }

        // Add file size range filter query
        if (m_options.hasSizeRangeFilter()) {
            SizeRangeFilter sizeFilter = m_options.sizeRangeFilter();
            QueryPtr sizeQuery = TimeRangeUtils::buildNumericRangeQuery(
                    LuceneFieldNames::OcrText::kFileSize,
                    sizeFilter.minSize(), sizeFilter.maxSize(),
                    sizeFilter.includeLower(), sizeFilter.includeUpper());

            if (sizeQuery) {
                if (mainQuery) {
                    BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                    finalQuery->add(mainQuery, BooleanClause::MUST);
                    finalQuery->add(sizeQuery, BooleanClause::MUST);
                    mainQuery = finalQuery;
                } else {
                    // Size filter alone is a valid query
                    mainQuery = sizeQuery;
                }
            }
        }

        // Add file extension filter query (pushed down to index query layer)
        {
            const QStringList fileExtensions = optAPI.fileExtensions();
            if (!fileExtensions.isEmpty() && mainQuery) {
                Lucene::BooleanQueryPtr extQuery = newLucene<BooleanQuery>();
                for (const QString &ext : fileExtensions) {
                    extQuery->add(
                            Lucene::newLucene<Lucene::TermQuery>(
                                    Lucene::newLucene<Lucene::Term>(
                                            LuceneFieldNames::OcrText::kFileExt,
                                            ext.toStdWString())),
                            Lucene::BooleanClause::SHOULD);
                }
                extQuery->setMinimumNumberShouldMatch(1);
                BooleanQueryPtr finalQuery = newLucene<BooleanQuery>();
                finalQuery->add(mainQuery, BooleanClause::MUST);
                finalQuery->add(extQuery, BooleanClause::MUST);
                mainQuery = finalQuery;
            }
        }

        return mainQuery;

    } catch (const Lucene::LuceneException &e) {
        qWarning() << "Error building Lucene query:" << QString::fromStdWString(e.getError());
        return nullptr;
    } catch (const std::exception &e) {
        qWarning() << "Standard exception building Lucene query:" << e.what();
        return nullptr;
    }
}

QueryPtr OcrTextIndexedStrategy::buildAdvancedAndQuery(const SearchQuery &query)
{
    // This method implements the "mixed" AND logic similar to content search.
    Lucene::BooleanQueryPtr overallQuery = newLucene<Lucene::BooleanQuery>();
    Lucene::BooleanQueryPtr mainAndClausesQuery = newLucene<Lucene::BooleanQuery>();
    Lucene::BooleanQueryPtr allOcrContentsQuery = newLucene<Lucene::BooleanQuery>();
    Lucene::BooleanQueryPtr allFilenamesQuery = newLucene<Lucene::BooleanQuery>();
    bool hasValidKeywords = false;

    for (const auto &subQuery : query.subQueries()) {
        m_keywords.append(subQuery.keyword());
        if (subQuery.keyword().isEmpty()) {
            continue;   // Skip empty keywords
        }
        hasValidKeywords = true;

        Lucene::QueryPtr ocrContentsTermQuery = LuceneQueryUtils::buildNGramSearchQuery(
                QString::fromWCharArray(LuceneFieldNames::OcrText::kOcrContents),
                subQuery.keyword());
        Lucene::QueryPtr filenameTermQuery = LuceneQueryUtils::buildNGramSearchQuery(
                QString::fromWCharArray(LuceneFieldNames::OcrText::kFilename),
                subQuery.keyword());

        // Build (ocr_contents:keyword OR filename:keyword)
        Lucene::BooleanQueryPtr combinedTermQuery = newLucene<Lucene::BooleanQuery>();
        combinedTermQuery->add(ocrContentsTermQuery, Lucene::BooleanClause::SHOULD);
        combinedTermQuery->add(filenameTermQuery, Lucene::BooleanClause::SHOULD);

        mainAndClausesQuery->add(combinedTermQuery, Lucene::BooleanClause::MUST);
        allOcrContentsQuery->add(ocrContentsTermQuery, Lucene::BooleanClause::MUST);
        allFilenamesQuery->add(filenameTermQuery, Lucene::BooleanClause::MUST);
    }

    if (!hasValidKeywords) {   // All subQuery keywords were empty
        qWarning() << "No valid keywords found in advanced AND query";
        return newLucene<Lucene::BooleanQuery>();   // Matches nothing
    }

    // Exclude pure filename-only matches
    // Final query: ( (ocr:k1 OR f:k1) AND ... ) AND NOT (f:k1 AND f:k2 ... AND NOT (ocr:k1 AND ocr:k2 ...))
    Lucene::BooleanQueryPtr pureFilenameQuery = newLucene<Lucene::BooleanQuery>();
    pureFilenameQuery->add(allFilenamesQuery, Lucene::BooleanClause::MUST);
    pureFilenameQuery->add(allOcrContentsQuery, Lucene::BooleanClause::MUST_NOT);

    overallQuery->add(mainAndClausesQuery, Lucene::BooleanClause::MUST);
    overallQuery->add(pureFilenameQuery, Lucene::BooleanClause::MUST_NOT);

    return overallQuery;
}

QueryPtr OcrTextIndexedStrategy::buildStandardBooleanOcrContentsQuery(const SearchQuery &query)
{
    // This method implements the "original" boolean logic, searching only "ocr_contents".
    Lucene::BooleanQueryPtr booleanQuery = newLucene<Lucene::BooleanQuery>();

    for (const auto &subQuery : query.subQueries()) {
        m_keywords.append(subQuery.keyword());
        if (subQuery.keyword().isEmpty()) {
            continue;   // Skip empty keywords
        }

        Lucene::QueryPtr termQuery = LuceneQueryUtils::buildNGramSearchQuery(
                QString::fromWCharArray(LuceneFieldNames::OcrText::kOcrContents),
                subQuery.keyword());
        booleanQuery->add(termQuery,
                          query.booleanOperator() == SearchQuery::BooleanOperator::AND ? Lucene::BooleanClause::MUST : Lucene::BooleanClause::SHOULD);
    }

    return booleanQuery;
}

QueryPtr OcrTextIndexedStrategy::buildSimpleOcrContentsQuery(const SearchQuery &query)
{
    m_keywords.append(query.keyword());
    if (query.keyword().isEmpty()) {
        return newLucene<Lucene::BooleanQuery>();   // Match nothing for empty keyword
    }
    return LuceneQueryUtils::buildNGramSearchQuery(
            QString::fromWCharArray(LuceneFieldNames::OcrText::kOcrContents),
            query.keyword());
}

void OcrTextIndexedStrategy::processSearchResults(const Lucene::IndexSearcherPtr &searcher,
                                                  const Lucene::Collection<Lucene::ScoreDocPtr> &scoreDocs)
{
    // Measure the time taken to process search results
    QElapsedTimer resultTimer;
    resultTimer.start();

    auto docsSize = scoreDocs.size();

    OcrTextOptionsAPI optAPI(m_options);
    bool enableHTML = optAPI.isSearchResultHighlightEnabled();
    int previewLen = optAPI.maxPreviewLength() > 0 ? optAPI.maxPreviewLength() : 50;
    bool enableRetrieval = optAPI.isFullTextRetrievalEnabled();
    bool detailedResults = m_options.detailedResultsEnabled();

    // Build field selector to avoid loading the large 'ocr_contents' field when not needed.
    // The ocr_contents field stores OCR-recognized text and loading it for every result
    // (even when only path is needed) causes significant disk I/O overhead.
    Lucene::Collection<Lucene::String> fieldsToLoad = Lucene::Collection<Lucene::String>::newInstance();
    if (enableRetrieval) {
        fieldsToLoad.add(LuceneFieldNames::OcrText::kOcrContents);
    }
    fieldsToLoad.add(LuceneFieldNames::OcrText::kPath);
    if (Q_UNLIKELY(detailedResults)) {
        fieldsToLoad.add(LuceneFieldNames::OcrText::kFilename);
        fieldsToLoad.add(LuceneFieldNames::OcrText::kIsHidden);
        fieldsToLoad.add(LuceneFieldNames::OcrText::kModifyTime);
        fieldsToLoad.add(LuceneFieldNames::OcrText::kBirthTime);
        fieldsToLoad.add(LuceneFieldNames::OcrText::kCheckSum);
        fieldsToLoad.add(LuceneFieldNames::OcrText::kFileSize);
    }
    Lucene::FieldSelectorPtr fieldSelector = newLucene<Lucene::MapFieldSelector>(fieldsToLoad);

    // Pre-allocate to avoid reallocation during append
    m_results.reserve(m_results.size() + static_cast<int>(docsSize));

    for (int32_t i = 0; i < docsSize; ++i) {
        if (m_cancelledRef && m_cancelledRef->load()) {
            qInfo() << "OCR text search cancelled";
            break;
        }

        try {
            Lucene::ScoreDocPtr scoreDoc = scoreDocs[i];
            if (!scoreDoc || scoreDoc->doc < 0) {
                qWarning() << "Invalid ScoreDoc at index" << i;
                continue;
            }

            Lucene::DocumentPtr doc;
            try {
                doc = searcher->doc(scoreDoc->doc, fieldSelector);
                if (!doc) {
                    qWarning() << "Failed to retrieve document at index:" << scoreDoc->doc;
                    continue;
                }
            } catch (const Lucene::LuceneException &e) {
                qWarning() << "Exception while retrieving document:" << QString::fromStdWString(e.getError());
                continue;
            } catch (const std::exception &e) {
                qWarning() << "Standard exception while retrieving document:" << e.what();
                continue;
            }

            // Path filtering, excluded paths, hidden file — handled at query layer
            Lucene::String pathField = doc->get(LuceneFieldNames::OcrText::kPath);
            if (pathField.empty()) {
                qWarning() << "Document missing path field at index:" << scoreDoc->doc;
                continue;
            }

            SearchResult result(QString::fromStdWString(pathField));

            // 设置 OCR 内容结果
            OcrTextResultAPI resultApi(result);
            result.setCustomAttribute("plainContentMatch", QString());
            result.setCustomAttribute("snippetOffset", -1);

            // 使用ContentHighlighter命名空间进行高亮
            if (enableRetrieval) {
                try {
                    Lucene::String ocrContentField = doc->get(LuceneFieldNames::OcrText::kOcrContents);
                    if (!ocrContentField.empty()) {
                        const QString content = QString::fromStdWString(ocrContentField);
                        // Fix: keep a dedicated plain-text snippet for CLI verbose output
                        // instead of reusing HTML-oriented highlightedContent.
                        const ContentHighlighter::PlainSnippetResult plainSnippet = ContentHighlighter::plainSnippet(
                                m_keywords, content, previewLen, previewLen);
                        // 设置原始 OCR 内容
                        resultApi.setOcrContent(content);
                        resultApi.setCharCount(content.size());
                        // 设置高亮内容
                        const QString highlightedContent = ContentHighlighter::customHighlight(
                                m_keywords, content, previewLen, enableHTML);
                        resultApi.setHighlightedContent(highlightedContent);
                        result.setCustomAttribute("plainContentMatch", plainSnippet.content);
                        result.setCustomAttribute("snippetOffset", plainSnippet.snippetOffset);
                    }
                } catch (const Lucene::LuceneException &e) {
                    qWarning() << "Exception retrieving OCR content field:" << QString::fromStdWString(e.getError());
                } catch (const std::exception &e) {
                    qWarning() << "Standard exception retrieving OCR content field:" << e.what();
                }
            }

            // 设置详细结果（如果启用）
            if (Q_UNLIKELY(detailedResults)) {
                // 文件名
                Lucene::String filenameField = doc->get(LuceneFieldNames::OcrText::kFilename);
                if (!filenameField.empty()) {
                    resultApi.setFilename(QString::fromStdWString(filenameField));
                }

                // 隐藏状态
                Lucene::String hiddenField = doc->get(LuceneFieldNames::OcrText::kIsHidden);
                if (!hiddenField.empty()) {
                    resultApi.setIsHidden(QString::fromStdWString(hiddenField).toLower() == "y");
                }

                // 修改时间
                Lucene::String modifyTimeField = doc->get(LuceneFieldNames::OcrText::kModifyTime);
                if (!modifyTimeField.empty()) {
                    bool ok = false;
                    qint64 timestamp = QString::fromStdWString(modifyTimeField).toLongLong(&ok);
                    if (ok && timestamp > 0) {
                        resultApi.setModifyTimestamp(timestamp);
                    }
                }

                // 创建时间
                Lucene::String birthTimeField = doc->get(LuceneFieldNames::OcrText::kBirthTime);
                if (!birthTimeField.empty()) {
                    bool ok = false;
                    qint64 timestamp = QString::fromStdWString(birthTimeField).toLongLong(&ok);
                    if (ok && timestamp > 0) {
                        resultApi.setBirthTimestamp(timestamp);
                    }
                }

                // 文件校验和
                Lucene::String checksumField = doc->get(LuceneFieldNames::OcrText::kCheckSum);
                if (!checksumField.empty()) {
                    resultApi.setChecksum(QString::fromStdWString(checksumField));
                }

                // 文件大小
                Lucene::String fileSizeField = doc->get(LuceneFieldNames::OcrText::kFileSize);
                if (!fileSizeField.empty()) {
                    bool ok = false;
                    qint64 fileSize = QString::fromStdWString(fileSizeField).toLongLong(&ok);
                    if (ok && fileSize > 0) {
                        resultApi.setFileSizeBytes(fileSize);
                    }
                }
            }

            // Add to result collection
            m_results.append(std::move(result));

            // Real-time result emission
            if (Q_UNLIKELY(m_options.resultFoundEnabled()))
                emit resultFound(m_results.last());

        } catch (const Lucene::LuceneException &e) {
            qWarning() << "Error processing result:" << QString::fromStdWString(e.getError());
            continue;
        } catch (const std::exception &e) {
            qWarning() << "Standard exception:" << e.what();
            continue;
        } catch (...) {
            qWarning() << "Unknown exception during result processing";
            continue;
        }
    }

    qInfo() << "OCR text result processing time:" << resultTimer.elapsed() << "ms";
    emit searchFinished(m_results);
}

void OcrTextIndexedStrategy::performOcrTextSearch(const SearchQuery &query)
{
    // 防御性空指针拦截：正常流程 m_cancelledRef 由引擎注入，非空；
    // 若注入失败（setCancelledFlag 拒绝空指针），此处拦截避免底层
    // SearchCancellationGuard / CancellableCollector 解引用 nullptr 导致 SIGSEGV。
    if (!m_cancelledRef) {
        qWarning("OcrTextIndexedStrategy::performOcrTextSearch: cancellation flag is null, aborting search");
        emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException));
        emit searchFinished(m_results);
        return;
    }

    // RAII guard: automatically manage cancellation flag lifecycle
    SearchCancellationGuard guard(m_cancelledRef);

    try {
        // Get index directory
        FSDirectoryPtr directory = FSDirectory::open(m_indexDir.toStdWString());
        if (!directory) {
            qWarning() << "Failed to open OCR text index directory:" << m_indexDir;
            emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexNotFound));
            return;
        }

        // Get index reader
        IndexReaderPtr reader = IndexReader::open(directory, true);
        if (!reader || reader->numDocs() == 0) {
            qWarning() << "OCR text index is empty or cannot be opened";
            emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexNotFound));
            return;
        }

        // Create searcher
        IndexSearcherPtr searcher = newLucene<IndexSearcher>(reader);

        // Build query
        m_currentQuery = buildLuceneQuery(query);
        if (!m_currentQuery) {
            qWarning() << "Failed to build Lucene query for OCR text search";
            emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException));
            return;
        }

        // Execute search
        QElapsedTimer searchTimer;
        searchTimer.start();

        int32_t maxResults = m_options.maxResults() > 0 ? m_options.maxResults() : reader->numDocs();

        // Use custom CancellableCollector for interruptible search
        Collection<ScoreDocPtr> scoreDocs;
        try {
            // Create cancellable collector
            boost::shared_ptr<CancellableCollector> collector = newLucene<CancellableCollector>(m_cancelledRef, maxResults);

            // Execute search with custom collector
            qInfo() << "OCR text search execution start:" << query.keyword();
            searcher->search(m_currentQuery, collector);
            // Get collected documents
            scoreDocs = collector->getScoreDocs();

            qInfo() << "OCR text search execution time:" << searchTimer.elapsed() << "ms"
                    << "Total hits:" << collector->getTotalHits()
                    << "Collected:" << scoreDocs.size()
                    << "Keyword:" << query.keyword()
                    << "Cancelled" << (m_cancelledRef ? m_cancelledRef->load() : false);
        } catch (const SearchCancelledException &e) {
            qInfo() << "OCR text search cancelled during execution";
            emit searchFinished(m_results);
            return;
        } catch (const RuntimeException &e) {
#if LUCENE_HAS_SEARCH_CANCELLATION
            // Check if this is a cancellation exception thrown in ExactPhraseScorer::phraseFreq()
            QString errorMsg = QString::fromStdWString(e.getError());
            if (errorMsg.contains("cancelled", Qt::CaseInsensitive)) {
                qInfo() << "OCR text search cancelled in phraseFreq():" << errorMsg;
                emit searchFinished(m_results);
                return;
            }
#endif
            // Other runtime exceptions, rethrow
            throw;
        }

        // Process search results
        processSearchResults(searcher, scoreDocs);
    } catch (const LuceneException &e) {
        qWarning() << "Lucene search exception:" << QString::fromStdWString(e.getError());
        emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException));
    } catch (const std::exception &e) {
        qWarning() << "Standard exception:" << e.what();
        emit errorOccurred(SearchError(OcrTextSearchErrorCode::OcrTextIndexException));
    }
}

void OcrTextIndexedStrategy::cancel()
{
    if (m_cancelledRef)
        m_cancelledRef->store(true);
}

DFM_SEARCH_END_NS
