/*
 * 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 <nlohmann/json.hpp>

#include "iceberg/expression/expression.h"
#include "iceberg/expression/json_serde_internal.h"
#include "iceberg/metrics/json_serde_internal.h"
#include "iceberg/util/json_util_internal.h"
#include "iceberg/util/string_util.h"

namespace iceberg {

namespace {

using StringMap = std::unordered_map<std::string, std::string>;

// JSON key constants (kebab-case, matching Iceberg spec)
constexpr std::string_view kTableName = "table-name";
constexpr std::string_view kSnapshotId = "snapshot-id";
constexpr std::string_view kFilter = "filter";
constexpr std::string_view kSchemaId = "schema-id";
constexpr std::string_view kProjectedFieldIds = "projected-field-ids";
constexpr std::string_view kProjectedFieldNames = "projected-field-names";
constexpr std::string_view kMetrics = "metrics";
constexpr std::string_view kMetadata = "metadata";
constexpr std::string_view kSequenceNumber = "sequence-number";
constexpr std::string_view kOperation = "operation";

// CounterResult / TimerResult keys
constexpr std::string_view kUnit = "unit";
constexpr std::string_view kTimeUnit = "time-unit";
constexpr std::string_view kValue = "value";
constexpr std::string_view kCount = "count";
constexpr std::string_view kTotalDuration = "total-duration";

// ScanMetricsResult keys
constexpr std::string_view kTotalPlanningDuration = "total-planning-duration";
constexpr std::string_view kResultDataFiles = "result-data-files";
constexpr std::string_view kResultDeleteFiles = "result-delete-files";
constexpr std::string_view kScannedDataManifests = "scanned-data-manifests";
constexpr std::string_view kScannedDeleteManifests = "scanned-delete-manifests";
constexpr std::string_view kTotalDataManifests = "total-data-manifests";
constexpr std::string_view kTotalDeleteManifests = "total-delete-manifests";
constexpr std::string_view kTotalFileSizeInBytes = "total-file-size-in-bytes";
constexpr std::string_view kTotalDeleteFileSizeInBytes =
    "total-delete-file-size-in-bytes";
constexpr std::string_view kSkippedDataManifests = "skipped-data-manifests";
constexpr std::string_view kSkippedDeleteManifests = "skipped-delete-manifests";
constexpr std::string_view kSkippedDataFiles = "skipped-data-files";
constexpr std::string_view kSkippedDeleteFiles = "skipped-delete-files";
constexpr std::string_view kIndexedDeleteFiles = "indexed-delete-files";
constexpr std::string_view kEqualityDeleteFiles = "equality-delete-files";
constexpr std::string_view kPositionalDeleteFiles = "positional-delete-files";
constexpr std::string_view kDvs = "dvs";

// CommitMetricsResult keys
constexpr std::string_view kAttempts = "attempts";
constexpr std::string_view kAddedDataFiles = "added-data-files";
constexpr std::string_view kRemovedDataFiles = "removed-data-files";
constexpr std::string_view kTotalDataFiles = "total-data-files";
constexpr std::string_view kAddedDeleteFiles = "added-delete-files";
constexpr std::string_view kAddedEqualityDeleteFiles = "added-equality-delete-files";
constexpr std::string_view kAddedPositionalDeleteFiles = "added-positional-delete-files";
constexpr std::string_view kAddedDvs = "added-dvs";
constexpr std::string_view kRemovedDeleteFiles = "removed-delete-files";
constexpr std::string_view kRemovedPositionalDeleteFiles =
    "removed-positional-delete-files";
constexpr std::string_view kRemovedDvs = "removed-dvs";
constexpr std::string_view kRemovedEqualityDeleteFiles = "removed-equality-delete-files";
constexpr std::string_view kTotalDeleteFiles = "total-delete-files";
constexpr std::string_view kAddedRecords = "added-records";
constexpr std::string_view kRemovedRecords = "removed-records";
constexpr std::string_view kTotalRecords = "total-records";
constexpr std::string_view kAddedFilesSizeBytes = "added-files-size-bytes";
constexpr std::string_view kRemovedFilesSizeBytes = "removed-files-size-bytes";
constexpr std::string_view kTotalFilesSizeBytes = "total-files-size-bytes";
constexpr std::string_view kAddedPositionalDeletes = "added-positional-deletes";
constexpr std::string_view kRemovedPositionalDeletes = "removed-positional-deletes";
constexpr std::string_view kTotalPositionalDeletes = "total-positional-deletes";
constexpr std::string_view kAddedEqualityDeletes = "added-equality-deletes";
constexpr std::string_view kRemovedEqualityDeletes = "removed-equality-deletes";
constexpr std::string_view kTotalEqualityDeletes = "total-equality-deletes";
constexpr std::string_view kKeptManifestCount = "manifests-kept";
constexpr std::string_view kCreatedManifestCount = "manifests-created";
constexpr std::string_view kReplacedManifestCount = "manifests-replaced";
constexpr std::string_view kProcessedManifestEntriesCount = "manifest-entries-processed";

void SetCounterField(nlohmann::json& json, std::string_view key,
                     const std::optional<CounterResult>& counter) {
  if (counter) {
    json[key] = ToJson(*counter);
  }
}

Result<std::optional<CounterResult>> ParseCounterResult(const nlohmann::json& json,
                                                        std::string_view key) {
  auto it = json.find(key);
  if (it == json.end()) return std::nullopt;
  return CounterResultFromJson(*it);
}

Result<std::optional<TimerResult>> ParseTimerResult(const nlohmann::json& json,
                                                    std::string_view key) {
  auto it = json.find(key);
  if (it == json.end()) return std::nullopt;
  return TimerResultFromJson(*it);
}

Result<std::chrono::nanoseconds> DurationToNanoseconds(int64_t duration,
                                                       std::string_view unit) {
  auto normalized = StringUtils::ToLower(unit);
  if (normalized == "nanoseconds") return std::chrono::nanoseconds{duration};
  if (normalized == "microseconds") return std::chrono::microseconds{duration};
  if (normalized == "milliseconds") return std::chrono::milliseconds{duration};
  if (normalized == "seconds") return std::chrono::seconds{duration};
  if (normalized == "minutes") return std::chrono::minutes{duration};
  if (normalized == "hours") return std::chrono::hours{duration};
  if (normalized == "days") return std::chrono::days{duration};
  return JsonParseError("Invalid time unit: {}", unit);
}

Result<int64_t> NanosecondsToDuration(std::chrono::nanoseconds duration,
                                      std::string_view unit) {
  auto normalized = StringUtils::ToLower(unit);
  if (normalized == "nanoseconds") return duration.count();
  if (normalized == "microseconds") {
    return std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
  }
  if (normalized == "milliseconds") {
    return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
  }
  if (normalized == "seconds") {
    return std::chrono::duration_cast<std::chrono::seconds>(duration).count();
  }
  if (normalized == "minutes") {
    return std::chrono::duration_cast<std::chrono::minutes>(duration).count();
  }
  if (normalized == "hours") {
    return std::chrono::duration_cast<std::chrono::hours>(duration).count();
  }
  if (normalized == "days") {
    return std::chrono::duration_cast<std::chrono::days>(duration).count();
  }
  return JsonParseError("Invalid time unit: {}", unit);
}

}  // namespace

// ---------------------------------------------------------------------------
// CounterResult
// ---------------------------------------------------------------------------

nlohmann::json ToJson(const CounterResult& counter) {
  return {{kUnit, ToString(counter.unit)}, {kValue, counter.value}};
}

Result<CounterResult> CounterResultFromJson(const nlohmann::json& json) {
  ICEBERG_ASSIGN_OR_RAISE(auto unit, GetJsonValue<std::string>(json, kUnit));
  ICEBERG_ASSIGN_OR_RAISE(auto parsed_unit, CounterUnitFromString(unit));
  ICEBERG_ASSIGN_OR_RAISE(auto value, GetJsonValue<int64_t>(json, kValue));
  return CounterResult{.unit = parsed_unit, .value = value};
}

// ---------------------------------------------------------------------------
// TimerResult
// ---------------------------------------------------------------------------

Result<nlohmann::json> ToJson(const TimerResult& timer) {
  ICEBERG_ASSIGN_OR_RAISE(auto duration,
                          NanosecondsToDuration(timer.total_duration, timer.unit));
  auto unit = StringUtils::ToLower(timer.unit);
  nlohmann::json json = {
      {kTimeUnit, unit}, {kCount, timer.count}, {kTotalDuration, duration}};
  return json;
}

Result<TimerResult> TimerResultFromJson(const nlohmann::json& json) {
  ICEBERG_ASSIGN_OR_RAISE(auto count, GetJsonValue<int64_t>(json, kCount));
  ICEBERG_ASSIGN_OR_RAISE(auto unit, GetJsonValue<std::string>(json, kTimeUnit));
  ICEBERG_ASSIGN_OR_RAISE(auto total, GetJsonValue<int64_t>(json, kTotalDuration));
  ICEBERG_ASSIGN_OR_RAISE(auto duration, DurationToNanoseconds(total, unit));
  return TimerResult{
      .unit = StringUtils::ToLower(unit), .count = count, .total_duration = duration};
}

// ---------------------------------------------------------------------------
// ScanMetricsResult
// ---------------------------------------------------------------------------

Result<nlohmann::json> ToJson(const ScanMetricsResult& m) {
  nlohmann::json json = nlohmann::json::object();
  if (m.total_planning_duration) {
    ICEBERG_ASSIGN_OR_RAISE(auto total_planning_duration,
                            ToJson(*m.total_planning_duration));
    json[std::string(kTotalPlanningDuration)] = std::move(total_planning_duration);
  }
  SetCounterField(json, kResultDataFiles, m.result_data_files);
  SetCounterField(json, kResultDeleteFiles, m.result_delete_files);
  SetCounterField(json, kScannedDataManifests, m.scanned_data_manifests);
  SetCounterField(json, kScannedDeleteManifests, m.scanned_delete_manifests);
  SetCounterField(json, kTotalDataManifests, m.total_data_manifests);
  SetCounterField(json, kTotalDeleteManifests, m.total_delete_manifests);
  SetCounterField(json, kTotalFileSizeInBytes, m.total_file_size_in_bytes);
  SetCounterField(json, kTotalDeleteFileSizeInBytes, m.total_delete_file_size_in_bytes);
  SetCounterField(json, kSkippedDataManifests, m.skipped_data_manifests);
  SetCounterField(json, kSkippedDeleteManifests, m.skipped_delete_manifests);
  SetCounterField(json, kSkippedDataFiles, m.skipped_data_files);
  SetCounterField(json, kSkippedDeleteFiles, m.skipped_delete_files);
  SetCounterField(json, kIndexedDeleteFiles, m.indexed_delete_files);
  SetCounterField(json, kEqualityDeleteFiles, m.equality_delete_files);
  SetCounterField(json, kPositionalDeleteFiles, m.positional_delete_files);
  SetCounterField(json, kDvs, m.dvs);
  return json;
}

Result<ScanMetricsResult> ScanMetricsResultFromJson(const nlohmann::json& json) {
  if (!json.is_object()) {
    return JsonParseError("Cannot parse scan metrics from non-object: {}",
                          SafeDumpJson(json));
  }
  ScanMetricsResult m;
  ICEBERG_ASSIGN_OR_RAISE(m.total_planning_duration,
                          ParseTimerResult(json, kTotalPlanningDuration));
  ICEBERG_ASSIGN_OR_RAISE(m.result_data_files,
                          ParseCounterResult(json, kResultDataFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.result_delete_files,
                          ParseCounterResult(json, kResultDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.scanned_data_manifests,
                          ParseCounterResult(json, kScannedDataManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.scanned_delete_manifests,
                          ParseCounterResult(json, kScannedDeleteManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.total_data_manifests,
                          ParseCounterResult(json, kTotalDataManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.total_delete_manifests,
                          ParseCounterResult(json, kTotalDeleteManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.total_file_size_in_bytes,
                          ParseCounterResult(json, kTotalFileSizeInBytes));
  ICEBERG_ASSIGN_OR_RAISE(m.total_delete_file_size_in_bytes,
                          ParseCounterResult(json, kTotalDeleteFileSizeInBytes));
  ICEBERG_ASSIGN_OR_RAISE(m.skipped_data_manifests,
                          ParseCounterResult(json, kSkippedDataManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.skipped_delete_manifests,
                          ParseCounterResult(json, kSkippedDeleteManifests));
  ICEBERG_ASSIGN_OR_RAISE(m.skipped_data_files,
                          ParseCounterResult(json, kSkippedDataFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.skipped_delete_files,
                          ParseCounterResult(json, kSkippedDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.indexed_delete_files,
                          ParseCounterResult(json, kIndexedDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.equality_delete_files,
                          ParseCounterResult(json, kEqualityDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.positional_delete_files,
                          ParseCounterResult(json, kPositionalDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.dvs, ParseCounterResult(json, kDvs));
  return m;
}

// ---------------------------------------------------------------------------
// CommitMetricsResult
// ---------------------------------------------------------------------------

Result<nlohmann::json> ToJson(const CommitMetricsResult& m) {
  nlohmann::json json = nlohmann::json::object();
  if (m.total_duration) {
    ICEBERG_ASSIGN_OR_RAISE(auto total_duration, ToJson(*m.total_duration));
    json[std::string(kTotalDuration)] = std::move(total_duration);
  }
  SetCounterField(json, kAttempts, m.attempts);
  SetCounterField(json, kAddedDataFiles, m.added_data_files);
  SetCounterField(json, kRemovedDataFiles, m.removed_data_files);
  SetCounterField(json, kTotalDataFiles, m.total_data_files);
  SetCounterField(json, kAddedDeleteFiles, m.added_delete_files);
  SetCounterField(json, kAddedEqualityDeleteFiles, m.added_equality_delete_files);
  SetCounterField(json, kAddedPositionalDeleteFiles, m.added_positional_delete_files);
  SetCounterField(json, kAddedDvs, m.added_dvs);
  SetCounterField(json, kRemovedDeleteFiles, m.removed_delete_files);
  SetCounterField(json, kRemovedPositionalDeleteFiles, m.removed_positional_delete_files);
  SetCounterField(json, kRemovedDvs, m.removed_dvs);
  SetCounterField(json, kRemovedEqualityDeleteFiles, m.removed_equality_delete_files);
  SetCounterField(json, kTotalDeleteFiles, m.total_delete_files);
  SetCounterField(json, kAddedRecords, m.added_records);
  SetCounterField(json, kRemovedRecords, m.removed_records);
  SetCounterField(json, kTotalRecords, m.total_records);
  SetCounterField(json, kAddedFilesSizeBytes, m.added_files_size_bytes);
  SetCounterField(json, kRemovedFilesSizeBytes, m.removed_files_size_bytes);
  SetCounterField(json, kTotalFilesSizeBytes, m.total_files_size_bytes);
  SetCounterField(json, kAddedPositionalDeletes, m.added_positional_deletes);
  SetCounterField(json, kRemovedPositionalDeletes, m.removed_positional_deletes);
  SetCounterField(json, kTotalPositionalDeletes, m.total_positional_deletes);
  SetCounterField(json, kAddedEqualityDeletes, m.added_equality_deletes);
  SetCounterField(json, kRemovedEqualityDeletes, m.removed_equality_deletes);
  SetCounterField(json, kTotalEqualityDeletes, m.total_equality_deletes);
  SetCounterField(json, kKeptManifestCount, m.kept_manifest_count);
  SetCounterField(json, kCreatedManifestCount, m.created_manifest_count);
  SetCounterField(json, kReplacedManifestCount, m.replaced_manifest_count);
  SetCounterField(json, kProcessedManifestEntriesCount,
                  m.processed_manifest_entries_count);
  return json;
}

Result<CommitMetricsResult> CommitMetricsResultFromJson(const nlohmann::json& json) {
  if (!json.is_object()) {
    return JsonParseError("Cannot parse commit metrics from non-object: {}",
                          SafeDumpJson(json));
  }
  CommitMetricsResult m;
  ICEBERG_ASSIGN_OR_RAISE(m.total_duration, ParseTimerResult(json, kTotalDuration));
  ICEBERG_ASSIGN_OR_RAISE(m.attempts, ParseCounterResult(json, kAttempts));
  ICEBERG_ASSIGN_OR_RAISE(m.added_data_files, ParseCounterResult(json, kAddedDataFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_data_files,
                          ParseCounterResult(json, kRemovedDataFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.total_data_files, ParseCounterResult(json, kTotalDataFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.added_delete_files,
                          ParseCounterResult(json, kAddedDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.added_equality_delete_files,
                          ParseCounterResult(json, kAddedEqualityDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.added_positional_delete_files,
                          ParseCounterResult(json, kAddedPositionalDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.added_dvs, ParseCounterResult(json, kAddedDvs));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_delete_files,
                          ParseCounterResult(json, kRemovedDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_positional_delete_files,
                          ParseCounterResult(json, kRemovedPositionalDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_dvs, ParseCounterResult(json, kRemovedDvs));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_equality_delete_files,
                          ParseCounterResult(json, kRemovedEqualityDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.total_delete_files,
                          ParseCounterResult(json, kTotalDeleteFiles));
  ICEBERG_ASSIGN_OR_RAISE(m.added_records, ParseCounterResult(json, kAddedRecords));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_records, ParseCounterResult(json, kRemovedRecords));
  ICEBERG_ASSIGN_OR_RAISE(m.total_records, ParseCounterResult(json, kTotalRecords));
  ICEBERG_ASSIGN_OR_RAISE(m.added_files_size_bytes,
                          ParseCounterResult(json, kAddedFilesSizeBytes));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_files_size_bytes,
                          ParseCounterResult(json, kRemovedFilesSizeBytes));
  ICEBERG_ASSIGN_OR_RAISE(m.total_files_size_bytes,
                          ParseCounterResult(json, kTotalFilesSizeBytes));
  ICEBERG_ASSIGN_OR_RAISE(m.added_positional_deletes,
                          ParseCounterResult(json, kAddedPositionalDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_positional_deletes,
                          ParseCounterResult(json, kRemovedPositionalDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.total_positional_deletes,
                          ParseCounterResult(json, kTotalPositionalDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.added_equality_deletes,
                          ParseCounterResult(json, kAddedEqualityDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.removed_equality_deletes,
                          ParseCounterResult(json, kRemovedEqualityDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.total_equality_deletes,
                          ParseCounterResult(json, kTotalEqualityDeletes));
  ICEBERG_ASSIGN_OR_RAISE(m.kept_manifest_count,
                          ParseCounterResult(json, kKeptManifestCount));
  ICEBERG_ASSIGN_OR_RAISE(m.created_manifest_count,
                          ParseCounterResult(json, kCreatedManifestCount));
  ICEBERG_ASSIGN_OR_RAISE(m.replaced_manifest_count,
                          ParseCounterResult(json, kReplacedManifestCount));
  ICEBERG_ASSIGN_OR_RAISE(m.processed_manifest_entries_count,
                          ParseCounterResult(json, kProcessedManifestEntriesCount));
  return m;
}

// ---------------------------------------------------------------------------
// ScanReport
// ---------------------------------------------------------------------------

Result<nlohmann::json> ToJson(const ScanReport& report) {
  nlohmann::json json;
  json[kTableName] = report.table_name;
  json[kSnapshotId] = report.snapshot_id;
  auto filter = report.filter ? report.filter : True::Instance();
  ICEBERG_ASSIGN_OR_RAISE(auto filter_json, ToJson(*filter));
  json[kFilter] = std::move(filter_json);
  json[kSchemaId] = report.schema_id;
  json[kProjectedFieldIds] = report.projected_field_ids;
  json[kProjectedFieldNames] = report.projected_field_names;
  ICEBERG_ASSIGN_OR_RAISE(auto metrics_json, ToJson(report.scan_metrics));
  json[kMetrics] = std::move(metrics_json);
  if (!report.metadata.empty()) {
    json[kMetadata] = report.metadata;
  }
  return json;
}

Result<ScanReport> ScanReportFromJson(const nlohmann::json& json) {
  ScanReport report;
  ICEBERG_ASSIGN_OR_RAISE(report.table_name, GetJsonValue<std::string>(json, kTableName));
  ICEBERG_ASSIGN_OR_RAISE(report.snapshot_id, GetJsonValue<int64_t>(json, kSnapshotId));
  ICEBERG_ASSIGN_OR_RAISE(auto filter_json, GetJsonValue<nlohmann::json>(json, kFilter));
  ICEBERG_ASSIGN_OR_RAISE(report.filter, ExpressionFromJson(filter_json));
  ICEBERG_ASSIGN_OR_RAISE(report.schema_id, GetJsonValue<int32_t>(json, kSchemaId));
  ICEBERG_ASSIGN_OR_RAISE(report.projected_field_ids,
                          GetJsonValue<std::vector<int32_t>>(json, kProjectedFieldIds));
  ICEBERG_ASSIGN_OR_RAISE(
      report.projected_field_names,
      GetJsonValue<std::vector<std::string>>(json, kProjectedFieldNames));
  ICEBERG_ASSIGN_OR_RAISE(auto metrics_json,
                          GetJsonValue<nlohmann::json>(json, kMetrics));
  ICEBERG_ASSIGN_OR_RAISE(report.scan_metrics, ScanMetricsResultFromJson(metrics_json));
  if (json.contains(kMetadata)) {
    ICEBERG_ASSIGN_OR_RAISE(report.metadata, GetJsonValue<StringMap>(json, kMetadata));
  }
  return report;
}

// ---------------------------------------------------------------------------
// CommitReport
// ---------------------------------------------------------------------------

Result<nlohmann::json> ToJson(const CommitReport& report) {
  nlohmann::json json;
  json[kTableName] = report.table_name;
  json[kSnapshotId] = report.snapshot_id;
  json[kSequenceNumber] = report.sequence_number;
  json[kOperation] = report.operation;
  ICEBERG_ASSIGN_OR_RAISE(auto metrics_json, ToJson(report.commit_metrics));
  json[kMetrics] = std::move(metrics_json);
  if (!report.metadata.empty()) {
    json[kMetadata] = report.metadata;
  }
  return json;
}

Result<CommitReport> CommitReportFromJson(const nlohmann::json& json) {
  CommitReport report;
  ICEBERG_ASSIGN_OR_RAISE(report.table_name, GetJsonValue<std::string>(json, kTableName));
  ICEBERG_ASSIGN_OR_RAISE(report.snapshot_id, GetJsonValue<int64_t>(json, kSnapshotId));
  ICEBERG_ASSIGN_OR_RAISE(report.sequence_number,
                          GetJsonValue<int64_t>(json, kSequenceNumber));
  ICEBERG_ASSIGN_OR_RAISE(report.operation, GetJsonValue<std::string>(json, kOperation));
  ICEBERG_ASSIGN_OR_RAISE(auto metrics_json,
                          GetJsonValue<nlohmann::json>(json, kMetrics));
  ICEBERG_ASSIGN_OR_RAISE(report.commit_metrics,
                          CommitMetricsResultFromJson(metrics_json));
  if (json.contains(kMetadata)) {
    ICEBERG_ASSIGN_OR_RAISE(report.metadata, GetJsonValue<StringMap>(json, kMetadata));
  }
  return report;
}

}  // namespace iceberg
