/*
 * 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/format/parquet/parquet_file_batch_reader.h"

#include <algorithm>
#include <cstddef>
#include <unordered_map>

#include "arrow/acero/options.h"
#include "arrow/array/array_nested.h"
#include "arrow/c/abi.h"
#include "arrow/c/bridge.h"
#include "arrow/compute/api.h"
#include "arrow/dataset/dataset.h"
#include "arrow/dataset/file_base.h"
#include "arrow/dataset/file_parquet.h"
#include "arrow/dataset/type_fwd.h"
#include "arrow/io/caching.h"
#include "arrow/io/interfaces.h"
#include "arrow/record_batch.h"
#include "arrow/type.h"
#include "arrow/util/range.h"
#include "arrow/util/thread_pool.h"
#include "fmt/format.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/core/schema/arrow_schema_validator.h"
#include "paimon/core/utils/nested_projection_utils.h"
#include "paimon/format/parquet/parquet_field_id_converter.h"
#include "paimon/format/parquet/parquet_format_defs.h"
#include "paimon/format/parquet/parquet_schema_util.h"
#include "paimon/format/parquet/parquet_timestamp_converter.h"
#include "paimon/format/parquet/predicate_converter.h"
#include "paimon/reader/batch_reader.h"
#include "paimon/utils/roaring_bitmap32.h"
#include "parquet/arrow/reader.h"
#include "parquet/properties.h"

namespace arrow {
class MemoryPool;
}  // namespace arrow
namespace paimon {
class Predicate;
}  // namespace paimon

namespace paimon::parquet {

namespace {
// LIST/MAP do not support pruning fields from their nested value types, but physical and
// logical leaf types may still differ (for example, Parquet reports LTZ timestamps as UTC
// while Paimon exposes them in the local timezone). Compare only the nested projection shape
// here so those representation differences are handled by the normal cast path.
bool HasSameNestedProjectionShape(const std::shared_ptr<arrow::DataType>& read_type,
                                  const std::shared_ptr<arrow::DataType>& file_type) {
    const bool read_is_nested = ArrowSchemaValidator::IsNestedType(read_type);
    const bool file_is_nested = ArrowSchemaValidator::IsNestedType(file_type);
    if (!read_is_nested || !file_is_nested) {
        if (read_is_nested || file_is_nested) {
            return false;
        }
        // ParquetTimestampConverter explicitly supports timestamp unit and timezone
        // conversion after reading. Other atomic type differences remain unsupported here.
        if (read_type->id() == arrow::Type::TIMESTAMP &&
            file_type->id() == arrow::Type::TIMESTAMP) {
            const auto& read_timestamp = static_cast<const arrow::TimestampType&>(*read_type);
            const auto& file_timestamp = static_cast<const arrow::TimestampType&>(*file_type);
            return read_timestamp.unit() == file_timestamp.unit() ||
                   (file_timestamp.unit() == arrow::TimeUnit::MILLI &&
                    read_timestamp.unit() == arrow::TimeUnit::SECOND);
        }
        return read_type->Equals(file_type);
    }
    if (read_type->id() != file_type->id()) {
        return false;
    }

    switch (file_type->id()) {
        case arrow::Type::STRUCT: {
            if (read_type->num_fields() != file_type->num_fields()) {
                return false;
            }
            for (int32_t i = 0; i < file_type->num_fields(); ++i) {
                const auto& read_child = read_type->field(i);
                const auto& file_child = file_type->field(i);
                if (read_child->name() != file_child->name() ||
                    !HasSameNestedProjectionShape(read_child->type(), file_child->type())) {
                    return false;
                }
            }
            return true;
        }
        case arrow::Type::LIST: {
            const auto& read_list = static_cast<const arrow::ListType&>(*read_type);
            const auto& file_list = static_cast<const arrow::ListType&>(*file_type);
            return HasSameNestedProjectionShape(read_list.value_type(), file_list.value_type());
        }
        case arrow::Type::MAP: {
            const auto& read_map = static_cast<const arrow::MapType&>(*read_type);
            const auto& file_map = static_cast<const arrow::MapType&>(*file_type);
            return HasSameNestedProjectionShape(read_map.key_type(), file_map.key_type()) &&
                   HasSameNestedProjectionShape(read_map.item_type(), file_map.item_type());
        }
        default:
            return false;
    }
}
}  // namespace

ParquetFileBatchReader::ParquetFileBatchReader(
    std::shared_ptr<arrow::io::RandomAccessFile>&& input_stream,
    std::unique_ptr<FileReaderWrapper>&& reader, const std::map<std::string, std::string>& options,
    const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
    std::shared_ptr<std::atomic<uint64_t>> storage_read_bytes)
    : options_(options),
      arrow_pool_(arrow_pool),
      input_stream_(std::move(input_stream)),
      reader_(std::move(reader)),
      metrics_(std::make_shared<MetricsImpl>()),
      storage_read_bytes_(std::move(storage_read_bytes)),
      logger_(Logger::GetLogger("ParquetFileBatchReader")) {}

Result<std::unique_ptr<ParquetFileBatchReader>> ParquetFileBatchReader::Create(
    std::shared_ptr<arrow::io::RandomAccessFile>&& input_stream,
    const std::map<std::string, std::string>& options, int32_t batch_size,
    std::shared_ptr<::parquet::FileMetaData> file_metadata,
    std::shared_ptr<std::atomic<uint64_t>> storage_read_bytes,
    const std::shared_ptr<arrow::MemoryPool>& pool) {
    try {
        assert(input_stream);
        PAIMON_ASSIGN_OR_RAISE(::parquet::ReaderProperties reader_properties,
                               CreateReaderProperties(pool, options));

        PAIMON_ASSIGN_OR_RAISE(::parquet::ArrowReaderProperties arrow_reader_properties,
                               CreateArrowReaderProperties(pool, options, batch_size));

        ::parquet::arrow::FileReaderBuilder file_reader_builder;
        PAIMON_RETURN_NOT_OK_FROM_ARROW(
            file_reader_builder.Open(input_stream, reader_properties, std::move(file_metadata)));

        std::unique_ptr<::parquet::arrow::FileReader> file_reader;
        PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.memory_pool(pool.get())
                                            ->properties(arrow_reader_properties)
                                            ->Build(&file_reader));
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileReaderWrapper> reader,
                               FileReaderWrapper::Create(std::move(file_reader),
                                                         static_cast<int64_t>(batch_size), pool));
        auto parquet_file_batch_reader = std::unique_ptr<ParquetFileBatchReader>(
            new ParquetFileBatchReader(std::move(input_stream), std::move(reader), options, pool,
                                       std::move(storage_read_bytes)));
        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::ArrowSchema> file_schema,
                               parquet_file_batch_reader->GetFileSchema());
        PAIMON_RETURN_NOT_OK(parquet_file_batch_reader->SetReadSchema(
            file_schema.get(), /*predicate=*/nullptr, /*selection_bitmap=*/std::nullopt));
        return parquet_file_batch_reader;
    }
    PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::Create")
}

