/*
 * 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/variant/variant_shredding_write_plan.h"

#include <utility>

#include "arrow/api.h"
#include "arrow/util/checked_cast.h"
#include "fmt/format.h"
#include "paimon/common/data/variant/variant_shredding_utils.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/types/data_type_json_parser.h"
#include "rapidjson/document.h"

namespace paimon {

namespace {

/// Recursively rebuilds `field`, replacing the variant fields planned under `paths` (grouped by
/// their leading index at this level) with their shredded physical types.
Result<std::shared_ptr<arrow::Field>> ReplacePlannedFields(
    const std::shared_ptr<arrow::Field>& field,
    const std::map<std::vector<int32_t>, std::shared_ptr<arrow::DataType>>& paths, size_t depth,
    std::vector<VariantShreddingWritePlan::PlannedColumn>* columns) {
    const auto& struct_type =
        arrow::internal::checked_cast<const arrow::StructType&>(*field->type());
    arrow::FieldVector new_fields = struct_type.fields();
    bool changed = false;
    auto it = paths.begin();
    while (it != paths.end()) {
        int32_t index = it->first[depth];
        // Collect the consecutive paths that descend into the same child.
        std::map<std::vector<int32_t>, std::shared_ptr<arrow::DataType>> child_paths;
        for (; it != paths.end() && it->first[depth] == index; ++it) {
            child_paths.emplace(it->first, it->second);
        }
        if (index < 0 || index >= struct_type.num_fields()) {
            return Status::Invalid(
                fmt::format("variant shredding path index {} is out of bounds", index));
        }
        const std::shared_ptr<arrow::Field>& child = struct_type.field(index);
        auto terminal = child_paths.begin();
        if (terminal->first.size() == depth + 1) {
            // The path terminates at this child: it must be a variant field.
            if (child_paths.size() > 1 || !VariantTypeUtils::IsVariantField(child)) {
                continue;
            }
            PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::DataType> physical_type,
                                   VariantShreddingUtils::VariantShreddingSchema(terminal->second));
            PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<VariantSchema> variant_schema,
                                   VariantShreddingUtils::BuildVariantSchema(physical_type));
            columns->push_back(VariantShreddingWritePlan::PlannedColumn{
                terminal->first, std::move(variant_schema), physical_type});
            new_fields[index] = child->WithType(physical_type);
            changed = true;
        } else {
            // The paths descend into a nested struct child.
            if (child->type()->id() != arrow::Type::STRUCT ||
                VariantTypeUtils::IsVariantField(child)) {
                continue;
            }
            size_t planned_before = columns->size();
            PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> new_child,
                                   ReplacePlannedFields(child, child_paths, depth + 1, columns));
            if (columns->size() > planned_before) {
                new_fields[index] = new_child;
                changed = true;
            }
        }
    }
    if (!changed) {
        return field;
    }
    return field->WithType(arrow::struct_(new_fields));
}

Status CollectPlannedColumns(const std::shared_ptr<arrow::Field>& logical_field,
                             const std::shared_ptr<arrow::Field>& physical_field,
                             std::vector<int32_t>* path,
                             std::vector<VariantShreddingWritePlan::PlannedColumn>* columns) {
    if (logical_field->name() != physical_field->name()) {
        return Status::Invalid(
            fmt::format("variant shredding physical field '{}' does not match logical field '{}'",
                        physical_field->name(), logical_field->name()));
    }
    if (VariantTypeUtils::IsVariantField(logical_field)) {
        if (logical_field->type()->Equals(*physical_field->type())) {
            return Status::OK();
        }
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<VariantSchema> variant_schema,
                               VariantShreddingUtils::BuildVariantSchema(physical_field->type()));
        columns->push_back(VariantShreddingWritePlan::PlannedColumn{
            *path, std::move(variant_schema), physical_field->type()});
        return Status::OK();
    }
    if (logical_field->type()->Equals(*physical_field->type())) {
        return Status::OK();
    }
    if (logical_field->type()->id() != arrow::Type::STRUCT ||
        physical_field->type()->id() != arrow::Type::STRUCT) {
        return Status::Invalid(fmt::format(
            "variant shredding physical type of field '{}' differs outside a Variant column",
            logical_field->name()));
    }
    const auto& logical_type =
        arrow::internal::checked_cast<const arrow::StructType&>(*logical_field->type());
    const auto& physical_type =
        arrow::internal::checked_cast<const arrow::StructType&>(*physical_field->type());
    if (logical_type.num_fields() != physical_type.num_fields()) {
        return Status::Invalid(
            fmt::format("variant shredding physical struct '{}' has a different field count",
                        logical_field->name()));
    }
    for (int32_t i = 0; i < logical_type.num_fields(); ++i) {
        path->push_back(i);
        PAIMON_RETURN_NOT_OK(
            CollectPlannedColumns(logical_type.field(i), physical_type.field(i), path, columns));
        path->pop_back();
    }
    return Status::OK();
}

}  // namespace

Result<std::shared_ptr<VariantShreddingWritePlan>> VariantShreddingWritePlan::Create(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::map<std::string, std::shared_ptr<arrow::DataType>>& column_shredding_types) {
    std::map<std::vector<int32_t>, std::shared_ptr<arrow::DataType>> path_shredding_types;
    for (int32_t i = 0; i < logical_schema->num_fields(); ++i) {
        auto it = column_shredding_types.find(logical_schema->field(i)->name());
        if (it != column_shredding_types.end()) {
            path_shredding_types.emplace(std::vector<int32_t>{i}, it->second);
        }
    }
    return CreateFromPaths(logical_schema, path_shredding_types);
}

Result<std::shared_ptr<VariantShreddingWritePlan>> VariantShreddingWritePlan::CreateFromPaths(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::map<std::vector<int32_t>, std::shared_ptr<arrow::DataType>>& path_shredding_types) {
    std::vector<PlannedColumn> columns;
    auto root_field = arrow::field("root", arrow::struct_(logical_schema->fields()));
    PAIMON_ASSIGN_OR_RAISE(
        std::shared_ptr<arrow::Field> new_root,
        ReplacePlannedFields(root_field, path_shredding_types, /*depth=*/0, &columns));
    auto physical_schema = arrow::schema(new_root->type()->fields(), logical_schema->metadata());
    return std::shared_ptr<VariantShreddingWritePlan>(new VariantShreddingWritePlan(
        logical_schema, std::move(physical_schema), std::move(columns)));
}

