/*
 * 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/core/schema/table_schema.h"

#include <algorithm>
#include <iterator>
#include <set>
#include <utility>

#include "arrow/api.h"
#include "arrow/c/bridge.h"
#include "arrow/util/checked_cast.h"
#include "fmt/format.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/common/utils/field_type_utils.h"
#include "paimon/common/utils/object_utils.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/common/utils/rapidjson_util.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/core/schema/arrow_schema_validator.h"
#include "paimon/defs.h"
#include "paimon/status.h"
#include "rapidjson/allocators.h"
#include "rapidjson/document.h"
#include "rapidjson/rapidjson.h"

namespace paimon {

Result<std::unique_ptr<TableSchema>> TableSchema::Create(
    int64_t schema_id, const std::shared_ptr<arrow::Schema>& schema,
    const std::vector<std::string>& partition_keys, const std::vector<std::string>& primary_keys,
    const std::map<std::string, std::string>& options) {
    std::vector<DataField> data_fields;
    int32_t field_id = 0;
    std::set<std::string> primary_key_set;
    for (const auto& primary_key : primary_keys) {
        primary_key_set.insert(primary_key);
    }
    for (const auto& field : schema->fields()) {
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> field_with_id,
                               AssignFieldIdsRecursively(field, /*set_field_id=*/true, &field_id));
        if (primary_key_set.count(field_with_id->name())) {
            field_with_id = field_with_id->WithNullable(false);
        }
        PAIMON_ASSIGN_OR_RAISE(DataField data_field,
                               DataField::ConvertArrowFieldToDataField(field_with_id));
        data_fields.push_back(data_field);
    }
    return InitSchema(schema_id, data_fields, field_id - 1, partition_keys, primary_keys, options,
                      /*comment=*/std::nullopt, DateTimeUtils::GetCurrentUTCTimeUs() / 1000);
}

Result<std::shared_ptr<arrow::KeyValueMetadata>> TableSchema::MakeMetaDataWithFieldId(
    const std::shared_ptr<arrow::Field>& field, int32_t field_id) {
    std::vector<std::string> keys = {std::string(DataField::FIELD_ID)};
    std::vector<std::string> values = {std::to_string(field_id)};
    std::shared_ptr<arrow::KeyValueMetadata> metadata = arrow::KeyValueMetadata::Make(keys, values);
    if (field->HasMetadata() && field->metadata()) {
        if (field->metadata()->Contains(DataField::FIELD_ID)) {
            PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::string field_id_result,
                                              field->metadata()->Get(DataField::FIELD_ID));
            if (std::to_string(field_id) != field_id_result) {
                return Status::Invalid(fmt::format("field id {} not match with {} {} in metadata",
                                                   field_id, DataField::FIELD_ID, field_id_result));
            }
        }
        metadata = metadata->Merge(*field->metadata());
    }
    return metadata;
}