Result<std::unique_ptr<::ArrowSchema>> ParquetFileBatchReader::GetFileSchema() const {
    try {
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema, reader_->GetSchema());
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> new_schema,
                               ParquetFieldIdConverter::GetPaimonIdsFromParquetIds(file_schema));
        PAIMON_ASSIGN_OR_RAISE(
            std::shared_ptr<arrow::DataType> new_type,
            ParquetTimestampConverter::AdjustTimezone(arrow::struct_(new_schema->fields())));

        auto c_schema = std::make_unique<::ArrowSchema>();
        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportType(*new_type, c_schema.get()));
        return c_schema;
    }
    PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::GetFileSchema")
}

Status ParquetFileBatchReader::SetReadSchema(
    ::ArrowSchema* schema, const std::shared_ptr<Predicate>& predicate,
    const std::optional<RoaringBitmap32>& selection_bitmap) {
    try {
        if (!schema) {
            return Status::Invalid("SetReadSchema failed: read schema cannot be nullptr");
        }
        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> read_schema,
                                          arrow::ImportSchema(schema));

        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema, reader_->GetSchema());

        // Recursively match read_schema against file_schema by field names.
        // STRUCT supports sub-field projection; LIST/MAP require exact type match.
        PAIMON_ASSIGN_OR_RAISE(std::vector<int32_t> column_indices,
                               ComputeNestedColumnIndices(read_schema, file_schema));

        // Build column name to index map for page-level filtering.
        // We still need the full per-top-level-field leaf indices for predicate pushdown.
        std::unordered_map<std::string, std::vector<int32_t>> field_index_map;
        int32_t flat_idx = 0;
        for (const auto& field : file_schema->fields()) {
            std::vector<int32_t> leaf_indices;
            FlattenSchema(field->type(), &flat_idx, &leaf_indices);
            field_index_map[field->name()] = leaf_indices;
        }

        TargetRowGroups target_row_groups =
            TargetRowGroup::MakeForAllRowGroups(reader_->GetAllRowGroupRanges());
        PAIMON_ASSIGN_OR_RAISE(
            bool enable_page_index_filter,
            OptionsUtils::GetValueFromMap<bool>(options_, PARQUET_READ_ENABLE_PAGE_INDEX_FILTER,
                                                DEFAULT_PARQUET_READ_ENABLE_PAGE_INDEX_FILTER));

        if (predicate) {
            PAIMON_ASSIGN_OR_RAISE(
                target_row_groups,
                FilterRowGroupsByPredicate(predicate, file_schema, target_row_groups));
        }
        if (selection_bitmap) {
            PAIMON_ASSIGN_OR_RAISE(
                target_row_groups,
                FilterRowGroupsByBitmap(selection_bitmap.value(), target_row_groups));
            if (enable_page_index_filter) {
                // To decide which strategy to use, "trim" or "coalesce". "Coalesce" By default.
                PAIMON_ASSIGN_OR_RAISE(
                    std::string strategy,
                    OptionsUtils::GetValueFromMap<std::string>(
                        options_, PARQUET_READ_BITMAP_ROW_RANGE_REFINING_STRATEGY,
                        DEFAULT_PARQUET_READ_BITMAP_STRATEGY));
                if (strategy == "trim") {
                    PAIMON_ASSIGN_OR_RAISE(
                        target_row_groups,
                        RefineRowRangesByTrimming(selection_bitmap.value(), target_row_groups,
                                                  column_indices));
                } else if (strategy == "coalesce") {
                    PAIMON_ASSIGN_OR_RAISE(
                        target_row_groups,
                        RefineRowRangesByCoalescing(selection_bitmap.value(), target_row_groups));
                } else {
                    return Status::Invalid(
                        fmt::format("Invalid row range refining strategy :{}, valid strategies "
                                    "are: trim, coalesce",
                                    strategy));
                }
            }
        }
        // Apply page-level filtering after bitmap pruning so we don't read page index
        // pages for row groups that the bitmap already excluded.
        // If no predicate is provided, skip page-level filtering
        if (predicate && !target_row_groups.empty()) {
            if (enable_page_index_filter) {
                // Build column name to index map for page-level filtering.
                // For leaf columns, indices[0] is the correct leaf column index in Parquet.
                // For nested types (struct/list/map), FlattenSchema produces multiple leaf indices,
                // but predicate pushdown only targets leaf columns with simple types, so indices[0]
                // is always the correct single leaf index for predicate evaluation.
                std::map<std::string, int32_t> column_name_to_index;
                for (const auto& [name, indices] : field_index_map) {
                    if (!indices.empty()) {
                        column_name_to_index[name] = indices[0];
                    }
                }
                PAIMON_ASSIGN_OR_RAISE(
                    target_row_groups,
                    FilterRowGroupsByPageIndex(predicate, column_name_to_index, target_row_groups));
            }
        }

        read_data_type_ = arrow::struct_(read_schema->fields());

        metrics_->SetCounter(ParquetMetrics::READ_ROW_GROUPS_TOTAL,
                             reader_->GetNumberOfRowGroups());
        metrics_->SetCounter(ParquetMetrics::READ_ROW_GROUPS_AFTER_FILTER,
                             target_row_groups.size());

        PAIMON_RETURN_NOT_OK(UpdateAllTargetRowRanges(target_row_groups));
        PAIMON_RETURN_NOT_OK(reader_->PrepareForReadingLazy(target_row_groups, column_indices));
    }
    PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::SetReadSchema")
    return Status::OK();
}

Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByPredicate(
    const std::shared_ptr<Predicate>& predicate, const std::shared_ptr<arrow::Schema> file_schema,
    const TargetRowGroups& src_row_groups) const {
    if (!predicate) {
        return Status::Invalid("cannot pushdown an empty predicate");
    }
    // convert paimon predicate to arrow expression
    PAIMON_ASSIGN_OR_RAISE(
        uint32_t predicate_node_count_limit,
        OptionsUtils::GetValueFromMap<uint32_t>(options_, PARQUET_READ_PREDICATE_NODE_COUNT_LIMIT,
                                                DEFAULT_PARQUET_READ_PREDICATE_NODE_COUNT_LIMIT));
    PAIMON_ASSIGN_OR_RAISE(arrow::compute::Expression expr,
                           PredicateConverter::Convert(predicate, predicate_node_count_limit));
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::Expression bind_expr, expr.Bind(*file_schema));

    // prepare file source
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(int64_t file_length, input_stream_->GetSize());
    auto file_source = arrow::dataset::FileSource(input_stream_, /*size=*/file_length);

    // filter row group by arrow expression and row group meta
    auto parquet_file_format = std::make_shared<arrow::dataset::ParquetFileFormat>();
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
        std::shared_ptr<arrow::dataset::ParquetFileFragment> file_fragment,
        parquet_file_format->MakeFragment(
            file_source, /*partition_expression=*/PredicateConverter::AlwaysTrue(),
            /*physical_schema=*/nullptr,
            /*row_groups=*/TargetRowGroup::GetRowGroupIndices(src_row_groups)));
    PAIMON_RETURN_NOT_OK_FROM_ARROW(
        file_fragment->EnsureCompleteMetadata(reader_->GetFileReader()));
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::dataset::FragmentVector target_fragments,
                                      file_fragment->SplitByRowGroup(bind_expr));
    TargetRowGroups target_row_groups;
    target_row_groups.reserve(src_row_groups.size());
    for (const auto& fragment : target_fragments) {
        auto parquet_fragment = dynamic_cast<arrow::dataset::ParquetFileFragment*>(fragment.get());
        if (!parquet_fragment) {
            return Status::Invalid("cannot cast to ParquetFileFragment in ParquetFileBatchReader");
        }
        for (auto rg_index : parquet_fragment->row_groups()) {
            for (const auto& row_group : src_row_groups) {
                if (row_group.GetRowGroupIndex() == rg_index) {
                    target_row_groups.emplace_back(row_group);
                    break;
                }
            }
        }
    }
    return target_row_groups;
}

Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByBitmap(
    const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups) const {
    if (bitmap.IsEmpty()) {
        return Status::Invalid("cannot push down an empty bitmap to ParquetFileBatchReader");
    }

    const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();

    TargetRowGroups target_row_groups;
    for (const auto& row_group : src_row_groups) {
        int32_t row_group_idx = row_group.GetRowGroupIndex();
        if (static_cast<size_t>(row_group_idx) >= all_row_group_ranges.size()) {
            return Status::Invalid(
                fmt::format("src row group {} not in row group meta", row_group_idx));
        }
        // half open interval [start_row_idx, end_row_idx)
        const auto& [start_row_idx, end_row_idx] = all_row_group_ranges[row_group_idx];
        if (!bitmap.ContainsAny(start_row_idx, end_row_idx)) {
            continue;
        }
        target_row_groups.emplace_back(row_group);
    }
    return target_row_groups;
}

