/*
 * 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/common/data/shredding/map_shared_shredding_batch_converter.h"

#include <map>
#include <string>
#include <utility>

#include "arrow/array.h"
#include "arrow/builder.h"
#include "arrow/c/bridge.h"
#include "arrow/type.h"
#include "fmt/format.h"
#include "paimon/common/data/shredding/lru_map_shared_shredding_column_allocator.h"
#include "paimon/common/data/shredding/map_shared_shredding_context.h"
#include "paimon/common/data/shredding/map_shared_shredding_utils.h"
#include "paimon/common/data/shredding/map_shredding_defs.h"
#include "paimon/common/data/shredding/plain_map_shared_shredding_column_allocator.h"
#include "paimon/common/data/shredding/sequential_map_shared_shredding_column_allocator.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/core/core_options.h"
namespace paimon {
/// Checks that a dynamic_cast result is not null, returning Status::Invalid on failure.
#define PAIMON_CHECK_NOT_NULL(ptr, msg)          \
    do {                                         \
        if (PAIMON_UNLIKELY((ptr) == nullptr)) { \
            return Status::Invalid(msg);         \
        }                                        \
    } while (false)

namespace {

Result<std::unique_ptr<MapSharedShreddingColumnAllocator>> CreateMapSharedShreddingColumnAllocator(
    int32_t num_columns, MapSharedShreddingColumnPlacementPolicy placement_policy) {
    switch (placement_policy) {
        case MapSharedShreddingColumnPlacementPolicy::PLAIN:
            return std::make_unique<PlainMapSharedShreddingColumnAllocator>(num_columns);
        case MapSharedShreddingColumnPlacementPolicy::SEQUENTIAL:
            return std::make_unique<SequentialMapSharedShreddingColumnAllocator>(num_columns);
        case MapSharedShreddingColumnPlacementPolicy::LRU:
            return std::make_unique<LruMapSharedShreddingColumnAllocator>(num_columns);
    }
    return Status::Invalid("unknown shared-shredding column placement policy");
}

}  // namespace

Result<std::shared_ptr<MapSharedShreddingBatchConverter>> MapSharedShreddingBatchConverter::Create(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::shared_ptr<MapSharedShreddingContext>& context, const CoreOptions& options,
    const std::shared_ptr<MemoryPool>& pool) {
    std::map<std::string, int32_t> field_to_num_columns = context->ComputeNextK();
    PAIMON_ASSIGN_OR_RAISE(
        std::shared_ptr<arrow::Schema> physical_schema,
        MapSharedShreddingUtils::LogicalToPhysicalSchema(logical_schema, field_to_num_columns));
    std::vector<ColumnContext> contexts;
    std::vector<std::string> shredding_field_names;
    contexts.reserve(field_to_num_columns.size());
    shredding_field_names.reserve(field_to_num_columns.size());
    // Iterate in schema field order (not map order) so that shredding_field_names_
    // matches the order in which shredding columns appear in the schema.
    // This is critical for the sequential matching logic in Convert().
    for (int32_t i = 0; i < logical_schema->num_fields(); ++i) {
        const std::string& name = logical_schema->field(i)->name();
        auto it = field_to_num_columns.find(name);
        if (it != field_to_num_columns.end()) {
            int32_t num_columns = it->second;
            PAIMON_ASSIGN_OR_RAISE(MapSharedShreddingColumnPlacementPolicy placement_policy,
                                   options.GetMapSharedShreddingColumnPlacementPolicy(name));
            PAIMON_ASSIGN_OR_RAISE(
                std::unique_ptr<MapSharedShreddingColumnAllocator> allocator,
                CreateMapSharedShreddingColumnAllocator(num_columns, placement_policy));
            contexts.emplace_back(name, num_columns, std::move(allocator));
            shredding_field_names.push_back(name);
        }
    }
    return std::shared_ptr<MapSharedShreddingBatchConverter>(
        new MapSharedShreddingBatchConverter(logical_schema, physical_schema, std::move(contexts),
                                             std::move(shredding_field_names), pool));
}

MapSharedShreddingBatchConverter::MapSharedShreddingBatchConverter(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::shared_ptr<arrow::Schema>& physical_schema, std::vector<ColumnContext>&& contexts,
    std::vector<std::string>&& shredding_field_names, const std::shared_ptr<MemoryPool>& pool)
    : logical_schema_(logical_schema),
      physical_schema_(physical_schema),
      contexts_(std::move(contexts)),
      shredding_field_names_(std::move(shredding_field_names)),
      pool_(GetArrowPool(pool)) {}

const std::shared_ptr<arrow::Schema>& MapSharedShreddingBatchConverter::GetPhysicalSchema() const {
    return physical_schema_;
}

Result<std::unique_ptr<ArrowArray>> MapSharedShreddingBatchConverter::Convert(
    ArrowArray* logical_batch) {
    std::shared_ptr<arrow::DataType> logical_type = arrow::struct_(logical_schema_->fields());
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> logical_array,
                                      arrow::ImportArray(logical_batch, logical_type));
    auto logical_struct = std::dynamic_pointer_cast<arrow::StructArray>(logical_array);
    PAIMON_CHECK_NOT_NULL(logical_struct,
                          "MapSharedShreddingBatchConverter: input is not a StructArray");

    int32_t num_fields = logical_schema_->num_fields();
    arrow::ArrayVector physical_columns;
    physical_columns.reserve(num_fields);
    size_t context_idx = 0;
    for (int32_t col = 0; col < num_fields; ++col) {
        auto column = logical_struct->field(col);
        const std::string& field_name = logical_schema_->field(col)->name();
        if (context_idx < shredding_field_names_.size() &&
            shredding_field_names_[context_idx] == field_name) {
            auto physical_struct_type = physical_schema_->field(col)->type();
            PAIMON_ASSIGN_OR_RAISE(
                std::shared_ptr<arrow::Array> physical_column,
                ConvertOneColumn(column, physical_struct_type, &contexts_[context_idx]));
            physical_columns.push_back(std::move(physical_column));
            ++context_idx;
        } else {
            physical_columns.push_back(column);
        }
    }

    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
        std::shared_ptr<arrow::Array> physical_struct,
        arrow::StructArray::Make(physical_columns, physical_schema_->field_names()));

    std::unique_ptr<ArrowArray> result = std::make_unique<ArrowArray>();
    PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*physical_struct, result.get()));
    return result;
}

Result<std::shared_ptr<arrow::Array>> MapSharedShreddingBatchConverter::ConvertOneColumn(
    const std::shared_ptr<arrow::Array>& map_column,
    const std::shared_ptr<arrow::DataType>& physical_struct_type, ColumnContext* context) const {
    auto map_array = std::dynamic_pointer_cast<arrow::MapArray>(map_column);
    PAIMON_CHECK_NOT_NULL(map_array, "MapSharedShreddingBatchConverter: column is not a MapArray");

    int64_t num_rows = map_array->length();
    int32_t num_cols = context->num_columns;

    auto keys_array = std::dynamic_pointer_cast<arrow::StringArray>(map_array->keys());
    PAIMON_CHECK_NOT_NULL(keys_array,
                          "MapSharedShreddingBatchConverter: MAP keys are not StringArray");
    auto values_array = map_array->items();

    // Create StructBuilder from physical struct type — it owns all child builders.
    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> struct_builder_base,
                                      arrow::MakeBuilder(physical_struct_type, pool_.get()));
    auto* struct_builder = dynamic_cast<arrow::StructBuilder*>(struct_builder_base.get());
    PAIMON_CHECK_NOT_NULL(struct_builder,
                          "MapSharedShreddingBatchConverter: failed to create StructBuilder");
    PAIMON_RETURN_NOT_OK_FROM_ARROW(struct_builder->Reserve(num_rows));

    // Extract child builders: [field_mapping, col_0..K-1, overflow]
    auto* field_mapping_builder =
        dynamic_cast<arrow::ListBuilder*>(struct_builder->field_builder(0));
    PAIMON_CHECK_NOT_NULL(field_mapping_builder,
                          "MapSharedShreddingBatchConverter: field_mapping is not a ListBuilder");
    auto* field_mapping_value_builder =
        dynamic_cast<arrow::Int32Builder*>(field_mapping_builder->value_builder());
    PAIMON_CHECK_NOT_NULL(
        field_mapping_value_builder,
        "MapSharedShreddingBatchConverter: field_mapping value is not Int32Builder");
    PAIMON_RETURN_NOT_OK_FROM_ARROW(field_mapping_builder->Reserve(num_rows));
    PAIMON_RETURN_NOT_OK_FROM_ARROW(field_mapping_value_builder->Reserve(num_rows * num_cols));

    std::vector<arrow::ArrayBuilder*> col_builders_raw;
    col_builders_raw.reserve(num_cols);
    for (int32_t c = 0; c < num_cols; ++c) {
        arrow::ArrayBuilder* col_builder = struct_builder->field_builder(1 + c);
        PAIMON_CHECK_NOT_NULL(col_builder, "MapSharedShreddingBatchConverter: col builder is null");
        PAIMON_RETURN_NOT_OK_FROM_ARROW(col_builder->Reserve(num_rows));
        col_builders_raw.push_back(col_builder);
    }

    int32_t overflow_field_idx = 1 + num_cols;
    auto* overflow_builder =
        dynamic_cast<arrow::MapBuilder*>(struct_builder->field_builder(overflow_field_idx));
    PAIMON_CHECK_NOT_NULL(overflow_builder,
                          "MapSharedShreddingBatchConverter: overflow is not a MapBuilder");
    auto* overflow_key_builder =
        dynamic_cast<arrow::Int32Builder*>(overflow_builder->key_builder());
    PAIMON_CHECK_NOT_NULL(overflow_key_builder,
                          "MapSharedShreddingBatchConverter: overflow key is not Int32Builder");
    arrow::ArrayBuilder* overflow_value_builder = overflow_builder->item_builder();
    PAIMON_CHECK_NOT_NULL(overflow_value_builder,
                          "MapSharedShreddingBatchConverter: overflow value builder is null");
    PAIMON_RETURN_NOT_OK_FROM_ARROW(overflow_builder->Reserve(num_rows));

    // Process each row
    for (int64_t row = 0; row < num_rows; ++row) {
        if (map_array->IsNull(row)) {
            // StructBuilder::AppendNull() auto-appends empty values to all children.
            PAIMON_RETURN_NOT_OK_FROM_ARROW(struct_builder->AppendNull());
            continue;
        }

        PAIMON_RETURN_NOT_OK_FROM_ARROW(struct_builder->Append());

        int64_t start = map_array->value_offset(row);
        int64_t length = map_array->value_length(row);

        // Extract field ids and build lookup map
        std::vector<int32_t> field_ids;
        std::unordered_map<int32_t, int64_t> field_id_to_value_index;
        ExtractRowFields(keys_array, start, length, &context->dict, &field_ids,
                         &field_id_to_value_index);

        // Allocate columns
        RowAllocation allocation = context->allocator->AllocateRow(field_ids);

        // Fill sub-columns
        PAIMON_RETURN_NOT_OK(AppendFieldMapping(allocation, num_cols, field_mapping_builder,
                                                field_mapping_value_builder));
        PAIMON_RETURN_NOT_OK(AppendColumnValues(values_array, allocation, field_id_to_value_index,
                                                num_cols, col_builders_raw));
        PAIMON_RETURN_NOT_OK(AppendOverflow(values_array, allocation, field_id_to_value_index,
                                            overflow_builder, overflow_key_builder,
                                            overflow_value_builder));
    }

    // Finalize
    std::shared_ptr<arrow::StructArray> result;
    PAIMON_RETURN_NOT_OK_FROM_ARROW(struct_builder->Finish(&result));
    return result;
}

void MapSharedShreddingBatchConverter::ExtractRowFields(
    const std::shared_ptr<arrow::StringArray>& keys_array, int64_t start, int64_t length,
    MapSharedShreddingFieldDict* dict, std::vector<int32_t>* field_ids_out,
    std::unordered_map<int32_t, int64_t>* field_id_to_value_index_out) const {
    field_ids_out->clear();
    field_ids_out->reserve(length);
    field_id_to_value_index_out->clear();
    field_id_to_value_index_out->reserve(length);
    for (int64_t j = 0; j < length; ++j) {
        std::string key_str = keys_array->GetString(start + j);
        int32_t field_id = dict->GetOrAssign(key_str);
        field_ids_out->push_back(field_id);
        (*field_id_to_value_index_out)[field_id] = start + j;
    }
}

Status MapSharedShreddingBatchConverter::AppendFieldMapping(
    const RowAllocation& allocation, int32_t num_cols, arrow::ListBuilder* list_builder,
    arrow::Int32Builder* value_builder) const {
    PAIMON_RETURN_NOT_OK_FROM_ARROW(list_builder->Append());
    for (int32_t c = 0; c < num_cols; ++c) {
        PAIMON_RETURN_NOT_OK_FROM_ARROW(value_builder->Append(allocation.col_to_field[c]));
    }
    return Status::OK();
}

Status MapSharedShreddingBatchConverter::AppendColumnValues(
    const std::shared_ptr<arrow::Array>& values_array, const RowAllocation& allocation,
    const std::unordered_map<int32_t, int64_t>& field_id_to_value_index, int32_t num_cols,
    const std::vector<arrow::ArrayBuilder*>& col_builders) const {
    for (int32_t c = 0; c < num_cols; ++c) {
        int32_t assigned_field_id = allocation.col_to_field[c];
        if (assigned_field_id == -1) {
            PAIMON_RETURN_NOT_OK_FROM_ARROW(col_builders[c]->AppendNull());
        } else {
            auto it = field_id_to_value_index.find(assigned_field_id);
            if (PAIMON_UNLIKELY(it == field_id_to_value_index.end())) {
                return Status::Invalid(
                    fmt::format("MapSharedShreddingBatchConverter: field_id {} assigned to col {} "
                                "but not found in current row",
                                assigned_field_id, c));
            }
            PAIMON_RETURN_NOT_OK_FROM_ARROW(
                col_builders[c]->AppendArraySlice(*values_array->data(), it->second, 1));
        }
    }
    return Status::OK();
}

Status MapSharedShreddingBatchConverter::AppendOverflow(
    const std::shared_ptr<arrow::Array>& values_array, const RowAllocation& allocation,
    const std::unordered_map<int32_t, int64_t>& field_id_to_value_index,
    arrow::MapBuilder* overflow_builder, arrow::Int32Builder* overflow_key_builder,
    arrow::ArrayBuilder* overflow_value_builder) const {
    if (allocation.overflow_fields.empty()) {
        PAIMON_RETURN_NOT_OK_FROM_ARROW(overflow_builder->AppendNull());
        return Status::OK();
    }

    PAIMON_RETURN_NOT_OK_FROM_ARROW(overflow_builder->Append());
    for (int32_t overflow_field_id : allocation.overflow_fields) {
        auto it = field_id_to_value_index.find(overflow_field_id);
        if (PAIMON_UNLIKELY(it == field_id_to_value_index.end())) {
            return Status::Invalid(fmt::format(
                "MapSharedShreddingBatchConverter: overflow field_id {} not found in current row",
                overflow_field_id));
        }
        PAIMON_RETURN_NOT_OK_FROM_ARROW(overflow_key_builder->Append(overflow_field_id));
        PAIMON_RETURN_NOT_OK_FROM_ARROW(
            overflow_value_builder->AppendArraySlice(*values_array->data(), it->second, 1));
    }
    return Status::OK();
}

Result<MapSharedShreddingFieldMeta> MapSharedShreddingBatchConverter::BuildFieldMeta(
    const std::string& field_name) const {
    for (const auto& context : contexts_) {
        if (context.field_name == field_name) {
            MapSharedShreddingFieldMeta meta;
            meta.name_to_id = context.dict.GetNameToId();
            // Convert set<int32_t> -> vector<int32_t> for field_to_columns
            for (const auto& [field_id, col_set] : context.allocator->GetFieldToColumns()) {
                meta.field_to_columns[field_id] =
                    std::vector<int32_t>(col_set.begin(), col_set.end());
            }
            meta.overflow_field_set = context.allocator->GetOverflowFieldSet();
            meta.num_columns = context.num_columns;
            meta.max_row_width = context.allocator->GetMaxRowWidth();
            return meta;
        }
    }
    return Status::Invalid(fmt::format(
        "cannot find field_name '{}' in MapSharedShreddingBatchConverter contexts", field_name));
}

Result<int32_t> MapSharedShreddingBatchConverter::GetMaxRowWidth(
    const std::string& field_name) const {
    for (const auto& context : contexts_) {
        if (context.field_name == field_name) {
            return context.allocator->GetMaxRowWidth();
        }
    }
    return Status::Invalid(fmt::format(
        "cannot find field_name '{}' in MapSharedShreddingBatchConverter contexts", field_name));
}

const std::vector<std::string>& MapSharedShreddingBatchConverter::GetShreddingColumnNames() const {
    return shredding_field_names_;
}

}  // namespace paimon