Result<std::shared_ptr<arrow::Field>> TableSchema::AssignFieldIdsRecursively(
    const std::shared_ptr<arrow::Field>& field, bool assign_id_to_self, int32_t* field_id) {
    std::shared_ptr<arrow::KeyValueMetadata> metadata;
    if (assign_id_to_self) {
        PAIMON_ASSIGN_OR_RAISE(metadata, MakeMetaDataWithFieldId(field, *field_id));
        (*field_id)++;
    }
    auto type = field->type();
    if (type->id() == arrow::Type::STRUCT) {
        if (VariantTypeUtils::IsVariantField(field)) {
            // A variant struct is a leaf type: its value/metadata children keep their fixed
            // paimon field ids 0/1 (mapped to parquet field ids on write).
            return metadata ? field->WithMergedMetadata(metadata) : field;
        }
        auto struct_type = arrow::internal::checked_pointer_cast<arrow::StructType>(field->type());
        arrow::FieldVector new_childs;
        for (const auto& child : struct_type->fields()) {
            PAIMON_ASSIGN_OR_RAISE(
                auto new_child, AssignFieldIdsRecursively(child, /*set_field_id=*/true, field_id));
            new_childs.push_back(new_child);
        }
        return arrow::field(field->name(), arrow::struct_(new_childs), field->nullable(), metadata);
    } else if (type->id() == arrow::Type::LIST) {
        auto list_type = arrow::internal::checked_pointer_cast<arrow::ListType>(field->type());
        PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Field> new_value_field,
                               AssignFieldIdsRecursively(list_type->value_field(),
                                                         /*set_field_id=*/false, field_id));
        return arrow::field(field->name(), arrow::list(new_value_field), field->nullable(),
                            metadata);
    } else if (field->type()->id() == arrow::Type::MAP) {
        auto map_type = arrow::internal::checked_pointer_cast<arrow::MapType>(field->type());
        std::shared_ptr<arrow::Field> key_field = map_type->key_field();
        std::shared_ptr<arrow::Field> value_field = map_type->item_field();
        PAIMON_ASSIGN_OR_RAISE(
            key_field, AssignFieldIdsRecursively(key_field, /*set_field_id=*/false, field_id));
        PAIMON_ASSIGN_OR_RAISE(
            value_field, AssignFieldIdsRecursively(value_field, /*set_field_id=*/false, field_id));
        // Paimon MAP does not expose Arrow's keys_sorted property. Normalize it so an
        // in-memory schema and the same schema reloaded from JSON remain equivalent.
        auto new_map_type =
            std::make_shared<arrow::MapType>(key_field, value_field, /*keys_sorted=*/false);
        return arrow::field(field->name(), new_map_type, field->nullable(), metadata);
    }
    return metadata ? field->WithMergedMetadata(metadata) : field;
}

rapidjson::Value TableSchema::ToJson(rapidjson::Document::AllocatorType* allocator) const
    noexcept(false) {
    rapidjson::Value obj(rapidjson::kObjectType);
    obj.AddMember(rapidjson::StringRef("version"),
                  RapidJsonUtil::SerializeValue(version_, allocator).Move(), *allocator);
    obj.AddMember(rapidjson::StringRef("id"), RapidJsonUtil::SerializeValue(id_, allocator).Move(),
                  *allocator);
    obj.AddMember(rapidjson::StringRef("fields"),
                  RapidJsonUtil::SerializeValue(fields_, allocator).Move(), *allocator);
    obj.AddMember(rapidjson::StringRef("highestFieldId"),
                  RapidJsonUtil::SerializeValue(highest_field_id_, allocator).Move(), *allocator);
    obj.AddMember(rapidjson::StringRef("partitionKeys"),
                  RapidJsonUtil::SerializeValue(partition_keys_, allocator).Move(), *allocator);
    obj.AddMember(rapidjson::StringRef("primaryKeys"),
                  RapidJsonUtil::SerializeValue(primary_keys_, allocator).Move(), *allocator);
    obj.AddMember(rapidjson::StringRef("options"),
                  RapidJsonUtil::SerializeValue(options_, allocator).Move(), *allocator);
    if (comment_) {
        obj.AddMember(rapidjson::StringRef("comment"),
                      RapidJsonUtil::SerializeValue(comment_, allocator).Move(), *allocator);
    }
    obj.AddMember(rapidjson::StringRef("timeMillis"),
                  RapidJsonUtil::SerializeValue(time_millis_, allocator).Move(), *allocator);
    return obj;
}

TableSchema::TableSchema(int32_t version, int64_t id, const std::vector<DataField>& fields,
                         int32_t highest_field_id, const std::vector<std::string>& partition_keys,
                         const std::vector<std::string>& primary_keys,
                         const std::map<std::string, std::string>& options,
                         const std::optional<std::string>& comment, int64_t time_millis)
    : version_(version),
      id_(id),
      fields_(fields),
      highest_field_id_(highest_field_id),
      partition_keys_(partition_keys),
      primary_keys_(primary_keys),
      options_(options),
      comment_(comment),
      time_millis_(time_millis) {}

bool TableSchema::operator==(const TableSchema& other) const {
    return version_ == other.version_ && fields_ == other.fields_ &&
           partition_keys_ == other.partition_keys_ && primary_keys_ == other.primary_keys_ &&
           options_ == other.options_ && comment_ == other.comment_ &&
           time_millis_ == other.time_millis_;
}

