/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "paimon/global_index/lucene/lucene_global_index_reader.h"

#include "arrow/c/bridge.h"
#include "lucene++/DisjunctionMaxQuery.h"
#include "lucene++/FileUtils.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/rapidjson_util.h"
#include "paimon/global_index/bitmap_global_index_result.h"
#include "paimon/global_index/bitmap_scored_global_index_result.h"
#include "paimon/global_index/lucene/jieba_analyzer.h"
#include "paimon/global_index/lucene/lucene_collector.h"
#include "paimon/global_index/lucene/lucene_defs.h"
#include "paimon/global_index/lucene/lucene_directory.h"
#include "paimon/global_index/lucene/lucene_filter.h"
#include "paimon/global_index/lucene/lucene_utils.h"
#include "paimon/io/data_input_stream.h"

namespace paimon::lucene {
Result<std::shared_ptr<LuceneGlobalIndexReader>> LuceneGlobalIndexReader::Create(
    const std::string& field_name, const GlobalIndexIOMeta& io_meta,
    const std::shared_ptr<GlobalIndexFileReader>& file_reader,
    const std::map<std::string, std::string>& options, const std::shared_ptr<MemoryPool>& pool) {
    try {
        auto meta_bytes = io_meta.metadata;
        if (!meta_bytes) {
            return Status::Invalid("Lucene global index must have meta data");
        }
        std::map<std::string, std::string> write_options;
        PAIMON_RETURN_NOT_OK(RapidJsonUtil::FromJsonString(
            std::string(meta_bytes->data(), meta_bytes->size()), &write_options));

        std::map<std::string, std::pair<int64_t, int64_t>> file_name_to_offset_and_length;
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> paimon_input,
                               file_reader->GetInputStream(io_meta.file_path));
        DataInputStream data_input_stream(paimon_input);
        PAIMON_ASSIGN_OR_RAISE(int32_t version, data_input_stream.ReadValue<int32_t>());
        if (version != kVersion) {
            return Status::Invalid(
                fmt::format("LuceneGlobalIndex not support version {}", kVersion));
        }
        PAIMON_ASSIGN_OR_RAISE(int32_t num_files, data_input_stream.ReadValue<int32_t>());
        for (int32_t i = 0; i < num_files; i++) {
            PAIMON_ASSIGN_OR_RAISE(int32_t file_name_len, data_input_stream.ReadValue<int32_t>());
            auto file_name_bytes = std::make_shared<Bytes>(file_name_len, pool.get());
            PAIMON_RETURN_NOT_OK(data_input_stream.ReadBytes(file_name_bytes.get()));
            std::string file_name(file_name_bytes->data(), file_name_bytes->size());
            PAIMON_ASSIGN_OR_RAISE(int64_t file_len, data_input_stream.ReadValue<int64_t>());
            PAIMON_ASSIGN_OR_RAISE(int64_t pos, data_input_stream.GetPos());
            file_name_to_offset_and_length[file_name] = {pos, file_len};
            pos += file_len;
            if (i != num_files - 1) {
                PAIMON_RETURN_NOT_OK(data_input_stream.Seek(pos));
            }
        }
        PAIMON_ASSIGN_OR_RAISE(
            int32_t read_buffer_size,
            OptionsUtils::GetValueFromMap(options, kLuceneReadBufferSize, kDefaultReadBufferSize));
        Lucene::DirectoryPtr lucene_dir = Lucene::newLucene<LuceneDirectory>(
            PathUtil::GetParentDirPath(io_meta.file_path), file_name_to_offset_and_length,
            paimon_input, read_buffer_size);

        Lucene::IndexReaderPtr reader = Lucene::IndexReader::open(lucene_dir, /*read_only=*/true);
        Lucene::IndexSearcherPtr searcher = Lucene::newLucene<Lucene::IndexSearcher>(reader);

        PAIMON_ASSIGN_OR_RAISE(std::string dictionary_dir, LuceneUtils::GetJiebaDictionaryDir());
        auto jieba = std::make_shared<cppjieba::Jieba>(
            dictionary_dir + "/jieba.dict.utf8", dictionary_dir + "/hmm_model.utf8",
            dictionary_dir + "/user.dict.utf8", dictionary_dir + "/idf.utf8",
            dictionary_dir + "/stop_words.utf8");

        // priority: read options > write options > kDefaultJiebaTokenizeMode
        PAIMON_ASSIGN_OR_RAISE(
            std::string tokenize_mode,
            OptionsUtils::GetValueFromMap(options, kJiebaTokenizeMode, std::string("")));
        if (tokenize_mode.empty()) {
            PAIMON_ASSIGN_OR_RAISE(tokenize_mode, OptionsUtils::GetValueFromMap(
                                                      write_options, kJiebaTokenizeMode,
                                                      std::string(kDefaultJiebaTokenizeMode)));
        }
        return std::shared_ptr<LuceneGlobalIndexReader>(new LuceneGlobalIndexReader(
            LuceneUtils::StringToWstring(field_name), searcher, tokenize_mode, jieba));
    } catch (const std::exception& e) {
        return Status::Invalid(
            fmt::format("create lucene global index reader failed, with {} error.", e.what()));
    } catch (...) {
        return Status::UnknownError(
            "create lucene global index reader failed, with unknown error.");
    }
}

