///|
/// Reftable writer: creates reftable binary files from RefRecords.

///|
/// Write big-endian u16 to output.
fn push_be_u16(out : Array[Byte], value : Int) -> Unit {
  out.push(((value >> 8) & 0xff).to_byte())
  out.push((value & 0xff).to_byte())
}

///|
/// Write big-endian u24 (3 bytes) to output.
fn push_be_u24(out : Array[Byte], value : Int) -> Unit {
  out.push(((value >> 16) & 0xff).to_byte())
  out.push(((value >> 8) & 0xff).to_byte())
  out.push((value & 0xff).to_byte())
}

///|
/// Write big-endian u32 to output.
fn push_be_u32(out : Array[Byte], value : UInt) -> Unit {
  out.push(((value >> 24) & 0xffU).reinterpret_as_int().to_byte())
  out.push(((value >> 16) & 0xffU).reinterpret_as_int().to_byte())
  out.push(((value >> 8) & 0xffU).reinterpret_as_int().to_byte())
  out.push((value & 0xffU).reinterpret_as_int().to_byte())
}

///|
/// Write big-endian u64 to output.
fn push_be_u64(out : Array[Byte], value : UInt64) -> Unit {
  push_be_u32(out, (value >> 32).to_uint())
  push_be_u32(out, value.to_uint())
}

///|
/// Encode a single ref record into output bytes.
/// Returns nothing; appends encoded bytes to `out`.
fn encode_ref_record(
  out : Array[Byte],
  rec : RefRecord,
  prev_key : String,
  min_update_index : UInt64,
) -> Unit {
  // Compute shared prefix length
  let mut prefix_len = 0
  let min_len = if prev_key.length() < rec.refname.length() {
    prev_key.length()
  } else {
    rec.refname.length()
  }
  while prefix_len < min_len && prev_key[prefix_len] == rec.refname[prefix_len] {
    prefix_len += 1
  }
  let suffix_len = rec.refname.length() - prefix_len
  // value_type for the lower 3 bits
  let value_type : Int = match rec.value {
    Deletion => 0
    Val1(_) => 1
    Val2(_, _) => 2
    Symref(_) => 3
  }
  // Encode prefix length
  encode_varint(out, prefix_len.to_uint64())
  // Encode (suffix_len << 3) | value_type
  encode_varint(out, ((suffix_len << 3) | value_type).to_uint64())
  // Write suffix bytes
  for i = prefix_len; i < rec.refname.length(); i = i + 1 {
    out.push((rec.refname[i].to_int() & 0xff).to_byte())
  }
  // Write update_index delta
  let update_index_delta = rec.update_index - min_update_index
  encode_varint(out, update_index_delta)
  // Write value
  match rec.value {
    Deletion => ()
    Val1(oid) => {
      let bytes = oid.to_bytes()
      for i = 0; i < hash_size; i = i + 1 {
        out.push(bytes[i])
      }
    }
    Val2(oid1, oid2) => {
      let b1 = oid1.to_bytes()
      let b2 = oid2.to_bytes()
      for i = 0; i < hash_size; i = i + 1 {
        out.push(b1[i])
      }
      for i = 0; i < hash_size; i = i + 1 {
        out.push(b2[i])
      }
    }
    Symref(target) => {
      encode_varint(out, target.length().to_uint64())
      for i = 0; i < target.length(); i = i + 1 {
        out.push((target[i].to_int() & 0xff).to_byte())
      }
    }
  }
}

///|
/// Write a reftable header to output.
fn write_header(
  out : Array[Byte],
  block_size : Int,
  min_update_index : UInt64,
  max_update_index : UInt64,
) -> Unit {
  // Magic "REFT"
  out.push(b'R')
  out.push(b'E')
  out.push(b'F')
  out.push(b'T')
  // Version
  out.push(reftable_version.to_byte())
  // Block size (24-bit BE)
  push_be_u24(out, block_size)
  // Min/max update index
  push_be_u64(out, min_update_index)
  push_be_u64(out, max_update_index)
}