Result<std::unique_ptr<::ArrowSchema>> TableSchema::GetArrowSchema() const {
    auto schema = DataField::ConvertDataFieldsToArrowSchema(fields_);
    auto c_schema = std::make_unique<::ArrowSchema>();
    PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema, c_schema.get()));
    return c_schema;
}

std::vector<std::string> TableSchema::FieldNames() const {
    std::vector<std::string> field_names;
    field_names.reserve(fields_.size());
    std::transform(fields_.begin(), fields_.end(), std::back_inserter(field_names),
                   [](const DataField& field) { return field.Name(); });
    return field_names;
}

Result<FieldType> TableSchema::GetFieldType(const std::string& field_name) const {
    PAIMON_ASSIGN_OR_RAISE(DataField field, GetField(field_name));
    return FieldTypeUtils::ConvertToFieldType(field.ArrowField());
}

Result<DataField> TableSchema::GetField(const std::string& field_name) const {
    for (const auto& field : Fields()) {
        if (field.Name() == field_name) {
            return field;
        }
    }
    return Status::Invalid(
        fmt::format("Get field {} failed: not exist in table schema", field_name));
}

Result<DataField> TableSchema::GetField(int32_t field_id) const {
    for (const auto& field : Fields()) {
        if (field.Id() == field_id) {
            return field;
        }
    }
    return Status::Invalid(
        fmt::format("Get field with id {} failed: not exist in table schema", field_id));
}

Result<std::vector<DataField>> TableSchema::GetFields(
    const std::vector<std::string>& field_names) const {
    std::vector<DataField> data_fields;
    data_fields.reserve(field_names.size());
    for (const auto& name : field_names) {
        PAIMON_ASSIGN_OR_RAISE(DataField field, GetField(name));
        data_fields.emplace_back(field);
    }
    return data_fields;
}

Result<std::unique_ptr<TableSchema>> TableSchema::CreateFromJson(const std::string& json_str) {
    PAIMON_ASSIGN_OR_RAISE(TableSchema table_schema, TableSchema::FromJsonString(json_str));
    return InitSchema(table_schema.id_, table_schema.fields_, table_schema.highest_field_id_,
                      table_schema.partition_keys_, table_schema.primary_keys_,
                      table_schema.options_, table_schema.comment_, table_schema.time_millis_);
}

Result<std::unique_ptr<TableSchema>> TableSchema::InitSchema(
    int64_t schema_id, const std::vector<DataField>& fields, int32_t highest_field_id,
    const std::vector<std::string>& partition_keys, const std::vector<std::string>& primary_keys,
    const std::map<std::string, std::string>& options, const std::optional<std::string>& comment,
    int64_t time_millis) {
    // validate schema first
    auto arrow_schema = DataField::ConvertDataFieldsToArrowSchema(fields);
    PAIMON_RETURN_NOT_OK(ArrowSchemaValidator::ValidateSchemaWithFieldId(*arrow_schema));

    auto table_schema = std::unique_ptr<TableSchema>(
        new TableSchema(TableSchema::CURRENT_VERSION, schema_id, fields, highest_field_id,
                        partition_keys, primary_keys, options, comment, time_millis));
    PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> keys, table_schema->TrimmedPrimaryKeys());

    // Try to validate bucket keys
    PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> bucket_keys,
                           table_schema->OriginalBucketKeys());
    if (bucket_keys.empty()) {
        bucket_keys = keys;
    }
    table_schema->bucket_keys_ = bucket_keys;
    PAIMON_ASSIGN_OR_RAISE(
        table_schema->num_bucket_,
        OptionsUtils::GetValueFromMap<int32_t>(table_schema->Options(), Options::BUCKET, -1));

    return table_schema;
}