RowRanges ParquetFileBatchReader::CoalesceNearbyRanges(const RowRanges& input,
                                                       uint64_t hole_size_limit) {
    if (input.IsEmpty()) {
        return RowRanges();
    }

    const auto& ranges = input.GetRanges();
    RowRanges result;
    int64_t merge_start = ranges.front().from;
    int64_t merge_end = ranges.front().to;

    for (size_t i = 1; i < ranges.size(); ++i) {
        // Gap between [merge_start, merge_end] and [ranges[i].from, ranges[i].to]
        int64_t gap = ranges[i].from - merge_end - 1;
        if (static_cast<uint64_t>(gap) > hole_size_limit) {
            result.Add(RowRanges::Range(merge_start, merge_end));
            merge_start = ranges[i].from;
        }
        merge_end = ranges[i].to;
    }
    result.Add(RowRanges::Range(merge_start, merge_end));
    return result;
}

RowRanges ParquetFileBatchReader::BitmapToContiguousRanges(const RoaringBitmap32& bitmap,
                                                           uint64_t start_row, uint64_t end_row) {
    RowRanges ranges;
    if (bitmap.IsEmpty() || start_row >= end_row) {
        return ranges;
    }

    auto it = bitmap.EqualOrLarger(static_cast<int32_t>(start_row));
    const auto end = bitmap.End();
    if (it == end || static_cast<uint64_t>(*it) >= end_row) {
        return ranges;
    }

    auto run_start = static_cast<int64_t>(*it);
    auto prev = run_start;

    for (++it; it != end; ++it) {
        auto current = static_cast<int64_t>(*it);
        if (current >= static_cast<int64_t>(end_row)) {
            break;
        }
        if (current != prev + 1) {
            ranges.Add(RowRanges::Range(run_start - start_row, prev - start_row));
            run_start = current;
        }
        prev = current;
    }
    ranges.Add(RowRanges::Range(run_start - start_row, prev - start_row));
    return ranges;
}

