/*
 * 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 "iceberg/data/position_delete_writer.h"

#include <map>
#include <set>
#include <vector>

#include <nanoarrow/nanoarrow.h>

#include "iceberg/arrow/nanoarrow_status_internal.h"
#include "iceberg/arrow_c_data_guard_internal.h"
#include "iceberg/file_writer.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/metadata_columns.h"
#include "iceberg/partition_spec.h"
#include "iceberg/schema.h"
#include "iceberg/schema_internal.h"
#include "iceberg/util/macros.h"

namespace iceberg {

class PositionDeleteWriter::Impl {
 public:
  static Result<std::unique_ptr<Impl>> Make(PositionDeleteWriterOptions options) {
    auto delete_schema = std::make_shared<Schema>(std::vector<SchemaField>{
        MetadataColumns::kDeleteFilePath,
        MetadataColumns::kDeleteFilePos,
    });

    WriterOptions writer_options{
        .path = options.path,
        .schema = delete_schema,
        .io = options.io,
        .properties = WriterProperties::FromMap(options.properties),
    };

    ICEBERG_ASSIGN_OR_RAISE(auto writer,
                            WriterFactoryRegistry::Open(options.format, writer_options));

    return std::unique_ptr<Impl>(
        new Impl(std::move(options), std::move(delete_schema), std::move(writer)));
  }

  Status Write(ArrowArray* data) {
    ICEBERG_PRECHECK(buffered_paths_.empty(),
                     "Cannot write batch data when there are buffered deletes.");
    // TODO(anyone): Extract file paths from ArrowArray to update referenced_paths_.
    return writer_->Write(data);
  }

  Status WriteDelete(std::string_view file_path, int64_t pos) {
    // TODO(anyone): check if the sort order of file_path and pos observes the spec.
    buffered_paths_.emplace_back(file_path);
    buffered_positions_.push_back(pos);
    referenced_paths_.emplace(file_path);

    if (buffered_paths_.size() >= options_.flush_threshold) {
      return FlushBuffer();
    }
    return {};
  }

  Result<int64_t> Length() const { return writer_->length(); }

  Status Close() {
    if (closed_) {
      return {};
    }
    if (!buffered_paths_.empty()) {
      ICEBERG_RETURN_UNEXPECTED(FlushBuffer());
    }
    ICEBERG_RETURN_UNEXPECTED(writer_->Close());
    closed_ = true;
    return {};
  }

  Result<FileWriter::WriteResult> Metadata() {
    ICEBERG_CHECK(closed_, "Cannot get metadata before closing the writer");

    ICEBERG_ASSIGN_OR_RAISE(auto metrics, writer_->metrics());
    ICEBERG_ASSIGN_OR_RAISE(auto length, writer_->length());
    auto split_offsets = writer_->split_offsets();

    // Filter out metrics for delete metadata columns (file_path, pos) to avoid
    // bloating the manifest, matching Java's PositionDeleteWriter behavior.
    // Always remove field counts; also remove bounds when referencing multiple files.
    const auto path_id = MetadataColumns::kDeleteFilePathColumnId;
    const auto pos_id = MetadataColumns::kDeleteFilePosColumnId;

    metrics.value_counts.erase(path_id);
    metrics.value_counts.erase(pos_id);
    metrics.null_value_counts.erase(path_id);
    metrics.null_value_counts.erase(pos_id);
    metrics.nan_value_counts.erase(path_id);
    metrics.nan_value_counts.erase(pos_id);

    if (referenced_paths_.size() > 1) {
      metrics.lower_bounds.erase(path_id);
      metrics.lower_bounds.erase(pos_id);
      metrics.upper_bounds.erase(path_id);
      metrics.upper_bounds.erase(pos_id);
    }

    // Serialize literal bounds to binary format
    std::map<int32_t, std::vector<uint8_t>> lower_bounds_map;
    for (const auto& [col_id, literal] : metrics.lower_bounds) {
      ICEBERG_ASSIGN_OR_RAISE(auto serialized, literal.Serialize());
      lower_bounds_map[col_id] = std::move(serialized);
    }
    std::map<int32_t, std::vector<uint8_t>> upper_bounds_map;
    for (const auto& [col_id, literal] : metrics.upper_bounds) {
      ICEBERG_ASSIGN_OR_RAISE(auto serialized, literal.Serialize());
      upper_bounds_map[col_id] = std::move(serialized);
    }

    // Set referenced_data_file if all deletes reference the same data file
    std::optional<std::string> referenced_data_file;
    if (referenced_paths_.size() == 1) {
      referenced_data_file = *referenced_paths_.begin();
    }

    auto data_file = std::make_shared<DataFile>(DataFile{
        .content = DataFile::Content::kPositionDeletes,
        .file_path = options_.path,
        .file_format = options_.format,
        .partition = options_.partition,
        .record_count = metrics.row_count.value_or(-1),
        .file_size_in_bytes = length,
        .column_sizes = {metrics.column_sizes.begin(), metrics.column_sizes.end()},
        .value_counts = {metrics.value_counts.begin(), metrics.value_counts.end()},
        .null_value_counts = {metrics.null_value_counts.begin(),
                              metrics.null_value_counts.end()},
        .nan_value_counts = {metrics.nan_value_counts.begin(),
                             metrics.nan_value_counts.end()},
        .lower_bounds = std::move(lower_bounds_map),
        .upper_bounds = std::move(upper_bounds_map),
        .split_offsets = std::move(split_offsets),
        .sort_order_id = std::nullopt,
        .referenced_data_file = std::move(referenced_data_file),
        .partition_spec_id =
            options_.spec ? std::make_optional(options_.spec->spec_id()) : std::nullopt,
    });

    FileWriter::WriteResult result;
    result.data_files.push_back(std::move(data_file));
    return result;
  }

 private:
  Impl(PositionDeleteWriterOptions options, std::shared_ptr<Schema> delete_schema,
       std::unique_ptr<Writer> writer)
      : options_(std::move(options)),
        delete_schema_(std::move(delete_schema)),
        writer_(std::move(writer)) {}

  Status FlushBuffer() {
    ArrowSchema arrow_schema;
    ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(*delete_schema_, &arrow_schema));
    internal::ArrowSchemaGuard schema_guard(&arrow_schema);

    ArrowArray array;
    ArrowError error;
    ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
        ArrowArrayInitFromSchema(&array, &arrow_schema, &error), error);
    internal::ArrowArrayGuard array_guard(&array);
    ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayStartAppending(&array));

    for (size_t i = 0; i < buffered_paths_.size(); ++i) {
      ArrowStringView path_view(buffered_paths_[i].data(),
                                static_cast<int64_t>(buffered_paths_[i].size()));
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(
          ArrowArrayAppendString(array.children[0], path_view));
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(
          ArrowArrayAppendInt(array.children[1], buffered_positions_[i]));
      ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(&array));
    }

    ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
        ArrowArrayFinishBuildingDefault(&array, &error), error);

    ICEBERG_RETURN_UNEXPECTED(writer_->Write(&array));

    buffered_paths_.clear();
    buffered_positions_.clear();
    return {};
  }

  PositionDeleteWriterOptions options_;
  std::shared_ptr<Schema> delete_schema_;
  std::unique_ptr<Writer> writer_;
  bool closed_ = false;
  std::vector<std::string> buffered_paths_;
  std::vector<int64_t> buffered_positions_;
  std::set<std::string> referenced_paths_;
};

PositionDeleteWriter::PositionDeleteWriter(std::unique_ptr<Impl> impl)
    : impl_(std::move(impl)) {}

PositionDeleteWriter::~PositionDeleteWriter() = default;

Result<std::unique_ptr<PositionDeleteWriter>> PositionDeleteWriter::Make(
    const PositionDeleteWriterOptions& options) {
  ICEBERG_ASSIGN_OR_RAISE(auto impl, Impl::Make(options));
  return std::unique_ptr<PositionDeleteWriter>(new PositionDeleteWriter(std::move(impl)));
}

Status PositionDeleteWriter::Write(ArrowArray* data) { return impl_->Write(data); }

Status PositionDeleteWriter::WriteDelete(std::string_view file_path, int64_t pos) {
  return impl_->WriteDelete(file_path, pos);
}

Result<int64_t> PositionDeleteWriter::Length() const { return impl_->Length(); }

Status PositionDeleteWriter::Close() { return impl_->Close(); }

Result<FileWriter::WriteResult> PositionDeleteWriter::Metadata() {
  return impl_->Metadata();
}

}  // namespace iceberg