Result<std::shared_ptr<VariantShreddingWritePlan>>
VariantShreddingWritePlan::CreateFromPhysicalSchema(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::shared_ptr<arrow::Schema>& physical_schema) {
    if (logical_schema->num_fields() != physical_schema->num_fields()) {
        return Status::Invalid(
            "variant shredding logical and physical schemas have different field counts");
    }
    std::vector<PlannedColumn> columns;
    std::vector<int32_t> path;
    for (int32_t i = 0; i < logical_schema->num_fields(); ++i) {
        path.push_back(i);
        PAIMON_RETURN_NOT_OK(CollectPlannedColumns(logical_schema->field(i),
                                                   physical_schema->field(i), &path, &columns));
        path.pop_back();
    }
    return std::shared_ptr<VariantShreddingWritePlan>(
        new VariantShreddingWritePlan(logical_schema, physical_schema, std::move(columns)));
}

Result<std::shared_ptr<VariantShreddingWritePlan>> VariantShreddingWritePlan::FromConfiguredSchema(
    const std::shared_ptr<arrow::Schema>& logical_schema,
    const std::string& configured_schema_json) {
    rapidjson::Document doc;
    doc.Parse(configured_schema_json.c_str());
    if (doc.HasParseError()) {
        return Status::Invalid(fmt::format("failed to parse variant shredding schema json: {}",
                                           configured_schema_json));
    }
    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> configured_field,
                           DataTypeJsonParser::ParseType("shredding_schema", doc));
    if (configured_field->type()->id() != arrow::Type::STRUCT) {
        return Status::Invalid("variant shredding schema must be a ROW type");
    }
    std::map<std::string, std::shared_ptr<arrow::DataType>> column_shredding_types;
    for (const auto& column_field : configured_field->type()->fields()) {
        column_shredding_types.emplace(column_field->name(), column_field->type());
    }
    return Create(logical_schema, column_shredding_types);
}

}  // namespace paimon