Result<TargetRowGroups> ParquetFileBatchReader::RefineRowRangesByCoalescing(
    const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups) const {
    PAIMON_ASSIGN_OR_RAISE(const uint64_t hole_size_limit,
                           OptionsUtils::GetValueFromMap<uint64_t>(
                               options_, PARQUET_READ_ROW_RANGES_COALESCE_HOLE_SIZE_LIMIT,
                               DEFAULT_PARQUET_READ_ROW_RANGES_COALESCE_HOLE_SIZE_LIMIT));

    const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();
    TargetRowGroups target_row_groups;
    target_row_groups.reserve(src_row_groups.size());

    for (const auto& row_group : src_row_groups) {
        int32_t rg_index = row_group.GetRowGroupIndex();
        uint64_t rg_start_row = all_row_group_ranges[rg_index].first;
        uint64_t rg_end_row = all_row_group_ranges[rg_index].second;

        // Step 1: bitmap -> contiguous ranges (relative to row group start).
        // Step 2: coalesce ranges with small gaps to reduce range count.
        RowRanges contiguous = BitmapToContiguousRanges(bitmap, rg_start_row, rg_end_row);
        RowRanges coalesced = CoalesceNearbyRanges(contiguous, hole_size_limit);

        auto rg_row_count = static_cast<int64_t>(rg_end_row - rg_start_row);
        if (coalesced.IsEmpty()) {
            continue;
        }
        if (coalesced.RowCount() == rg_row_count) {
            target_row_groups.emplace_back(row_group);
        } else {
            target_row_groups.emplace_back(rg_index, true, std::move(coalesced));
        }
    }
    return target_row_groups;
}

