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

#include <set>
#include <utility>

#include "arrow/api.h"
#include "fmt/format.h"
#include "paimon/common/data/variant/variant_defs.h"
#include "paimon/common/types/data_field.h"

namespace paimon {

namespace {

std::string GetDescription(const std::shared_ptr<arrow::Field>& field) {
    if (field->metadata() == nullptr) {
        return std::string();
    }
    auto result = field->metadata()->Get(DataField::DESCRIPTION);
    if (!result.ok()) {
        return std::string();
    }
    return result.ValueOrDie();
}

// Parses the description body from the right: the last two delimited tokens are `failOnError`
// and `timeZoneId` (neither contains the delimiter), and everything before them is the path,
// which may itself contain the delimiter inside object keys (e.g. `$['a;b']`).
std::vector<std::string> SplitDescription(const std::string& description) {
    std::string body = description.substr(sizeof(VariantAccessUtils::kMetadataKey) - 1);
    size_t tz_sep = body.rfind(VariantAccessUtils::kDelimiter);
    if (tz_sep == std::string::npos) {
        return {body};
    }
    size_t fail_sep =
        tz_sep == 0 ? std::string::npos : body.rfind(VariantAccessUtils::kDelimiter, tz_sep - 1);
    if (fail_sep == std::string::npos) {
        return {body.substr(0, tz_sep), body.substr(tz_sep + 1)};
    }
    return {body.substr(0, fail_sep), body.substr(fail_sep + 1, tz_sep - fail_sep - 1),
            body.substr(tz_sep + 1)};
}

bool HasAccessDescription(const std::shared_ptr<arrow::Field>& field) {
    return GetDescription(field).rfind(VariantAccessUtils::kMetadataKey, 0) == 0;
}

}  // namespace

constexpr char VariantAccessUtils::kMetadataKey[];
constexpr char VariantAccessUtils::kDelimiter;

std::string VariantAccessUtils::BuildVariantMetadata(const std::string& path, bool fail_on_error,
                                                     const std::string& zone_id) {
    return fmt::format("{}{}{}{}{}{}", kMetadataKey, path, kDelimiter,
                       fail_on_error ? "true" : "false", kDelimiter, zone_id);
}

bool VariantAccessUtils::IsVariantAccessType(const std::shared_ptr<arrow::DataType>& type) {
    if (type == nullptr || type->id() != arrow::Type::STRUCT || type->num_fields() == 0) {
        return false;
    }
    for (const auto& child : type->fields()) {
        if (!HasAccessDescription(child)) {
            return false;
        }
    }
    return true;
}

Result<std::vector<VariantAccessSpec>> VariantAccessUtils::ParseAccessSpecs(
    const std::shared_ptr<arrow::Field>& access_field) {
    if (!IsVariantAccessType(access_field->type())) {
        return Status::Invalid(
            fmt::format("field '{}' is not a variant-access projection", access_field->name()));
    }
    std::vector<VariantAccessSpec> specs;
    specs.reserve(access_field->type()->num_fields());
    for (const auto& child : access_field->type()->fields()) {
        std::string description = GetDescription(child);
        std::vector<std::string> parts = SplitDescription(description);
        if (parts.size() != 3) {
            return Status::Invalid(
                fmt::format("malformed variant access description '{}' on field '{}'", description,
                            child->name()));
        }
        VariantAccessSpec spec;
        spec.path = parts[0];
        PAIMON_ASSIGN_OR_RAISE(spec.segments, VariantPathSegment::Parse(spec.path));
        spec.cast_args.fail_on_error = parts[1] == "true";
        spec.cast_args.zone_id = parts[2];
        spec.target_field = child;
        specs.push_back(std::move(spec));
    }
    return specs;
}

Result<std::shared_ptr<arrow::Field>> VariantAccessUtils::ClipShreddedFileField(
    const std::vector<VariantAccessSpec>& specs, const std::shared_ptr<arrow::Field>& file_field) {
    if (file_field->type()->id() != arrow::Type::STRUCT) {
        return Status::Invalid(
            fmt::format("variant file field '{}' is not a struct", file_field->name()));
    }
    const auto& file_struct = static_cast<const arrow::StructType&>(*file_field->type());
    std::shared_ptr<arrow::Field> typed_value =
        file_struct.GetFieldByName(VariantDefs::kTypedValueFieldName);
    if (typed_value == nullptr) {
        // The file stores the column unshredded; there is nothing to prune.
        return file_field;
    }

    bool can_clip = true;
    std::set<std::string> fields_to_read;
    for (const auto& spec : specs) {
        if (spec.segments.empty()) {
            // A root path needs the whole variant.
            can_clip = false;
            break;
        }
        if (spec.segments[0].kind == VariantPathSegment::Kind::kObjectExtraction) {
            // Only top-level object keys are pruned; nested paths still narrow to their
            // top-level key.
            fields_to_read.insert(spec.segments[0].key);
        } else {
            can_clip = false;
            break;
        }
    }
    if (!can_clip) {
        return file_field;
    }

    std::shared_ptr<arrow::Field> metadata_field =
        file_struct.GetFieldByName(VariantDefs::kMetadataFieldName);
    std::shared_ptr<arrow::Field> value_field =
        file_struct.GetFieldByName(VariantDefs::kValueFieldName);
    if (metadata_field == nullptr) {
        return Status::Invalid(
            fmt::format("shredded variant field '{}' misses metadata", file_field->name()));
    }

    arrow::FieldVector typed_fields;
    if (typed_value->type()->id() == arrow::Type::STRUCT) {
        for (const auto& typed_child : typed_value->type()->fields()) {
            if (fields_to_read.erase(typed_child->name()) > 0) {
                typed_fields.push_back(typed_child);
            }
        }
    }

    arrow::FieldVector clipped_fields = {metadata_field};
    if (!fields_to_read.empty() && value_field != nullptr) {
        // Some requested key is not shredded; keep `value` for the binary fallback.
        clipped_fields.push_back(value_field);
    }
    if (!typed_fields.empty()) {
        clipped_fields.push_back(typed_value->WithType(arrow::struct_(typed_fields)));
    }
    return file_field->WithType(arrow::struct_(clipped_fields));
}

}  // namespace paimon