///|
/// Write a reftable footer to output.
fn write_footer(
  out : Array[Byte],
  block_size : Int,
  min_update_index : UInt64,
  max_update_index : UInt64,
) -> Unit {
  // Footer starts with a copy of the header
  write_header(out, block_size, min_update_index, max_update_index)
  // ref_index_offset (0 = none)
  push_be_u64(out, 0UL)
  // obj_offset (0 = none)
  push_be_u64(out, 0UL)
  // obj_index_offset (0 = none)
  push_be_u64(out, 0UL)
  // log_offset (0 = none)
  push_be_u64(out, 0UL)
  // log_index_offset (0 = none)
  push_be_u64(out, 0UL)
  // CRC32 over footer bytes (all except the CRC32 field itself)
  let footer_start = out.length() - 64
  let crc = crc32_array(out, footer_start, out.length())
  push_be_u32(out, crc)
}

///|
/// Write a complete reftable file from sorted ref records.
/// Records MUST be sorted by refname.
/// Returns the file contents as Bytes.
pub fn write_reftable(
  records : Array[RefRecord],
  block_size : Int,
  min_update_index : UInt64,
  max_update_index : UInt64,
) -> Bytes {
  let out : Array[Byte] = []
  let effective_block_size = if block_size == 0 {
    default_block_size
  } else {
    block_size
  }
  // Write file header
  write_header(out, effective_block_size, min_update_index, max_update_index)
  if records.length() > 0 {
    // Build ref blocks
    let mut block_records : Array[Byte] = []
    let mut restart_offsets : Array[Int] = []
    let mut prev_key = ""
    let mut record_count = 0
    let block_header_size = 4
    let restart_overhead_per = 3 // 3 bytes per restart offset
    let restart_count_size = 2 // 2 bytes for count
    for i, rec in records {
      let rec_out : Array[Byte] = []
      let current_prev = if record_count % default_restart_interval == 0 {
        // Restart point: no prefix compression
        ""
      } else {
        prev_key
      }
      if record_count % default_restart_interval == 0 {
        restart_offsets.push(block_records.length())
      }
      encode_ref_record(rec_out, rec, current_prev, min_update_index)
      // Check if adding this record would exceed block size
      let projected_size = block_header_size +
        block_records.length() +
        rec_out.length() +
        restart_offsets.length() * restart_overhead_per +
        restart_count_size
      if projected_size > effective_block_size && block_records.length() > 0 {
        // Flush current block
        flush_ref_block(
          out, block_records, restart_offsets, effective_block_size,
        )
        // Start new block
        block_records = []
        restart_offsets = []
        prev_key = ""
        record_count = 0
        // Re-encode the current record with no prefix compression
        let new_rec_out : Array[Byte] = []
        restart_offsets.push(0)
        encode_ref_record(new_rec_out, rec, "", min_update_index)
        for b in new_rec_out {
          block_records.push(b)
        }
        prev_key = rec.refname
        record_count = 1
      } else {
        for b in rec_out {
          block_records.push(b)
        }
        prev_key = rec.refname
        record_count += 1
      }
      // Flush on last record
      if i == records.length() - 1 && block_records.length() > 0 {
        flush_ref_block(
          out, block_records, restart_offsets, effective_block_size,
        )
      }
    }
  }
  // Write footer
  write_footer(out, effective_block_size, min_update_index, max_update_index)
  Bytes::from_array(FixedArray::makei(out.length(), fn(i) { out[i] }))
}

///|
/// Flush a completed ref block to output, padded to block_size.
fn flush_ref_block(
  out : Array[Byte],
  records : Array[Byte],
  restart_offsets : Array[Int],
  block_size : Int,
) -> Unit {
  // Calculate total block content size
  let content_size = 4 + records.length() + restart_offsets.length() * 3 + 2
  // Write block header
  out.push(block_type_ref)
  push_be_u24(out, content_size)
  // Write record data
  for b in records {
    out.push(b)
  }
  // Write restart offsets (3 bytes BE each)
  for offset in restart_offsets {
    push_be_u24(out, offset)
  }
  // Write restart count (2 bytes BE)
  push_be_u16(out, restart_offsets.length())
  // Pad to block_size
  let current_size = content_size
  if current_size < block_size {
    for _i = current_size; _i < block_size; _i = _i + 1 {
      out.push(b'\x00')
    }
  }
}