Result<TargetRowGroups> ParquetFileBatchReader::RefineRowRangesByTrimming(
    const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups,
    const std::vector<int32_t>& column_indices) const {
    auto page_index_reader = reader_->GetPageIndexReader();
    if (!page_index_reader) {
        return src_row_groups;
    }

    TargetRowGroups target_row_groups;
    target_row_groups.reserve(src_row_groups.size());
    for (const auto& row_group : src_row_groups) {
        auto filtered = TrimRowGroupPageRanges(bitmap, row_group, column_indices);
        if (!filtered.GetRowRanges().IsEmpty()) {
            target_row_groups.emplace_back(std::move(filtered));
        }
    }
    return target_row_groups;
}

TargetRowGroup ParquetFileBatchReader::TrimRowGroupPageRanges(
    const RoaringBitmap32& bitmap, const TargetRowGroup& row_group,
    const std::vector<int32_t>& column_indices) const {
    int32_t row_group_idx = row_group.GetRowGroupIndex();
    auto rg_page_index_reader = reader_->GetRowGroupPageIndexReader(row_group_idx);
    if (!rg_page_index_reader) {
        return row_group;
    }

    const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();
    uint64_t rg_start_row = all_row_group_ranges[row_group_idx].first;
    uint64_t rg_row_count = all_row_group_ranges[row_group_idx].second - rg_start_row;

    RowRanges row_ranges = row_group.GetRowRanges();
    for (int32_t col_index : column_indices) {
        auto offset_index = rg_page_index_reader->GetOffsetIndex(col_index);
        if (!offset_index) {
            continue;
        }
        auto page_ranges = ComputeColumnPageRanges(bitmap, offset_index->page_locations(),
                                                   rg_start_row, rg_row_count);
        row_ranges = RowRanges::Intersection(row_ranges, page_ranges);
    }
    if (row_ranges.RowCount() == static_cast<int64_t>(rg_row_count)) {
        return row_group;
    } else {
        return TargetRowGroup(row_group_idx, true, std::move(row_ranges));
    }
}

RowRanges ParquetFileBatchReader::ComputeColumnPageRanges(
    const RoaringBitmap32& bitmap, const std::vector<::parquet::PageLocation>& page_locations,
    uint64_t rg_start_row, uint64_t rg_row_count) {
    RowRanges page_row_ranges;
    for (size_t page_idx = 0; page_idx < page_locations.size(); ++page_idx) {
        // half open interval [first_row, last_row)
        auto first_row = page_locations[page_idx].first_row_index;
        auto last_row = page_idx + 1 < page_locations.size()
                            ? page_locations[page_idx + 1].first_row_index
                            : rg_row_count;

        if (!bitmap.ContainsAny(rg_start_row + first_row, rg_start_row + last_row)) {
            continue;
        }
        // closed interval [range_start_row, range_end_row]
        auto range_start_row = bitmap.NextValue(rg_start_row + first_row);
        auto range_end_row = bitmap.PreviousValue(rg_start_row + last_row);
        if (!range_start_row.has_value() || !range_end_row.has_value()) {
            continue;
        }
        page_row_ranges.Add(
            Range(range_start_row.value() - rg_start_row, range_end_row.value() - rg_start_row));
    }
    return page_row_ranges;
}