std::vector<std::wstring> LuceneGlobalIndexReader::TokenizeQuery(const std::string& query) const {
    std::vector<std::string> terms;
    JiebaTokenizer::CutWithMode(tokenize_mode_, jieba_.get(), query, &terms);
    std::vector<std::string_view> normalized_terms;
    JiebaTokenizer::Normalize(jieba_->extractor.GetStopWords(), &terms, &normalized_terms);
    std::vector<std::wstring> wterms;
    wterms.reserve(normalized_terms.size());
    for (const auto& term : normalized_terms) {
        wterms.push_back(LuceneUtils::StringToWstring(term));
    }
    return wterms;
}

std::string LuceneGlobalIndexReader::NormalizeWildcardQuery(const std::string& query) {
    std::string normalized_query;
    normalized_query.reserve(query.size());
    size_t term_begin = 0;
    for (size_t i = 0; i <= query.size(); i++) {
        if (i != query.size() && query[i] != '*' && query[i] != '?') {
            continue;
        }
        std::string term = query.substr(term_begin, i - term_begin);
        JiebaTokenizer::NormalizeCase(&term);
        normalized_query.append(term);
        if (i != query.size()) {
            normalized_query.push_back(query[i]);
        }
        term_begin = i + 1;
    }
    return normalized_query;
}

Lucene::QueryPtr LuceneGlobalIndexReader::ConstructMatchQuery(
    const std::shared_ptr<FullTextSearch>& full_text_search) const noexcept(false) {
    assert(full_text_search->search_type == FullTextSearch::SearchType::MATCH_ALL ||
           full_text_search->search_type == FullTextSearch::SearchType::MATCH_ANY);
    Lucene::BooleanClause::Occur occur =
        full_text_search->search_type == FullTextSearch::SearchType::MATCH_ALL
            ? Lucene::BooleanClause::Occur::MUST
            : Lucene::BooleanClause::Occur::SHOULD;
    std::vector<std::wstring> query_terms = TokenizeQuery(full_text_search->query);
    if (query_terms.size() == 1) {
        return Lucene::newLucene<Lucene::TermQuery>(
            Lucene::newLucene<Lucene::Term>(wfield_name_, query_terms[0]));
    } else {
        auto typed_query = Lucene::newLucene<Lucene::BooleanQuery>();
        for (const auto& term : query_terms) {
            typed_query->add(Lucene::newLucene<Lucene::TermQuery>(
                                 Lucene::newLucene<Lucene::Term>(wfield_name_, term)),
                             occur);
        }
        return typed_query;
    }
}

Lucene::QueryPtr LuceneGlobalIndexReader::ConstructPhraseQuery(
    const std::shared_ptr<FullTextSearch>& full_text_search) const noexcept(false) {
    assert(full_text_search->search_type == FullTextSearch::SearchType::PHRASE);
    std::vector<std::wstring> query_terms = TokenizeQuery(full_text_search->query);
    auto typed_query = Lucene::newLucene<Lucene::PhraseQuery>();
    for (const auto& term : query_terms) {
        typed_query->add(Lucene::newLucene<Lucene::Term>(wfield_name_, term));
    }
    return typed_query;
}

Lucene::QueryPtr LuceneGlobalIndexReader::ConstructPrefixQuery(
    const std::shared_ptr<FullTextSearch>& full_text_search) const noexcept(false) {
    assert(full_text_search->search_type == FullTextSearch::SearchType::PREFIX);
    auto create_query = [this](const std::string& query) -> Lucene::QueryPtr {
        return Lucene::newLucene<Lucene::PrefixQuery>(
            Lucene::newLucene<Lucene::Term>(wfield_name_, LuceneUtils::StringToWstring(query)));
    };
    std::string normalized_query = full_text_search->query;
    JiebaTokenizer::NormalizeCase(&normalized_query);
    Lucene::QueryPtr query = create_query(full_text_search->query);
    if (normalized_query == full_text_search->query) {
        return query;
    }

    // Preserve the original query for mixed ASCII/CJK indexed terms such as "B超", whose
    // complete token is not lowercased by the index analyzer. The normalized alternative still
    // matches pure ASCII terms. DisjunctionMax avoids double-counting scores if both match.
    auto disjunction = Lucene::newLucene<Lucene::DisjunctionMaxQuery>(0.0);
    disjunction->add(query);
    disjunction->add(create_query(normalized_query));
    return disjunction;
}

