/*
 * 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 <algorithm>
#include <cstring>

#include <arrow/array.h>
#include <arrow/extension_type.h>
#include <arrow/type.h>
#include <avro/Specific.hh>

#include "iceberg/avro/avro_direct_encoder_internal.h"
#include "iceberg/type.h"
#include "iceberg/util/checked_cast.h"
#include "iceberg/util/macros.h"

namespace iceberg::avro {

namespace {

// Utility struct for union branch information
struct UnionBranches {
  size_t null_index;
  size_t value_index;
  ::avro::NodePtr value_node;
};

Result<UnionBranches> ValidateUnion(const ::avro::NodePtr& union_node) {
  ICEBERG_PRECHECK(union_node->leaves() == 2,
                   "Union must have exactly 2 branches, got {}", union_node->leaves());

  const auto& branch_0 = union_node->leafAt(0);
  const auto& branch_1 = union_node->leafAt(1);

  if (branch_0->type() == ::avro::AVRO_NULL && branch_1->type() != ::avro::AVRO_NULL) {
    return UnionBranches{.null_index = 0, .value_index = 1, .value_node = branch_1};
  }
  if (branch_1->type() == ::avro::AVRO_NULL && branch_0->type() != ::avro::AVRO_NULL) {
    return UnionBranches{.null_index = 1, .value_index = 0, .value_node = branch_0};
  }
  return InvalidArgument("Union must have exactly one null branch");
}

}  // namespace

Status EncodeArrowToAvro(const ::avro::NodePtr& avro_node, ::avro::Encoder& encoder,
                         const Type& type, const ::arrow::Array& array, int64_t row_index,
                         EncodeContext& ctx) {
  ICEBERG_PRECHECK(row_index >= 0 && row_index < array.length(),
                   "Row index {} out of bounds {}", row_index, array.length());

  const bool is_null = array.IsNull(row_index);

  if (avro_node->type() == ::avro::AVRO_UNION) {
    ICEBERG_ASSIGN_OR_RAISE(auto branches, ValidateUnion(avro_node));

    if (is_null) {
      encoder.encodeUnionIndex(branches.null_index);
      encoder.encodeNull();
      return {};
    }

    encoder.encodeUnionIndex(branches.value_index);
    return EncodeArrowToAvro(branches.value_node, encoder, type, array, row_index, ctx);
  }

  if (avro_node->type() == ::avro::AVRO_NULL) {
    encoder.encodeNull();
    return {};
  }

  if (is_null) {
    return InvalidArgument("Null value in non-nullable field");
  }

  switch (avro_node->type()) {
    case ::avro::AVRO_BOOL: {
      const auto& bool_array =
          internal::checked_cast<const ::arrow::BooleanArray&>(array);
      encoder.encodeBool(bool_array.Value(row_index));
      return {};
    }

    case ::avro::AVRO_INT: {
      // AVRO_INT can represent: int32, date (days since epoch)
      switch (array.type()->id()) {
        case ::arrow::Type::INT32: {
          const auto& int32_array =
              internal::checked_cast<const ::arrow::Int32Array&>(array);
          encoder.encodeInt(int32_array.Value(row_index));
          return {};
        }
        case ::arrow::Type::DATE32: {
          const auto& date_array =
              internal::checked_cast<const ::arrow::Date32Array&>(array);
          encoder.encodeInt(date_array.Value(row_index));
          return {};
        }
        default:
          return InvalidArgument("AVRO_INT expects Int32Array or Date32Array, got {}",
                                 array.type()->ToString());
      }
    }

    case ::avro::AVRO_LONG: {
      // AVRO_LONG can represent: int64, time (microseconds), timestamp (microseconds)
      switch (array.type()->id()) {
        case ::arrow::Type::INT64: {
          const auto& int64_array =
              internal::checked_cast<const ::arrow::Int64Array&>(array);
          encoder.encodeLong(int64_array.Value(row_index));
          return {};
        }
        case ::arrow::Type::TIME64: {
          const auto& time_array =
              internal::checked_cast<const ::arrow::Time64Array&>(array);
          encoder.encodeLong(time_array.Value(row_index));
          return {};
        }
        case ::arrow::Type::TIMESTAMP: {
          const auto& timestamp_array =
              internal::checked_cast<const ::arrow::TimestampArray&>(array);
          encoder.encodeLong(timestamp_array.Value(row_index));
          return {};
        }
        default:
          return InvalidArgument(
              "AVRO_LONG expects Int64Array, Time64Array, or TimestampArray, got {}",
              array.type()->ToString());
      }
    }

    case ::avro::AVRO_FLOAT: {
      const auto& float_array = internal::checked_cast<const ::arrow::FloatArray&>(array);
      encoder.encodeFloat(float_array.Value(row_index));
      return {};
    }

    case ::avro::AVRO_DOUBLE: {
      const auto& double_array =
          internal::checked_cast<const ::arrow::DoubleArray&>(array);
      encoder.encodeDouble(double_array.Value(row_index));
      return {};
    }

    case ::avro::AVRO_STRING: {
      const auto& string_array =
          internal::checked_cast<const ::arrow::StringArray&>(array);
      std::string_view value = string_array.GetView(row_index);
      encoder.encodeString(std::string(value));
      return {};
    }

    case ::avro::AVRO_BYTES: {
      const auto& binary_array =
          internal::checked_cast<const ::arrow::BinaryArray&>(array);
      std::string_view value = binary_array.GetView(row_index);
      ctx.bytes_scratch.assign(value.begin(), value.end());
      encoder.encodeBytes(ctx.bytes_scratch);
      return {};
    }

    case ::avro::AVRO_FIXED: {
      // Handle UUID
      if (avro_node->logicalType().type() == ::avro::LogicalType::UUID) {
        const auto& extension_array =
            internal::checked_cast<const ::arrow::ExtensionArray&>(array);
        const auto& fixed_array =
            internal::checked_cast<const ::arrow::FixedSizeBinaryArray&>(
                *extension_array.storage());
        std::string_view value = fixed_array.GetView(row_index);
        ctx.bytes_scratch.assign(value.begin(), value.end());
        encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
        return {};
      }

      // Handle DECIMAL
      if (avro_node->logicalType().type() == ::avro::LogicalType::DECIMAL) {
        const auto& decimal_array =
            internal::checked_cast<const ::arrow::Decimal128Array&>(array);
        std::string_view decimal_value = decimal_array.GetView(row_index);
        ctx.bytes_scratch.assign(decimal_value.begin(), decimal_value.end());
        // Arrow Decimal128 bytes are in little-endian order, Avro requires big-endian
        std::ranges::reverse(ctx.bytes_scratch);
        encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
        return {};
      }

      // Handle regular FIXED
      const auto& fixed_array =
          internal::checked_cast<const ::arrow::FixedSizeBinaryArray&>(array);
      std::string_view value = fixed_array.GetView(row_index);
      ctx.bytes_scratch.assign(value.begin(), value.end());
      encoder.encodeFixed(ctx.bytes_scratch.data(), ctx.bytes_scratch.size());
      return {};
    }

    case ::avro::AVRO_RECORD: {
      ICEBERG_PRECHECK(array.type()->id() == ::arrow::Type::STRUCT,
                       "AVRO_RECORD expects StructArray, got {}",
                       array.type()->ToString());
      ICEBERG_PRECHECK(type.type_id() == TypeId::kStruct,
                       "AVRO_RECORD expects struct type, got type {}", type.ToString());

      const auto& struct_array =
          internal::checked_cast<const ::arrow::StructArray&>(array);
      const auto& struct_type = internal::checked_cast<const StructType&>(type);
      const size_t num_fields = avro_node->leaves();

      ICEBERG_PRECHECK(
          struct_array.num_fields() == static_cast<int>(num_fields),
          "Field count mismatch: Arrow struct has {} fields, Avro node has {} fields",
          struct_array.num_fields(), num_fields);
      ICEBERG_PRECHECK(
          struct_type.fields().size() == num_fields,
          "Field count mismatch: Iceberg struct has {} fields, Avro node has {} fields",
          struct_type.fields().size(), num_fields);

      for (size_t i = 0; i < num_fields; ++i) {
        const auto& field_node = avro_node->leafAt(i);
        const auto& field_array = struct_array.field(static_cast<int>(i));
        const auto& field_schema = struct_type.fields()[i];

        ICEBERG_RETURN_UNEXPECTED(EncodeArrowToAvro(
            field_node, encoder, *field_schema.type(), *field_array, row_index, ctx));
      }
      return {};
    }

    case ::avro::AVRO_ARRAY: {
      const auto& element_node = avro_node->leafAt(0);

      // Handle ListArray
      if (array.type()->id() == ::arrow::Type::LIST) {
        const auto& list_array = internal::checked_cast<const ::arrow::ListArray&>(array);
        const auto& list_type = internal::checked_cast<const ListType&>(type);

        const auto start = list_array.value_offset(row_index);
        const auto end = list_array.value_offset(row_index + 1);
        const auto length = end - start;

        encoder.arrayStart();
        if (length > 0) {
          encoder.setItemCount(length);
          const auto& values = list_array.values();
          const auto& element_type = *list_type.fields()[0].type();

          for (int64_t i = start; i < end; ++i) {
            encoder.startItem();
            ICEBERG_RETURN_UNEXPECTED(
                EncodeArrowToAvro(element_node, encoder, element_type, *values, i, ctx));
          }
        }
        encoder.arrayEnd();
        return {};
      }

      // Handle MapArray (for Avro maps with non-string keys)
      if (array.type()->id() == ::arrow::Type::MAP) {
        ICEBERG_PRECHECK(
            element_node->type() == ::avro::AVRO_RECORD && element_node->leaves() == 2,
            "Expected AVRO_RECORD for map key-value pair, got {}",
            ::avro::toString(element_node->type()));

        const auto& map_array = internal::checked_cast<const ::arrow::MapArray&>(array);
        const auto& map_type = internal::checked_cast<const MapType&>(type);

        const auto start = map_array.value_offset(row_index);
        const auto end = map_array.value_offset(row_index + 1);
        const auto length = end - start;

        encoder.arrayStart();
        if (length > 0) {
          encoder.setItemCount(length);
          const auto& keys = map_array.keys();
          const auto& values = map_array.items();
          const auto& key_type = *map_type.key().type();
          const auto& value_type = *map_type.value().type();

          // The element_node should be a RECORD with "key" and "value" fields
          for (int64_t i = start; i < end; ++i) {
            const auto& key_node = element_node->leafAt(0);
            const auto& value_node = element_node->leafAt(1);

            encoder.startItem();
            ICEBERG_RETURN_UNEXPECTED(
                EncodeArrowToAvro(key_node, encoder, key_type, *keys, i, ctx));
            ICEBERG_RETURN_UNEXPECTED(
                EncodeArrowToAvro(value_node, encoder, value_type, *values, i, ctx));
          }
        }
        encoder.arrayEnd();
        return {};
      }

      return InvalidArgument("AVRO_ARRAY must map to ListArray or MapArray, got {}",
                             array.type()->ToString());
    }

    case ::avro::AVRO_MAP: {
      ICEBERG_PRECHECK(array.type()->id() == ::arrow::Type::MAP,
                       "AVRO_MAP expects MapArray, got {}", array.type()->ToString());
      ICEBERG_PRECHECK(type.type_id() == TypeId::kMap,
                       "AVRO_MAP expects MapType, got type {}", type.ToString());

      const auto& map_array = internal::checked_cast<const ::arrow::MapArray&>(array);
      const auto& map_type = internal::checked_cast<const MapType&>(type);

      const auto start = map_array.value_offset(row_index);
      const auto end = map_array.value_offset(row_index + 1);
      const auto length = end - start;

      encoder.mapStart();
      if (length > 0) {
        encoder.setItemCount(length);
        const auto& keys = map_array.keys();
        const auto& values = map_array.items();
        const auto& value_type = *map_type.value().type();
        const auto& value_node = avro_node->leafAt(1);

        ICEBERG_PRECHECK(keys->type()->id() == ::arrow::Type::STRING ||
                             keys->type()->id() == ::arrow::Type::LARGE_STRING,
                         "AVRO_MAP keys must be StringArray, got {}",
                         keys->type()->ToString());

        for (int64_t i = start; i < end; ++i) {
          encoder.startItem();

          if (keys->type()->id() == ::arrow::Type::STRING) {
            const auto& string_array =
                internal::checked_cast<const ::arrow::StringArray&>(*keys);
            std::string_view key_value = string_array.GetView(i);
            encoder.encodeString(std::string(key_value));
          } else {
            const auto& large_string_array =
                internal::checked_cast<const ::arrow::LargeStringArray&>(*keys);
            std::string_view key_value = large_string_array.GetView(i);
            encoder.encodeString(std::string(key_value));
          }

          ICEBERG_RETURN_UNEXPECTED(
              EncodeArrowToAvro(value_node, encoder, value_type, *values, i, ctx));
        }
      }
      encoder.mapEnd();
      return {};
    }

    case ::avro::AVRO_ENUM:
      return NotSupported("ENUM type encoding not yet implemented");

    case ::avro::AVRO_UNION:
      // Already handled above
      return Invalid("Unexpected union handling");

    default:
      return NotSupported("Unsupported Avro type: {}",
                          ::avro::toString(avro_node->type()));
  }
}

}  // namespace iceberg::avro