// Uses page-level column index statistics to filter row groups and store per-row-group
// RowRanges for true page-level skipping. A row group is excluded if ALL its pages are
// determined to not match the predicate. For partially matched row groups, RowRanges
// are stored for page-level filtering during reading.
Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByPageIndex(
    const std::shared_ptr<Predicate>& predicate,
    const std::map<std::string, int32_t>& column_name_to_index,
    const TargetRowGroups& src_row_groups) const {
    if (!predicate) {
        return src_row_groups;
    }

    auto page_index_reader = reader_->GetPageIndexReader();
    if (!page_index_reader) {
        PAIMON_LOG_DEBUG(logger_,
                         "Page index not available in file, skipping page-level filtering (%s)",
                         PARQUET_WRITE_ENABLE_PAGE_INDEX);
        return src_row_groups;
    }

    auto file_metadata = reader_->GetFileReader()->parquet_reader()->metadata();

    TargetRowGroups target_row_groups;

    for (const auto& row_group : src_row_groups) {
        int32_t row_group_idx = row_group.GetRowGroupIndex();
        auto result =
            reader_->CalculateFilteredRowRanges(row_group_idx, predicate, column_name_to_index);

        if (!result.ok()) {
            target_row_groups.emplace_back(row_group);
            continue;
        }

        const auto& row_ranges = result.value();
        if (!row_ranges.IsEmpty()) {
            int64_t rg_row_count = file_metadata->RowGroup(row_group_idx)->num_rows();
            auto intersection = row_group.IsPartiallyMatched()
                                    ? RowRanges::Intersection(row_group.GetRowRanges(), row_ranges)
                                    : row_ranges;
            if (intersection.IsEmpty()) {
                continue;
            }
            if (intersection.RowCount() < rg_row_count) {
                target_row_groups.emplace_back(row_group_idx, true, intersection);
            } else {
                target_row_groups.emplace_back(row_group);
            }
        }
    }

    return target_row_groups;
}

Result<BatchReader::ReadBatch> ParquetFileBatchReader::NextBatch() {
    try {
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch> batch, reader_->Next());
        if (batch == nullptr) {
            row_mapping_.clear();
            return BatchReader::MakeEofBatch();
        }
        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
                                          batch->ToStructArray());
        PAIMON_RETURN_NOT_OK_FROM_ARROW(array->Validate());
        PAIMON_ASSIGN_OR_RAISE(bool need_cast, ParquetTimestampConverter::NeedCastArrayForTimestamp(
                                                   array->type(), read_data_type_));
        if (need_cast) {
            PAIMON_ASSIGN_OR_RAISE(array, ParquetTimestampConverter::CastArrayForTimestamp(
                                              array, read_data_type_, arrow_pool_));
        }
        PAIMON_ASSIGN_OR_RAISE(need_cast, ParquetTimestampConverter::NeedCastArrayForTimestamp(
                                              array->type(), read_data_type_));
        if (need_cast) {
            return Status::Invalid(fmt::format(
                "unexpected: in parquet, after CastArrayForTimestamp, output type {} not "
                "equal with read schema {}",
                array->type()->ToString(), read_data_type_->ToString()));
        }
        PAIMON_RETURN_NOT_OK(GenerateRowMapping(array->length()));
        std::unique_ptr<ArrowArray> c_array = std::make_unique<ArrowArray>();
        std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, c_array.get(), c_schema.get()));

        read_rows_ += array->length();
        read_batch_count_++;
        metrics_->SetCounter(ParquetMetrics::READ_ROWS, read_rows_);
        metrics_->SetCounter(ParquetMetrics::READ_BATCH_COUNT, read_batch_count_);

        return make_pair(std::move(c_array), std::move(c_schema));
    }
    PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::NextBatch")
}

Result<std::vector<std::pair<uint64_t, uint64_t>>> ParquetFileBatchReader::GenReadRanges(
    bool* need_prefetch) const {
    try {
        *need_prefetch = true;
        return reader_->GetAllRowGroupRanges();
    }
    PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::GenReadRanges")
}

Result<::parquet::ReaderProperties> ParquetFileBatchReader::CreateReaderProperties(
    const std::shared_ptr<arrow::MemoryPool>& pool,
    const std::map<std::string, std::string>& options) {
    ::parquet::ReaderProperties reader_properties;
    // TODO(jinli.zjw): set more ReaderProperties (compare with java)
    PAIMON_ASSIGN_OR_RAISE(
        bool enable_pre_buffer,
        OptionsUtils::GetValueFromMap<bool>(options, PARQUET_READ_ENABLE_PRE_BUFFER, true));
    if (enable_pre_buffer) {
        reader_properties.enable_buffered_stream();
    } else {
        reader_properties.disable_buffered_stream();
    }
    return reader_properties;
}