Lucene::QueryPtr LuceneGlobalIndexReader::ConstructWildCardQuery(
    const std::shared_ptr<FullTextSearch>& full_text_search) const noexcept(false) {
    assert(full_text_search->search_type == FullTextSearch::SearchType::WILDCARD);
    auto create_query = [this](const std::string& query) -> Lucene::QueryPtr {
        return Lucene::newLucene<Lucene::WildcardQuery>(
            Lucene::newLucene<Lucene::Term>(wfield_name_, LuceneUtils::StringToWstring(query)));
    };
    std::string normalized_query = NormalizeWildcardQuery(full_text_search->query);
    Lucene::QueryPtr query = create_query(full_text_search->query);
    if (normalized_query == full_text_search->query) {
        return query;
    }

    auto disjunction = Lucene::newLucene<Lucene::DisjunctionMaxQuery>(0.0);
    disjunction->add(query);
    disjunction->add(create_query(normalized_query));
    return disjunction;
}

Result<std::shared_ptr<GlobalIndexResult>> LuceneGlobalIndexReader::SearchWithLimit(
    const Lucene::QueryPtr& query, const std::shared_ptr<FullTextSearch>& full_text_search) const
    noexcept(false) {
    assert(full_text_search->limit);
    Lucene::FilterPtr filter =
        full_text_search->pre_filter
            ? Lucene::newLucene<LuceneFilter>(&(full_text_search->pre_filter.value()))
            : Lucene::FilterPtr();

    Lucene::TopDocsPtr results = searcher_->search(query, filter, full_text_search->limit.value());

    // prepare BitmapScoredGlobalIndexResult
    std::map<int64_t, float> id_to_score;
    for (auto score_doc : results->scoreDocs) {
        id_to_score[static_cast<int64_t>(score_doc->doc)] = static_cast<float>(score_doc->score);
    }
    RoaringBitmap64 bitmap;
    std::vector<float> scores;
    scores.reserve(id_to_score.size());
    for (const auto& [id, score] : id_to_score) {
        bitmap.Add(id);
        scores.push_back(score);
    }
    return std::make_shared<BitmapScoredGlobalIndexResult>(std::move(bitmap), std::move(scores));
}

std::shared_ptr<GlobalIndexResult> LuceneGlobalIndexReader::SearchWithNoLimit(
    const Lucene::QueryPtr& query, const std::shared_ptr<FullTextSearch>& full_text_search) const
    noexcept(false) {
    assert(!full_text_search->limit);
    Lucene::FilterPtr filter =
        full_text_search->pre_filter
            ? Lucene::newLucene<LuceneFilter>(&(full_text_search->pre_filter.value()))
            : Lucene::FilterPtr();

    // with no limit & no score
    auto collector = Lucene::newLucene<LuceneCollector>();
    searcher_->search(query, filter, collector);
    return std::make_shared<BitmapGlobalIndexResult>(
        [collector]() -> Result<RoaringBitmap64> { return collector->GetBitmap(); });
}

Result<std::shared_ptr<GlobalIndexResult>> LuceneGlobalIndexReader::VisitFullTextSearch(
    const std::shared_ptr<FullTextSearch>& full_text_search) {
    if (!full_text_search) {
        return Status::Invalid("VisitFullTextSearch: null FullTextSearch pointer");
    }
    if (full_text_search->min_score.has_value()) {
        // The lucene backend does not support min_score pushdown. Fail loudly
        // instead of silently ignoring the threshold and returning unfiltered
        // results, which would be a correctness bug for the caller.
        return Status::NotImplemented(
            "lucene full-text search does not support min_score; "
            "min_score pushdown is only available on the tantivy backend");
    }
    try {
        Lucene::QueryPtr query;
        switch (full_text_search->search_type) {
            case FullTextSearch::SearchType::MATCH_ALL:
            case FullTextSearch::SearchType::MATCH_ANY: {
                query = ConstructMatchQuery(full_text_search);
                break;
            }
            case FullTextSearch::SearchType::PHRASE: {
                query = ConstructPhraseQuery(full_text_search);
                break;
            }
            case FullTextSearch::SearchType::PREFIX: {
                query = ConstructPrefixQuery(full_text_search);
                break;
            }
            case FullTextSearch::SearchType::WILDCARD: {
                query = ConstructWildCardQuery(full_text_search);
                break;
            }
            default:
                return Status::Invalid(
                    fmt::format("Not support for FullTextSearch SearchType {}",
                                static_cast<int32_t>(full_text_search->search_type)));
        }
        if (full_text_search->limit) {
            return SearchWithLimit(query, full_text_search);
        } else {
            return SearchWithNoLimit(query, full_text_search);
        }
    } catch (const std::exception& e) {
        return Status::Invalid(
            fmt::format("visit full text search failed, with {} error.", e.what()));
    } catch (...) {
        return Status::UnknownError("visit full text search failed, with unknown error.");
    }
}

}  // namespace paimon::lucene
