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

#include <cstdint>
#include <utility>

#include "iceberg/expression/binder.h"
#include "iceberg/expression/expression.h"
#include "iceberg/expression/residual_evaluator.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/manifest/manifest_group.h"
#include "iceberg/result.h"
#include "iceberg/schema.h"
#include "iceberg/snapshot.h"
#include "iceberg/table_metadata.h"
#include "iceberg/util/content_file_util.h"
#include "iceberg/util/macros.h"
#include "iceberg/util/snapshot_util_internal.h"
#include "iceberg/util/timepoint.h"
#include "iceberg/util/type_util.h"

namespace iceberg {

namespace {

const std::vector<std::string> kScanColumns = {
    "snapshot_id",         "file_path",          "file_ordinal",  "file_format",
    "block_size_in_bytes", "file_size_in_bytes", "record_count",  "partition",
    "key_metadata",        "split_offsets",      "sort_order_id",
};

const std::vector<std::string> kStatsColumns = {
    "value_counts", "null_value_counts", "nan_value_counts",
    "lower_bounds", "upper_bounds",      "column_sizes",
};

const std::vector<std::string> kScanColumnsWithStats = [] {
  auto cols = kScanColumns;
  cols.insert(cols.end(), kStatsColumns.begin(), kStatsColumns.end());
  return cols;
}();

}  // namespace

namespace internal {

Status TableScanContext::Validate() const {
  ICEBERG_CHECK(columns_to_keep_stats.empty() || return_column_stats,
                "Cannot select columns to keep stats when column stats are not returned");
  ICEBERG_CHECK(projected_schema == nullptr || selected_columns.empty(),
                "Cannot set projection schema and selected columns at the same time");
  ICEBERG_CHECK(!snapshot_id.has_value() ||
                    (!from_snapshot_id.has_value() && !to_snapshot_id.has_value()),
                "Cannot mix snapshot scan and incremental scan");
  ICEBERG_CHECK(!min_rows_requested.has_value() || min_rows_requested.value() >= 0,
                "Min rows requested cannot be negative");
  return {};
}

bool IsScanCurrentLineage(const TableScanContext& context) {
  return !context.from_snapshot_id.has_value() && !context.to_snapshot_id.has_value();
}

Result<int64_t> ToSnapshotIdInclusive(const TableScanContext& context,
                                      const TableMetadata& metadata) {
  // Get the branch's current snapshot ID if branch is set
  std::shared_ptr<Snapshot> branch_snapshot;
  const std::string& branch = context.branch;
  if (!branch.empty()) {
    auto iter = metadata.refs.find(branch);
    ICEBERG_CHECK(iter != metadata.refs.end() && iter->second != nullptr,
                  "Cannot find branch: {}", branch);
    ICEBERG_ASSIGN_OR_RAISE(branch_snapshot,
                            metadata.SnapshotById(iter->second->snapshot_id));
  }

  if (context.to_snapshot_id.has_value()) {
    int64_t to_snapshot_id_value = context.to_snapshot_id.value();

    if (branch_snapshot != nullptr) {
      // Validate `to_snapshot_id` is on the current branch
      ICEBERG_ASSIGN_OR_RAISE(
          bool is_ancestor,
          SnapshotUtil::IsAncestorOf(metadata, branch_snapshot->snapshot_id,
                                     to_snapshot_id_value));
      ICEBERG_CHECK(is_ancestor,
                    "End snapshot is not a valid snapshot on the current branch: {}",
                    branch);
    }

    return to_snapshot_id_value;
  }

  // If to_snapshot_id is not set, use branch's current snapshot if branch is set
  if (branch_snapshot != nullptr) {
    return branch_snapshot->snapshot_id;
  }

  // Get current snapshot from table's current snapshot
  std::shared_ptr<Snapshot> current_snapshot;
  ICEBERG_ASSIGN_OR_RAISE(current_snapshot, metadata.Snapshot());
  ICEBERG_CHECK(current_snapshot != nullptr,
                "End snapshot is not set and table has no current snapshot");
  return current_snapshot->snapshot_id;
}

Result<std::optional<int64_t>> FromSnapshotIdExclusive(const TableScanContext& context,
                                                       const TableMetadata& metadata,
                                                       int64_t to_snapshot_id_inclusive) {
  if (!context.from_snapshot_id.has_value()) {
    return std::nullopt;
  }

  int64_t from_snapshot_id = context.from_snapshot_id.value();

  // Validate `from_snapshot_id` is an ancestor of `to_snapshot_id_inclusive`
  if (context.from_snapshot_id_inclusive) {
    ICEBERG_ASSIGN_OR_RAISE(
        bool is_ancestor,
        SnapshotUtil::IsAncestorOf(metadata, to_snapshot_id_inclusive, from_snapshot_id));
    ICEBERG_CHECK(
        is_ancestor,
        "Starting snapshot (inclusive) {} is not an ancestor of end snapshot {}",
        from_snapshot_id, to_snapshot_id_inclusive);

    // For inclusive behavior, return the parent snapshot ID (can be nullopt)
    ICEBERG_ASSIGN_OR_RAISE(auto from_snapshot, metadata.SnapshotById(from_snapshot_id));
    return from_snapshot->parent_snapshot_id;
  }

  // Validate there is an ancestor of `to_snapshot_id_inclusive` where parent is
  // `from_snapshot_id`
  ICEBERG_ASSIGN_OR_RAISE(bool is_parent_ancestor,
                          SnapshotUtil::IsParentAncestorOf(
                              metadata, to_snapshot_id_inclusive, from_snapshot_id));
  ICEBERG_CHECK(
      is_parent_ancestor,
      "Starting snapshot (exclusive) {} is not a parent ancestor of end snapshot {}",
      from_snapshot_id, to_snapshot_id_inclusive);

  return from_snapshot_id;
}

}  // namespace internal

ScanTask::~ScanTask() = default;

// FileScanTask implementation

FileScanTask::FileScanTask(std::shared_ptr<DataFile> data_file,
                           std::vector<std::shared_ptr<DataFile>> delete_files,
                           std::shared_ptr<Expression> residual_filter)
    : data_file_(std::move(data_file)),
      delete_files_(std::move(delete_files)),
      residual_filter_(std::move(residual_filter)) {
  ICEBERG_DCHECK(data_file_ != nullptr, "Data file cannot be null for FileScanTask");
}

int64_t FileScanTask::size_bytes() const { return data_file_->file_size_in_bytes; }

int32_t FileScanTask::files_count() const { return 1; }

int64_t FileScanTask::estimated_row_count() const { return data_file_->record_count; }

// ChangelogScanTask implementation

int64_t ChangelogScanTask::size_bytes() const {
  int64_t total_size = data_file_->file_size_in_bytes;
  for (const auto& delete_file : delete_files_) {
    ICEBERG_DCHECK(delete_file->content_size_in_bytes.has_value(),
                   "Delete file content size must be available");
    total_size +=
        (delete_file->IsDeletionVector() ? delete_file->content_size_in_bytes.value()
                                         : delete_file->file_size_in_bytes);
  }
  return total_size;
}

int32_t ChangelogScanTask::files_count() const { return 1 + delete_files_.size(); }

int64_t ChangelogScanTask::estimated_row_count() const {
  return data_file_->record_count;
}

// Generic template implementation for Make
template <typename ScanType>
Result<std::unique_ptr<TableScanBuilder<ScanType>>> TableScanBuilder<ScanType>::Make(
    std::shared_ptr<TableMetadata> metadata, std::shared_ptr<FileIO> io) {
  ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
  ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
  return std::unique_ptr<TableScanBuilder<ScanType>>(
      new TableScanBuilder<ScanType>(std::move(metadata), std::move(io)));
}

template <typename ScanType>
TableScanBuilder<ScanType>::TableScanBuilder(
    std::shared_ptr<TableMetadata> table_metadata, std::shared_ptr<FileIO> file_io)
    : metadata_(std::move(table_metadata)), io_(std::move(file_io)) {}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Option(std::string key,
                                                               std::string value) {
  context_.options[std::move(key)] = std::move(value);
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Project(
    std::shared_ptr<Schema> schema) {
  context_.projected_schema = std::move(schema);
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::CaseSensitive(
    bool case_sensitive) {
  context_.case_sensitive = case_sensitive;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IncludeColumnStats() {
  context_.return_column_stats = true;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IncludeColumnStats(
    const std::vector<std::string>& requested_columns) {
  context_.return_column_stats = true;
  context_.columns_to_keep_stats.clear();
  context_.columns_to_keep_stats.reserve(requested_columns.size());

  ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto schema_ref, ResolveSnapshotSchema());
  const auto& schema = schema_ref.get();
  for (const auto& column_name : requested_columns) {
    ICEBERG_BUILDER_ASSIGN_OR_RETURN(auto field, schema->FindFieldByName(column_name));
    if (field.has_value()) {
      context_.columns_to_keep_stats.insert(field.value().get().field_id());
    }
  }

  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Select(
    const std::vector<std::string>& column_names) {
  context_.selected_columns = column_names;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::Filter(
    std::shared_ptr<Expression> filter) {
  context_.filter = std::move(filter);
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::IgnoreResiduals() {
  context_.ignore_residuals = true;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::MinRowsRequested(
    int64_t num_rows) {
  context_.min_rows_requested = num_rows;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseSnapshot(int64_t snapshot_id) {
  ICEBERG_BUILDER_CHECK(!context_.snapshot_id.has_value(),
                        "Cannot override snapshot, already set snapshot id={}",
                        context_.snapshot_id.value());
  ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(snapshot_id));
  context_.snapshot_id = snapshot_id;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseRef(const std::string& ref) {
  if (ref == SnapshotRef::kMainBranch) {
    snapshot_schema_ = nullptr;
    context_.snapshot_id.reset();
    return *this;
  }

  ICEBERG_BUILDER_CHECK(!context_.snapshot_id.has_value(),
                        "Cannot override ref, already set snapshot id={}",
                        context_.snapshot_id.value());
  auto iter = metadata_->refs.find(ref);
  ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref {}", ref);
  ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
  const int64_t snapshot_id = iter->second->snapshot_id;
  ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(snapshot_id));
  context_.snapshot_id = snapshot_id;

  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::AsOfTime(
    int64_t timestamp_millis) {
  auto time_point_ms = TimePointMsFromUnixMs(timestamp_millis);
  ICEBERG_BUILDER_ASSIGN_OR_RETURN(
      auto snapshot_id, SnapshotUtil::SnapshotIdAsOfTime(*metadata_, time_point_ms));
  return UseSnapshot(snapshot_id);
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::FromSnapshot(
    int64_t from_snapshot_id, bool inclusive)
  requires IsIncrementalScan<ScanType>
{
  if (inclusive) {
    ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore,
                                     metadata_->SnapshotById(from_snapshot_id));
  }
  this->context_.from_snapshot_id = from_snapshot_id;
  this->context_.from_snapshot_id_inclusive = inclusive;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::FromSnapshot(
    const std::string& ref, bool inclusive)
  requires IsIncrementalScan<ScanType>
{
  auto iter = metadata_->refs.find(ref);
  ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", ref);
  ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
  ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kTag,
                        "Ref {} is not a tag", ref);
  return FromSnapshot(iter->second->snapshot_id, inclusive);
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::ToSnapshot(int64_t to_snapshot_id)
  requires IsIncrementalScan<ScanType>
{
  ICEBERG_BUILDER_ASSIGN_OR_RETURN(std::ignore, metadata_->SnapshotById(to_snapshot_id));
  context_.to_snapshot_id = to_snapshot_id;
  return *this;
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::ToSnapshot(const std::string& ref)
  requires IsIncrementalScan<ScanType>
{
  auto iter = metadata_->refs.find(ref);
  ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", ref);
  ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", ref);
  ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kTag,
                        "Ref {} is not a tag", ref);
  return ToSnapshot(iter->second->snapshot_id);
}

template <typename ScanType>
TableScanBuilder<ScanType>& TableScanBuilder<ScanType>::UseBranch(
    const std::string& branch)
  requires IsIncrementalScan<ScanType>
{
  auto iter = metadata_->refs.find(branch);
  ICEBERG_BUILDER_CHECK(iter != metadata_->refs.end(), "Cannot find ref: {}", branch);
  ICEBERG_BUILDER_CHECK(iter->second != nullptr, "Ref {} is null", branch);
  ICEBERG_BUILDER_CHECK(iter->second->type() == SnapshotRefType::kBranch,
                        "Ref {} is not a branch", branch);
  context_.branch = branch;
  return *this;
}

template <typename ScanType>
Result<std::reference_wrapper<const std::shared_ptr<Schema>>>
TableScanBuilder<ScanType>::ResolveSnapshotSchema() {
  if (snapshot_schema_ == nullptr) {
    if (context_.snapshot_id.has_value()) {
      ICEBERG_ASSIGN_OR_RAISE(auto snapshot,
                              metadata_->SnapshotById(*context_.snapshot_id));
      int32_t schema_id = snapshot->schema_id.value_or(Schema::kInitialSchemaId);
      ICEBERG_ASSIGN_OR_RAISE(snapshot_schema_, metadata_->SchemaById(schema_id));
    } else {
      ICEBERG_ASSIGN_OR_RAISE(snapshot_schema_, metadata_->Schema());
    }
  }
  ICEBERG_CHECK(snapshot_schema_ != nullptr, "Snapshot schema is null");
  return snapshot_schema_;
}

template <typename ScanType>
Result<std::unique_ptr<ScanType>> TableScanBuilder<ScanType>::Build() {
  ICEBERG_RETURN_UNEXPECTED(CheckErrors());
  ICEBERG_RETURN_UNEXPECTED(context_.Validate());

  ICEBERG_ASSIGN_OR_RAISE(auto schema, ResolveSnapshotSchema());
  return ScanType::Make(metadata_, schema.get(), io_, std::move(context_));
}

// Explicit template instantiations
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<DataTableScan>;
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<IncrementalAppendScan>;
template class ICEBERG_TEMPLATE_EXPORT TableScanBuilder<IncrementalChangelogScan>;

TableScan::TableScan(std::shared_ptr<TableMetadata> metadata,
                     std::shared_ptr<Schema> schema, std::shared_ptr<FileIO> file_io,
                     internal::TableScanContext context)
    : metadata_(std::move(metadata)),
      schema_(std::move(schema)),
      io_(std::move(file_io)),
      context_(std::move(context)) {}

TableScan::~TableScan() = default;

const std::shared_ptr<TableMetadata>& TableScan::metadata() const { return metadata_; }

Result<std::shared_ptr<Snapshot>> TableScan::snapshot() const {
  auto snapshot_id = context_.snapshot_id ? context_.snapshot_id.value()
                                          : metadata_->current_snapshot_id;
  if (snapshot_id == kInvalidSnapshotId) {
    return std::shared_ptr<Snapshot>{nullptr};
  }
  return metadata_->SnapshotById(snapshot_id);
}

Result<std::shared_ptr<Schema>> TableScan::schema() const {
  return ResolveProjectedSchema();
}

const internal::TableScanContext& TableScan::context() const { return context_; }

const std::shared_ptr<FileIO>& TableScan::io() const { return io_; }

const std::shared_ptr<Expression>& TableScan::filter() const {
  const static std::shared_ptr<Expression> true_expr = True::Instance();
  if (!context_.filter) {
    return true_expr;
  }
  return context_.filter;
}

bool TableScan::is_case_sensitive() const { return context_.case_sensitive; }

Result<std::reference_wrapper<const std::shared_ptr<Schema>>>
TableScan::ResolveProjectedSchema() const {
  if (projected_schema_ != nullptr) {
    return projected_schema_;
  }

  if (!context_.selected_columns.empty()) {
    std::unordered_set<int32_t> required_field_ids;

    // Include columns referenced by filter
    if (context_.filter != nullptr) {
      ICEBERG_ASSIGN_OR_RAISE(auto is_bound, IsBoundVisitor::IsBound(context_.filter));
      if (is_bound) {
        ICEBERG_ASSIGN_OR_RAISE(required_field_ids,
                                ReferenceVisitor::GetReferencedFieldIds(context_.filter));
      } else {
        ICEBERG_ASSIGN_OR_RAISE(auto filter, Binder::Bind(*schema_, context_.filter,
                                                          context_.case_sensitive));
        ICEBERG_ASSIGN_OR_RAISE(required_field_ids,
                                ReferenceVisitor::GetReferencedFieldIds(filter));
      }
    }

    // Include columns selected by option
    ICEBERG_ASSIGN_OR_RAISE(auto selected, schema_->Select(context_.selected_columns,
                                                           context_.case_sensitive));
    ICEBERG_ASSIGN_OR_RAISE(
        auto selected_field_ids,
        GetProjectedIdsVisitor::GetProjectedIds(*selected, /*include_struct_ids=*/true));
    required_field_ids.insert(std::make_move_iterator(selected_field_ids.begin()),
                              std::make_move_iterator(selected_field_ids.end()));

    ICEBERG_ASSIGN_OR_RAISE(projected_schema_, schema_->Project(required_field_ids));
  } else if (context_.projected_schema != nullptr) {
    projected_schema_ = context_.projected_schema;
  } else {
    projected_schema_ = schema_;
  }

  return projected_schema_;
}

const std::vector<std::string>& TableScan::ScanColumns() const {
  return context_.return_column_stats ? kScanColumnsWithStats : kScanColumns;
}

Result<std::unique_ptr<DataTableScan>> DataTableScan::Make(
    std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
    std::shared_ptr<FileIO> io, internal::TableScanContext context) {
  ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
  ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
  ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
  return std::unique_ptr<DataTableScan>(new DataTableScan(
      std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}

Result<std::vector<std::shared_ptr<FileScanTask>>> DataTableScan::PlanFiles() const {
  ICEBERG_ASSIGN_OR_RAISE(auto snapshot, this->snapshot());
  if (!snapshot) {
    return std::vector<std::shared_ptr<FileScanTask>>{};
  }

  TableMetadataCache metadata_cache(metadata_.get());
  ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());

  SnapshotCache snapshot_cache(snapshot.get());
  ICEBERG_ASSIGN_OR_RAISE(auto data_manifests, snapshot_cache.DataManifests(io_));
  ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests, snapshot_cache.DeleteManifests(io_));

  ICEBERG_ASSIGN_OR_RAISE(
      auto manifest_group,
      ManifestGroup::Make(io_, schema_, specs_by_id,
                          {data_manifests.begin(), data_manifests.end()},
                          {delete_manifests.begin(), delete_manifests.end()}));
  manifest_group->CaseSensitive(context_.case_sensitive)
      .Select(ScanColumns())
      .FilterData(filter())
      .IgnoreDeleted()
      .ColumnsToKeepStats(context_.columns_to_keep_stats);
  if (context_.ignore_residuals) {
    manifest_group->IgnoreResiduals();
  }
  return manifest_group->PlanFiles();
}

// Friend function template for IncrementalScan that implements the shared PlanFiles
// logic. It resolves the from/to snapshot range from the scan context and delegates
// to the two-arg virtual PlanFiles() override in the concrete subclass.
// Defined as a friend to access the protected two-arg PlanFiles().
template <typename ScanTaskType>
Result<std::vector<std::shared_ptr<ScanTaskType>>> ResolvePlanFiles(
    const IncrementalScan<ScanTaskType>& scan) {
  if (IsScanCurrentLineage(scan.context())) {
    if (scan.metadata()->current_snapshot_id == kInvalidSnapshotId) {
      return std::vector<std::shared_ptr<ScanTaskType>>{};
    }
  }

  ICEBERG_ASSIGN_OR_RAISE(
      int64_t to_snapshot_id_inclusive,
      internal::ToSnapshotIdInclusive(scan.context(), *scan.metadata()));
  ICEBERG_ASSIGN_OR_RAISE(
      std::optional<int64_t> from_snapshot_id_exclusive,
      internal::FromSnapshotIdExclusive(scan.context(), *scan.metadata(),
                                        to_snapshot_id_inclusive));

  return scan.PlanFiles(from_snapshot_id_exclusive, to_snapshot_id_inclusive);
}

// IncrementalAppendScan implementation

Result<std::unique_ptr<IncrementalAppendScan>> IncrementalAppendScan::Make(
    std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
    std::shared_ptr<FileIO> io, internal::TableScanContext context) {
  ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
  ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
  ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
  return std::unique_ptr<IncrementalAppendScan>(new IncrementalAppendScan(
      std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}

Result<std::vector<std::shared_ptr<FileScanTask>>> IncrementalAppendScan::PlanFiles()
    const {
  return ResolvePlanFiles<FileScanTask>(*this);
}

Result<std::vector<std::shared_ptr<FileScanTask>>> IncrementalAppendScan::PlanFiles(
    std::optional<int64_t> from_snapshot_id_exclusive,
    int64_t to_snapshot_id_inclusive) const {
  ICEBERG_ASSIGN_OR_RAISE(
      auto ancestors_snapshots,
      SnapshotUtil::AncestorsBetween(*metadata_, to_snapshot_id_inclusive,
                                     from_snapshot_id_exclusive));

  std::vector<std::shared_ptr<Snapshot>> append_snapshots;
  std::ranges::copy_if(ancestors_snapshots, std::back_inserter(append_snapshots),
                       [](const auto& snapshot) {
                         return snapshot != nullptr &&
                                snapshot->Operation().has_value() &&
                                snapshot->Operation().value() == DataOperation::kAppend;
                       });
  if (append_snapshots.empty()) {
    return std::vector<std::shared_ptr<FileScanTask>>{};
  }

  std::unordered_set<int64_t> snapshot_ids;
  std::ranges::transform(append_snapshots,
                         std::inserter(snapshot_ids, snapshot_ids.end()),
                         [](const auto& snapshot) { return snapshot->snapshot_id; });

  std::unordered_set<ManifestFile> data_manifests;
  for (const auto& snapshot : append_snapshots) {
    SnapshotCache snapshot_cache(snapshot.get());
    ICEBERG_ASSIGN_OR_RAISE(auto manifests, snapshot_cache.DataManifests(io_));
    std::ranges::copy_if(manifests, std::inserter(data_manifests, data_manifests.end()),
                         [&snapshot_ids](const ManifestFile& manifest) {
                           return snapshot_ids.contains(manifest.added_snapshot_id);
                         });
  }
  if (data_manifests.empty()) {
    return std::vector<std::shared_ptr<FileScanTask>>{};
  }

  TableMetadataCache metadata_cache(metadata_.get());
  ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());

  ICEBERG_ASSIGN_OR_RAISE(
      auto manifest_group,
      ManifestGroup::Make(
          io_, schema_, specs_by_id,
          std::vector<ManifestFile>(data_manifests.begin(), data_manifests.end()), {}));

  manifest_group->CaseSensitive(context_.case_sensitive)
      .Select(ScanColumns())
      .FilterData(filter())
      .FilterManifestEntries([&snapshot_ids](const ManifestEntry& entry) {
        return entry.snapshot_id.has_value() &&
               snapshot_ids.contains(entry.snapshot_id.value()) &&
               entry.status == ManifestStatus::kAdded;
      })
      .IgnoreDeleted()
      .ColumnsToKeepStats(context_.columns_to_keep_stats);

  if (context_.ignore_residuals) {
    manifest_group->IgnoreResiduals();
  }

  return manifest_group->PlanFiles();
}

// IncrementalChangelogScan implementation

Result<std::unique_ptr<IncrementalChangelogScan>> IncrementalChangelogScan::Make(
    std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
    std::shared_ptr<FileIO> io, internal::TableScanContext context) {
  ICEBERG_PRECHECK(metadata != nullptr, "Table metadata cannot be null");
  ICEBERG_PRECHECK(schema != nullptr, "Schema cannot be null");
  ICEBERG_PRECHECK(io != nullptr, "FileIO cannot be null");
  return std::unique_ptr<IncrementalChangelogScan>(new IncrementalChangelogScan(
      std::move(metadata), std::move(schema), std::move(io), std::move(context)));
}

Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
IncrementalChangelogScan::PlanFiles() const {
  return ResolvePlanFiles<ChangelogScanTask>(*this);
}

Result<std::vector<std::shared_ptr<ChangelogScanTask>>>
IncrementalChangelogScan::PlanFiles(std::optional<int64_t> from_snapshot_id_exclusive,
                                    int64_t to_snapshot_id_inclusive) const {
  ICEBERG_ASSIGN_OR_RAISE(
      auto ancestors_snapshots,
      SnapshotUtil::AncestorsBetween(*metadata_, to_snapshot_id_inclusive,
                                     from_snapshot_id_exclusive));

  std::vector<std::pair<std::shared_ptr<Snapshot>, std::unique_ptr<SnapshotCache>>>
      changelog_snapshots;

  for (const auto& snapshot : std::ranges::reverse_view(ancestors_snapshots)) {
    auto operation = snapshot->Operation();
    if (!operation.has_value() || operation.value() != DataOperation::kReplace) {
      auto snapshot_cache = std::make_unique<SnapshotCache>(snapshot.get());
      ICEBERG_ASSIGN_OR_RAISE(auto delete_manifests,
                              snapshot_cache->DeleteManifests(io_));
      if (!delete_manifests.empty()) {
        return NotSupported(
            "Delete files are currently not supported in changelog scans");
      }
      changelog_snapshots.emplace_back(snapshot, std::move(snapshot_cache));
    }
  }
  if (changelog_snapshots.empty()) {
    return std::vector<std::shared_ptr<ChangelogScanTask>>{};
  }

  std::unordered_set<int64_t> snapshot_ids;
  std::unordered_map<int64_t, int32_t> snapshot_ordinals;
  for (const auto& snapshot : changelog_snapshots) {
    ICEBERG_PRECHECK(
        std::cmp_less_equal(snapshot_ids.size(), std::numeric_limits<int32_t>::max()),
        "Number of snapshots in changelog scan exceeds maximum supported");
    snapshot_ids.insert(snapshot.first->snapshot_id);
    snapshot_ordinals.try_emplace(snapshot.first->snapshot_id,
                                  static_cast<int32_t>(snapshot_ordinals.size()));
  }

  std::vector<ManifestFile> data_manifests;
  std::unordered_set<std::string> seen_manifest_paths;
  for (const auto& snapshot : changelog_snapshots) {
    ICEBERG_ASSIGN_OR_RAISE(auto manifests, snapshot.second->DataManifests(io_));
    for (auto& manifest : manifests) {
      if (snapshot_ids.contains(manifest.added_snapshot_id) &&
          seen_manifest_paths.insert(manifest.manifest_path).second) {
        data_manifests.push_back(manifest);
      }
    }
  }
  if (data_manifests.empty()) {
    return std::vector<std::shared_ptr<ChangelogScanTask>>{};
  }

  TableMetadataCache metadata_cache(metadata_.get());
  ICEBERG_ASSIGN_OR_RAISE(auto specs_by_id, metadata_cache.GetPartitionSpecsById());

  ICEBERG_ASSIGN_OR_RAISE(
      auto manifest_group,
      ManifestGroup::Make(io_, schema_, specs_by_id, std::move(data_manifests),
                          /*delete_manifests=*/{}));

  manifest_group->CaseSensitive(context_.case_sensitive)
      .Select(ScanColumns())
      .FilterData(filter())
      .FilterManifestEntries([&snapshot_ids](const ManifestEntry& entry) {
        return entry.snapshot_id.has_value() &&
               snapshot_ids.contains(entry.snapshot_id.value());
      })
      .IgnoreExisting()
      .ColumnsToKeepStats(context_.columns_to_keep_stats);

  if (context_.ignore_residuals) {
    manifest_group->IgnoreResiduals();
  }

  auto create_tasks_func =
      [&snapshot_ordinals](
          std::vector<ManifestEntry>&& entries,
          const TaskContext& ctx) -> Result<std::vector<std::shared_ptr<ScanTask>>> {
    std::vector<std::shared_ptr<ScanTask>> tasks;
    tasks.reserve(entries.size());

    for (auto& entry : entries) {
      ICEBERG_PRECHECK(entry.snapshot_id.has_value() && entry.data_file,
                       "Invalid manifest entry with missing snapshot id or data file");

      int64_t commit_snapshot_id = entry.snapshot_id.value();
      auto ordinal_it = snapshot_ordinals.find(commit_snapshot_id);
      ICEBERG_PRECHECK(ordinal_it != snapshot_ordinals.end(),
                       "Invalid manifest entry with missing snapshot ordinal");

      int32_t change_ordinal = ordinal_it->second;

      if (ctx.drop_stats) {
        ContentFileUtil::DropAllStats(*entry.data_file);
      } else if (!ctx.columns_to_keep_stats.empty()) {
        ContentFileUtil::DropUnselectedStats(*entry.data_file, ctx.columns_to_keep_stats);
      }

      ICEBERG_ASSIGN_OR_RAISE(auto residual,
                              ctx.residuals->ResidualFor(entry.data_file->partition));

      switch (entry.status) {
        case ManifestStatus::kAdded:
          tasks.push_back(std::make_shared<AddedRowsScanTask>(
              change_ordinal, commit_snapshot_id, std::move(entry.data_file),
              std::vector<std::shared_ptr<DataFile>>{}, std::move(residual)));
          break;
        case ManifestStatus::kDeleted:
          tasks.push_back(std::make_shared<DeletedDataFileScanTask>(
              change_ordinal, commit_snapshot_id, std::move(entry.data_file),
              std::vector<std::shared_ptr<DataFile>>{}, std::move(residual)));
          break;
        case ManifestStatus::kExisting:
          return InvalidArgument("Unexpected entry status: EXISTING");
      }
    }
    return tasks;
  };

  ICEBERG_ASSIGN_OR_RAISE(auto tasks, manifest_group->Plan(create_tasks_func));
  return tasks | std::views::transform([](const auto& task) {
           return std::static_pointer_cast<ChangelogScanTask>(task);
         }) |
         std::ranges::to<std::vector>();
}

}  // namespace iceberg