Result<::parquet::ArrowReaderProperties> ParquetFileBatchReader::CreateArrowReaderProperties(
    const std::shared_ptr<arrow::MemoryPool>& pool,
    const std::map<std::string, std::string>& options, int32_t batch_size) {
    PAIMON_ASSIGN_OR_RAISE(
        uint32_t executor_thread_count,
        OptionsUtils::GetValueFromMap<uint32_t>(options, PARQUET_READ_EXECUTOR_THREAD_COUNT,
                                                DEFAULT_PARQUET_READ_EXECUTOR_THREAD_COUNT));

    ::parquet::ArrowReaderProperties arrow_reader_props;
    // TODO(jinli.zjw): set more ArrowReaderProperties (compare with java)
    PAIMON_ASSIGN_OR_RAISE(
        bool enable_pre_buffer,
        OptionsUtils::GetValueFromMap<bool>(options, PARQUET_READ_ENABLE_PRE_BUFFER, true));
    arrow_reader_props.set_pre_buffer(enable_pre_buffer);
    arrow_reader_props.set_batch_size(static_cast<int64_t>(batch_size));
    if (executor_thread_count != 0) {
        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::SetCpuThreadPoolCapacity(executor_thread_count));
        arrow_reader_props.set_use_threads(true);
    } else {
        arrow_reader_props.set_use_threads(false);
    }
    PAIMON_ASSIGN_OR_RAISE(bool cache_lazy, OptionsUtils::GetValueFromMap<bool>(
                                                options, PARQUET_READ_CACHE_OPTION_LAZY, false));
    PAIMON_ASSIGN_OR_RAISE(
        int64_t cache_prefetch_limit,
        OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_PREFETCH_LIMIT,
                                               DEFAULT_PARQUET_READ_CACHE_OPTION_PREFETCH_LIMIT));
    PAIMON_ASSIGN_OR_RAISE(
        int64_t cache_hole_size_limit,
        OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
                                               DEFAULT_PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT));
    PAIMON_ASSIGN_OR_RAISE(
        int64_t cache_range_size_limit,
        OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT,
                                               DEFAULT_PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT));
    if (cache_hole_size_limit < 0) {
        return Status::Invalid(fmt::format("{} must be non-negative, but was {}",
                                           PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
                                           cache_hole_size_limit));
    }
    if (cache_range_size_limit <= cache_hole_size_limit) {
        return Status::Invalid(fmt::format("{} must be greater than {}, but was {} <= {}",
                                           PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT,
                                           PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
                                           cache_range_size_limit, cache_hole_size_limit));
    }
    auto cache_option = arrow::io::CacheOptions::Defaults();
    cache_option.lazy = cache_lazy;
    cache_option.prefetch_limit = cache_prefetch_limit;
    cache_option.hole_size_limit = cache_hole_size_limit;
    cache_option.range_size_limit = cache_range_size_limit;
    arrow_reader_props.set_cache_options(cache_option);
    return arrow_reader_props;
}

// Nested column index computation

Status ParquetFileBatchReader::CollectLeafIndices(const std::shared_ptr<arrow::DataType>& read_type,
                                                  const std::shared_ptr<arrow::DataType>& file_type,
                                                  int32_t* leaf_index,
                                                  std::vector<int32_t>* indices) {
    if (file_type->id() == arrow::Type::STRUCT) {
        for (const auto& file_child : file_type->fields()) {
            std::shared_ptr<arrow::Field> read_child = nullptr;
            for (const auto& candidate : read_type->fields()) {
                if (candidate->name() == file_child->name()) {
                    read_child = candidate;
                    break;
                }
            }
            if (read_child) {
                PAIMON_RETURN_NOT_OK(CollectLeafIndices(read_child->type(), file_child->type(),
                                                        leaf_index, indices));
            } else {
                SkipLeafIndices(file_child->type(), leaf_index);
            }
        }
    } else if (file_type->id() == arrow::Type::LIST) {
        // Keep behavior aligned with ORC path: list/map inner partial projection
        // is currently unsupported and should fail-fast.
        if (!HasSameNestedProjectionShape(read_type, file_type)) {
            return Status::Invalid(fmt::format(
                "Parquet does not support partial projection inside list/map: src {} vs target {}",
                file_type->ToString(), read_type->ToString()));
        }
        const auto& read_list = static_cast<const arrow::ListType&>(*read_type);
        const auto& file_list = static_cast<const arrow::ListType&>(*file_type);
        PAIMON_RETURN_NOT_OK(CollectLeafIndices(read_list.value_type(), file_list.value_type(),
                                                leaf_index, indices));
    } else if (file_type->id() == arrow::Type::MAP) {
        if (!HasSameNestedProjectionShape(read_type, file_type)) {
            return Status::Invalid(fmt::format(
                "Parquet does not support partial projection inside list/map: src {} vs target {}",
                file_type->ToString(), read_type->ToString()));
        }
        const auto& read_map = static_cast<const arrow::MapType&>(*read_type);
        const auto& file_map = static_cast<const arrow::MapType&>(*file_type);
        PAIMON_RETURN_NOT_OK(
            CollectLeafIndices(read_map.key_type(), file_map.key_type(), leaf_index, indices));
        PAIMON_RETURN_NOT_OK(
            CollectLeafIndices(read_map.item_type(), file_map.item_type(), leaf_index, indices));
    } else {
        // Leaf column — collect its index.
        indices->push_back((*leaf_index)++);
    }
    return Status::OK();
}