void TableSchema::FromJson(const rapidjson::Value& obj) noexcept(false) {
    version_ = RapidJsonUtil::DeserializeKeyValue<int32_t>(obj, "version", PAIMON_07_VERSION);
    id_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj, "id");
    fields_ = RapidJsonUtil::DeserializeKeyValue<std::vector<DataField>>(obj, "fields");
    highest_field_id_ = RapidJsonUtil::DeserializeKeyValue<int32_t>(obj, "highestFieldId");
    partition_keys_ =
        RapidJsonUtil::DeserializeKeyValue<std::vector<std::string>>(obj, "partitionKeys");
    primary_keys_ =
        RapidJsonUtil::DeserializeKeyValue<std::vector<std::string>>(obj, "primaryKeys");
    options_ =
        RapidJsonUtil::DeserializeKeyValue<std::map<std::string, std::string>>(obj, "options");
    if (version_ <= PAIMON_07_VERSION && options_.find(Options::BUCKET) == options_.end()) {
        // the default value of BUCKET in old version is 1
        options_[Options::BUCKET] = "1";
    }
    if (version_ <= PAIMON_08_VERSION && options_.find(Options::FILE_FORMAT) == options_.end()) {
        // the default value of FILE_FORMAT in old version is orc
        options_[Options::FILE_FORMAT] = "orc";
    }
    comment_ =
        RapidJsonUtil::DeserializeKeyValue<std::optional<std::string>>(obj, "comment", comment_);
    time_millis_ = RapidJsonUtil::DeserializeKeyValue<int64_t>(obj, "timeMillis", 0);
}

Result<std::vector<std::string>> TableSchema::TrimmedPrimaryKeys() const {
    if (primary_keys_.size() > 0) {
        std::vector<std::string> result;
        result.reserve(primary_keys_.size());
        std::set<std::string> partition_keys_set(partition_keys_.begin(), partition_keys_.end());
        for (const auto& pk : primary_keys_) {
            if (partition_keys_set.find(pk) == partition_keys_set.end()) {
                result.emplace_back(pk);
            }
        }
        if (result.size() <= 0) {
            return Status::Invalid(
                fmt::format("Primary key constraint {} should not be same with partition "
                            "fields {}, this will result in only one record in a partition",
                            primary_keys_, partition_keys_));
        }
        return result;
    }
    return primary_keys_;
}

Result<std::vector<DataField>> TableSchema::TrimmedPrimaryKeyFields() const {
    PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> trimmed_primary_key, TrimmedPrimaryKeys());
    return GetFields(trimmed_primary_key);
}

Result<std::shared_ptr<arrow::Schema>> TableSchema::TrimmedPrimaryKeySchema() const {
    PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> pk_fields, TrimmedPrimaryKeyFields());
    return DataField::ConvertDataFieldsToArrowSchema(pk_fields);
}

/// Original bucket keys, maybe empty.
Result<std::vector<std::string>> TableSchema::OriginalBucketKeys() const {
    std::vector<std::string> bucket_keys;
    auto iter = options_.find(Options::BUCKET_KEY);
    if (iter == options_.end()) {
        return bucket_keys;
    }
    const auto& key = iter->second;
    if (StringUtils::IsNullOrWhitespaceOnly(key)) {
        return bucket_keys;
    }
    bucket_keys = StringUtils::Split(key, ",", /*ignore_empty=*/false);
    if (!ObjectUtils::ContainsAll(FieldNames(), bucket_keys)) {
        return Status::Invalid(fmt::format("Field names {} should contain all bucket keys {}.",
                                           FieldNames(), bucket_keys));
    }
    bool any_match =
        std::any_of(bucket_keys.begin(), bucket_keys.end(), [this](const std::string& key) {
            return std::find(partition_keys_.begin(), partition_keys_.end(), key) !=
                   partition_keys_.end();
        });
    if (any_match) {
        return Status::Invalid(fmt::format("Bucket keys {} should not in partition keys {}.",
                                           bucket_keys, partition_keys_));
    }

    if (primary_keys_.size() > 0) {
        if (!ObjectUtils::ContainsAll(primary_keys_, bucket_keys)) {
            return Status::Invalid(fmt::format("Primary keys {} should contain all bucket keys {}.",
                                               primary_keys_, bucket_keys));
        }
    }
    return bucket_keys;
}

bool TableSchema::CrossPartitionUpdate() const {
    if (primary_keys_.empty() || partition_keys_.empty()) {
        return false;
    }

    // If any partition key is not in primary keys, return true
    return !ObjectUtils::ContainsAll(primary_keys_, partition_keys_);
}

}  // namespace paimon