void ParquetFileBatchReader::SkipLeafIndices(const std::shared_ptr<arrow::DataType>& file_type,
                                             int32_t* leaf_index) {
    if (file_type->id() == arrow::Type::STRUCT || file_type->id() == arrow::Type::LIST ||
        file_type->id() == arrow::Type::MAP) {
        for (int32_t i = 0; i < file_type->num_fields(); i++) {
            SkipLeafIndices(file_type->field(i)->type(), leaf_index);
        }
    } else {
        (*leaf_index)++;
    }
}

Result<std::vector<int32_t>> ParquetFileBatchReader::ComputeNestedColumnIndices(
    const std::shared_ptr<arrow::Schema>& read_schema,
    const std::shared_ptr<arrow::Schema>& file_schema) {
    std::vector<int32_t> indices;
    std::vector<int32_t> file_field_leaf_starts;
    file_field_leaf_starts.reserve(file_schema->num_fields());

    int32_t file_leaf_index = 0;
    for (const auto& file_field : file_schema->fields()) {
        file_field_leaf_starts.push_back(file_leaf_index);
        SkipLeafIndices(file_field->type(), &file_leaf_index);
    }

    const auto& file_fields = file_schema->fields();
    for (const auto& read_field : read_schema->fields()) {
        int32_t file_field_idx = -1;
        for (int32_t i = 0; i < static_cast<int32_t>(file_fields.size()); ++i) {
            if (file_fields[i]->name() == read_field->name()) {
                file_field_idx = i;
                break;
            }
        }
        if (file_field_idx < 0) {
            return Status::Invalid(
                fmt::format("Field '{}' in read schema does not exist in parquet file schema",
                            read_field->name()));
        }
        int32_t leaf_index = file_field_leaf_starts[file_field_idx];
        PAIMON_RETURN_NOT_OK(CollectLeafIndices(
            read_field->type(), file_fields[file_field_idx]->type(), &leaf_index, &indices));
    }
    return indices;
}

Status ParquetFileBatchReader::UpdateAllTargetRowRanges(
    const std::vector<TargetRowGroup>& target_row_groups) {
    row_mapping_.clear();
    auto all_row_group_ranges = reader_->GetAllRowGroupRanges();
    RowRanges all_ranges;
    for (const auto& target_row_group : target_row_groups) {
        auto row_group_idx = target_row_group.GetRowGroupIndex();
        for (const auto& range : target_row_group.GetRowRanges().GetRanges()) {
            all_ranges.Add(Range(all_row_group_ranges[row_group_idx].first + range.from,
                                 all_row_group_ranges[row_group_idx].first + range.to));
        }
    }
    all_row_ranges_ = std::move(all_ranges);
    return Status::OK();
}

Status ParquetFileBatchReader::GenerateRowMapping(int64_t batch_length) {
    const std::vector<Range>& all_ranges = all_row_ranges_.GetRanges();
    PAIMON_ASSIGN_OR_RAISE(int64_t batch_start_row, reader_->GetPreviousBatchFirstRowNumber());

    auto cur_range_it =
        std::upper_bound(all_ranges.begin(), all_ranges.end(), batch_start_row,
                         [](int64_t value, const Range& r) { return value < r.from; });
    if (cur_range_it == all_ranges.begin()) {
        return Status::Invalid("No range found!");
    }
    --cur_range_it;
    if (batch_start_row < cur_range_it->from || batch_start_row > cur_range_it->to) {
        return Status::Invalid(
            fmt::format("Batch start row {} is not in the current range [{}, {}]!", batch_start_row,
                        cur_range_it->from, cur_range_it->to));
    }

    std::vector<uint64_t> row_mapping;
    row_mapping.reserve(batch_length);
    int64_t global_row = batch_start_row;
    for (int64_t i = 0; i < batch_length; ++i) {
        if (global_row > cur_range_it->to) {
            ++cur_range_it;
            if (cur_range_it == all_ranges.end()) {
                return Status::Invalid("Batch length exceeds the total row ranges!");
            }
            global_row = cur_range_it->from;
        }
        row_mapping.push_back(global_row);
        global_row++;
    }
    row_mapping_ = std::move(row_mapping);
    return Status::OK();
}
}  // namespace paimon::parquet
