///| Receive-pack (server-side) implementation

///|
pub(all) struct RefUpdate {
  old_id : @bit.ObjectId
  new_id : @bit.ObjectId
  refname : String
}

///|
pub(all) struct ReceivePackRequest {
  updates : Array[RefUpdate]
  capabilities : Array[String]
  pack : Bytes
  push_options : Array[String]
  cert_push_options : Array[String]
  has_push_cert : Bool
}

///|
pub(all) struct ReceivePackResult {
  updated : Array[String]
  rejected : Array[(String, String)]
  unpack_error : String?
  connectivity_error_oid : String?
}

///|
/// Result from a proc-receive hook for a single ref.
pub(all) struct ReceivePackProcResult {
  refname : String
  original_refname : String
  old_id : String
  new_id : String
  status : String
  message : String
  fall_through : Bool
}

///|
/// Build receive-pack advertisement (info/refs payload).
pub fn build_receive_pack_advertisement(
  fs : &@bit.RepoFileSystem,
  root : String,
  agent? : String = "git/moonbit",
  protocol_version? : Int = 0,
  extra_haves? : Array[@bit.ObjectId] = [],
  git_dir_override? : String = "",
  advertise_push_options? : Bool = false,
  cert_nonce? : String = "",
) -> Bytes raise @bit.GitError {
  let git_dir = if git_dir_override.length() > 0 {
    git_dir_override
  } else {
    join_path(root, ".git")
  }
  let refs = show_ref(fs, git_dir)
  refs.sort_by((a, b) => String::compare(a.0, b.0))
  let caps = receive_default_caps(agent, advertise_push_options~, cert_nonce~)
  let out : Array[Byte] = []
  // Protocol v1: output version line before refs
  // (receive-pack has no v2, so v2 falls back to v0)
  if protocol_version == 1 {
    receive_push_bytes(out, @protocol.pktline_encode("version 1\n"))
  }
  if refs.length() == 0 {
    let zero = @bit.ObjectId::zero().to_hex()
    let line = "\{zero} capabilities^{}\u0000\{caps}\n"
    receive_push_bytes(out, @protocol.pktline_encode(line))
  } else {
    let (first_name, first_id) = refs[0]
    let first_line = "\{first_id.to_hex()} \{first_name}\u0000\{caps}\n"
    receive_push_bytes(out, @protocol.pktline_encode(first_line))
    for i in 1.. out[i]))
}

///|
fn normalize_receive_refname(refname : String) -> String raise @bit.GitError {
  let normalized = normalize_repo_path(refname) catch {
    _ =>
      raise @bit.GitError::ProtocolError("Invalid receive-pack ref: " + refname)
  }
  if !normalized.has_prefix("refs/") {
    raise @bit.GitError::ProtocolError("Invalid receive-pack ref: " + refname)
  }
  normalized
}

///|
pub fn parse_receive_pack_request(
  data : Bytes,
) -> ReceivePackRequest raise @bit.GitError {
  let updates : Array[RefUpdate] = []
  let capabilities : Array[String] = []
  let cert_push_options : Array[String] = []
  let mut has_push_cert = false
  let mut i = 0
  let mut first = true
  let mut in_cert = false
  let cert_lines : Array[String] = []
  while i + 4 <= data.length() {
    let len = receive_parse_pkt_len(data, i)
    if len == 0 {
      i += 4
      break
    }
    if len < 4 || i + len > data.length() {
      raise @bit.GitError::ProtocolError("Invalid pkt-line length")
    }
    let line = receive_bytes_to_string_range(data, i + 4, i + len)
    let mut line = receive_trim_line(line)
    if in_cert {
      if line == "push-cert-end" {
        in_cert = false
        // Parse certificate content for ref updates and push-options
        receive_parse_cert_content(cert_lines, updates, cert_push_options)
      } else {
        cert_lines.push(line)
      }
      i += len
      continue
    }
    // Check for push-cert start
    let is_push_cert = line == "push-cert" || line.has_prefix("push-cert\u0000")
    if is_push_cert {
      has_push_cert = true
      in_cert = true
      // Extract capabilities from push-cert line (after NUL)
      if first {
        first = false
        match line.find("\u0000") {
          None => ()
          Some(idx) => {
            let caps_str = String::unsafe_substring(
              line,
              start=idx + 1,
              end=line.length(),
            )
            let caps = receive_split_by_space(caps_str)
            for c in caps {
              if c.length() > 0 {
                capabilities.push(c)
              }
            }
          }
        }
      }
      i += len
      continue
    }
    // Normal ref update command
    if first {
      first = false
      match line.find("\u0000") {
        None => ()
        Some(idx) => {
          let caps_str = String::unsafe_substring(
            line,
            start=idx + 1,
            end=line.length(),
          )
          line = String::unsafe_substring(line, start=0, end=idx)
          let caps = receive_split_by_space(caps_str)
          for c in caps {
            if c.length() > 0 {
              capabilities.push(c)
            }
          }
        }
      }
    }
    let parts = receive_split_by_space(line)
    if parts.length() < 3 {
      raise @bit.GitError::ProtocolError("Invalid receive-pack command")
    }
    let old_id = @bit.ObjectId::from_hex(parts[0])
    let new_id = @bit.ObjectId::from_hex(parts[1])
    let refname = normalize_receive_refname(parts[2])
    updates.push({ old_id, new_id, refname })
    i += len
  }
  // Phase 2: Read push-options after flush (if push-options capability is negotiated)
  let push_options : Array[String] = []
  let mut has_push_options_cap = false
  for cap in capabilities {
    if cap == "push-options" {
      has_push_options_cap = true
      break
    }
  }
  if has_push_options_cap && i + 4 <= data.length() {
    while i + 4 <= data.length() {
      let len = receive_parse_pkt_len(data, i)
      if len == 0 {
        i += 4
        break
      }
      if len < 4 || i + len > data.length() {
        break
      }
      let line = receive_bytes_to_string_range(data, i + 4, i + len)
      let line = receive_trim_line(line)
      push_options.push(line)
      i += len
    }
  }
  // Phase 3: Remaining data is pack
  let pack = receive_bytes_slice(data, i, data.length())
  {
    updates,
    capabilities,
    pack,
    push_options,
    cert_push_options,
    has_push_cert,
  }
}

///|
/// Parse push certificate content to extract ref updates and push-options.
fn receive_parse_cert_content(
  cert_lines : Array[String],
  updates : Array[RefUpdate],
  cert_push_options : Array[String],
) -> Unit {
  let mut in_body = false
  let mut in_sig = false
  for cert_line in cert_lines {
    if in_sig {
      continue
    }
    if !in_body {
      if cert_line.length() == 0 {
        in_body = true
        continue
      }
      if cert_line.has_prefix("push-option ") {
        let opt = String::unsafe_substring(
          cert_line,
          start=12,
          end=cert_line.length(),
        )
        cert_push_options.push(opt)
      }
      continue
    }
    // In body: ref updates and GPG signature
    if cert_line.has_prefix("-----BEGIN ") {
      in_sig = true
      continue
    }
    // Parse ref update line:   
    let parts = receive_split_by_space(cert_line)
    if parts.length() >= 3 &&
      (parts[0].length() == 40 || parts[0].length() == 64) {
      let old_id = @bit.ObjectId::from_hex(parts[0]) catch { _ => continue }
      let new_id = @bit.ObjectId::from_hex(parts[1]) catch { _ => continue }
      let refname = normalize_receive_refname(parts[2]) catch { _ => continue }
      updates.push({ old_id, new_id, refname })
    }
  }
}

///|
pub fn apply_receive_pack(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  req : ReceivePackRequest,
  skip_connectivity_check? : Bool = false,
  git_dir_override? : String = "",
) -> ReceivePackResult raise @bit.GitError {
  let git_dir = if git_dir_override.length() > 0 {
    git_dir_override
  } else {
    join_path(root, ".git")
  }
  // Check push-option inconsistency (cert vs protocol push-options)
  if req.has_push_cert && req.push_options.length() > 0 {
    let mut inconsistent = false
    if req.cert_push_options.length() != req.push_options.length() {
      inconsistent = true
    } else {
      for i in 0.. 0 {
    pack_objects = @pack.parse_packfile(req.pack)
  }
  // Connectivity check BEFORE writing pack to disk
  if !skip_connectivity_check && pack_objects.length() > 0 {
    let fail_oid = receive_connectivity_check(rfs, git_dir, pack_objects)
    if fail_oid is Some(oid) {
      return {
        updated: [],
        rejected: [],
        unpack_error: Some("missing necessary objects"),
        connectivity_error_oid: Some(oid),
      }
    }
  }
  // Write pack after connectivity check passes
  if req.pack.length() > 0 && pack_objects.length() > 0 {
    @pack.write_packfile_with_index(fs, git_dir, req.pack, pack_objects)
  }
  let db = ObjectDb::load(rfs, git_dir)
  let updated : Array[String] = []
  let rejected : Array[(String, String)] = []
  for upd in req.updates {
    let refname = normalize_receive_refname(upd.refname) catch {
      _ => {
        rejected.push((upd.refname, "invalid refname"))
        continue
      }
    }
    let current = resolve_ref(rfs, git_dir, refname)
    let current_id = match current {
      None => @bit.ObjectId::zero()
      Some(id) => id
    }
    if current_id != upd.old_id {
      rejected.push((refname, "old id mismatch"))
      continue
    }
    if refname.has_prefix("refs/heads/") {
      if current_id != @bit.ObjectId::zero() &&
        upd.new_id != @bit.ObjectId::zero() {
        let ff = receive_is_ancestor_commit(db, rfs, current_id, upd.new_id) catch {
          _ => false
        }
        if !ff {
          rejected.push((refname, "non-fast-forward"))
          continue
        }
      }
    }
    if upd.new_id == @bit.ObjectId::zero() {
      if !receive_delete_ref(fs, rfs, git_dir, refname) {
        rejected.push((refname, "ref not found"))
        continue
      }
    } else {
      let ref_path = join_path(git_dir, refname)
      let dir = parent_dir(ref_path)
      fs.mkdir_p(dir)
      fs.write_string(ref_path, upd.new_id.to_hex() + "\n")
    }
    updated.push(refname)
  }
  { updated, rejected, unpack_error: None, connectivity_error_oid: None }
}

///|
pub fn receive_pack(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  data : Bytes,
  skip_connectivity_check? : Bool = false,
  git_dir_override? : String = "",
) -> Bytes raise @bit.GitError {
  let req = parse_receive_pack_request(data)
  let result = apply_receive_pack(
    fs,
    rfs,
    root,
    req,
    skip_connectivity_check~,
    git_dir_override~,
  )
  receive_build_status_response(result)
}

///|
pub fn receive_build_status_response(result : ReceivePackResult) -> Bytes {
  receive_build_status_response_with_proc(result, [])
}

///|
/// Build receive-pack status response including proc-receive results.
/// When original_updates is provided, output status lines in the original
/// push order (matching git's behavior). Otherwise fall back to the
/// legacy order (updated, rejected, proc).
pub fn receive_build_status_response_with_proc(
  result : ReceivePackResult,
  proc_results : Array[ReceivePackProcResult],
  original_updates? : Array[RefUpdate] = [],
) -> Bytes {
  let out : Array[Byte] = []
  let unpack_line = match result.unpack_error {
    Some(msg) => "unpack \{msg}\n"
    None => "unpack ok\n"
  }
  receive_push_bytes(out, @protocol.pktline_encode(unpack_line))
  if original_updates.length() > 0 {
    // Output status in original push order
    let seen : Map[String, Bool] = Map([])
    for upd in original_updates {
      let refname = upd.refname
      // Check if this ref was updated (regular or fall-through)
      if result.updated.contains(refname) {
        receive_push_bytes(out, @protocol.pktline_encode("ok \{refname}\n"))
        continue
      }
      // Check if this ref was rejected
      let mut found_rejected = false
      for item in result.rejected {
        let (rname, msg) = item
        if rname == refname {
          receive_push_bytes(
            out,
            @protocol.pktline_encode("ng \{refname} \{msg}\n"),
          )
          found_rejected = true
          break
        }
      }
      if found_rejected {
        continue
      }
      // Check proc-receive results by original_refname
      // report-status v1: one line per original refname using the original name
      if seen.contains(refname) {
        continue
      }
      // Find first proc result for this original ref
      for pr in proc_results {
        if pr.original_refname == refname {
          seen[refname] = true
          if pr.status == "ok" {
            receive_push_bytes(out, @protocol.pktline_encode("ok \{refname}\n"))
          } else {
            let msg = if pr.message.length() > 0 {
              pr.message
            } else {
              "failed"
            }
            receive_push_bytes(
              out,
              @protocol.pktline_encode("ng \{refname} \{msg}\n"),
            )
          }
          break
        }
      }
    }
  } else {
    // Legacy: output in result order
    for refname in result.updated {
      receive_push_bytes(out, @protocol.pktline_encode("ok \{refname}\n"))
    }
    for item in result.rejected {
      let (refname, msg) = item
      receive_push_bytes(
        out,
        @protocol.pktline_encode("ng \{refname} \{msg}\n"),
      )
    }
    for pr in proc_results {
      if pr.status == "ok" {
        receive_push_bytes(out, @protocol.pktline_encode("ok \{pr.refname}\n"))
      } else {
        let msg = if pr.message.length() > 0 { pr.message } else { "failed" }
        receive_push_bytes(
          out,
          @protocol.pktline_encode("ng \{pr.refname} \{msg}\n"),
        )
      }
    }
  }
  receive_push_bytes(out, @protocol.pktline_flush())
  Bytes::from_array(FixedArray::makei(out.length(), i => out[i]))
}

///|
fn receive_is_ancestor_commit(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  ancestor : @bit.ObjectId,
  commit_id : @bit.ObjectId,
) -> Bool raise @bit.GitError {
  if ancestor == commit_id {
    return true
  }
  let stack : Array[@bit.ObjectId] = [commit_id]
  let seen : Map[String, Bool] = Map([])
  while stack.length() > 0 {
    let id = match stack.pop() {
      None => raise @bit.GitError::InvalidObject("Empty stack")
      Some(v) => v
    }
    let hex = id.to_hex()
    if seen.contains(hex) {
      continue
    }
    seen[hex] = true
    if id == ancestor {
      return true
    }
    let parents = receive_commit_parents(db, fs, id)
    for p in parents {
      stack.push(p)
    }
  }
  false
}

///|
fn receive_commit_parents(
  db : ObjectDb,
  fs : &@bit.RepoFileSystem,
  commit_id : @bit.ObjectId,
) -> Array[@bit.ObjectId] raise @bit.GitError {
  let obj = db.get(fs, commit_id)
  match obj {
    None => raise @bit.GitError::InvalidObject("Missing commit object")
    Some(o) => {
      if o.obj_type != @bit.ObjectType::Commit {
        raise @bit.GitError::InvalidObject("Object is not a commit")
      }
      let info = @bit.parse_commit(o.data)
      info.parents
    }
  }
}

///|
fn receive_delete_ref(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  refname : String,
) -> Bool raise @bit.GitError {
  let normalized = normalize_receive_refname(refname) catch {
    _ => return false
  }
  let ref_path = join_path(git_dir, normalized)
  if rfs.is_file(ref_path) {
    fs.remove_file(ref_path)
    return true
  }
  let packed = join_path(git_dir, "packed-refs")
  if !rfs.is_file(packed) {
    return false
  }
  let data = rfs.read_file(packed)
  let text = @utf8.decode_lossy(data[:])
  let entries = @bitrefs.parse_packed_refs_text(text)
  let kept = entries.filter(e => e.refname != normalized)
  let removed = kept.length() != entries.length()
  if removed {
    fs.write_string(packed, @bitrefs.serialize_packed_refs_entries(kept))
  }
  removed
}

///|
fn receive_parse_pkt_len(data : Bytes, start : Int) -> Int raise @bit.GitError {
  if start + 4 > data.length() {
    raise @bit.GitError::ProtocolError("Invalid pkt-line length")
  }
  let mut v = 0
  for i in 0..<4 {
    let c = data[start + i].to_int().unsafe_to_char()
    let digit = @bithash.hex_char_to_int(c) catch {
      _ => raise @bit.GitError::ProtocolError("Invalid pkt-line length")
    }
    v = v * 16 + digit
  }
  v
}

///|
fn receive_bytes_to_string_range(
  data : Bytes,
  start : Int,
  end : Int,
) -> String {
  let buf = StringBuilder::new()
  let mut i = start
  while i < end {
    buf.write_char(data[i].to_int().unsafe_to_char())
    i += 1
  }
  buf.to_string()
}

///|
fn receive_bytes_slice(data : Bytes, start : Int, end : Int) -> Bytes {
  let len = end - start
  if len <= 0 {
    return Bytes::from_array([])
  }
  let fixed = FixedArray::makei(len, i => data[start + i])
  Bytes::from_array(fixed)
}

///|
fn receive_trim_line(line : String) -> String {
  if line.has_suffix("\n") {
    String::unsafe_substring(line, start=0, end=line.length() - 1)
  } else {
    line
  }
}

///|
fn receive_split_by_space(s : String) -> Array[String] {
  let out : Array[String] = []
  let mut buf : Array[Char] = []
  for c in s {
    if c == ' ' {
      out.push(receive_chars_to_string(buf))
      buf = []
    } else {
      buf.push(c)
    }
  }
  out.push(receive_chars_to_string(buf))
  out
}

///|
fn receive_chars_to_string(chars : Array[Char]) -> String {
  let buf = StringBuilder::new()
  for c in chars {
    buf.write_char(c)
  }
  buf.to_string()
}

///|
fn receive_push_bytes(out : Array[Byte], data : Bytes) -> Unit {
  for b in data {
    out.push(b)
  }
}

///|
fn receive_default_caps(
  agent : String,
  advertise_push_options? : Bool = false,
  cert_nonce? : String = "",
) -> String {
  let caps : Array[String] = [
    "report-status",
    "report-status-v2",
    "delete-refs",
    "side-band-64k",
    "ofs-delta",
    "object-format=sha1",
    "agent=\{agent}",
  ]
  if advertise_push_options {
    caps.push("push-options")
  }
  if cert_nonce.length() > 0 {
    caps.push("push-cert=\{cert_nonce}")
  }
  caps.join(" ")
}

///|
/// Check connectivity of pack objects against existing repository.
/// Returns Some(commit_hex) on failure, None on success.
fn receive_connectivity_check(
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  pack_objects : Array[@bit.PackObject],
) -> String? raise @bit.GitError {
  let pack_ids : Map[String, Bool] = Map([])
  for obj in pack_objects {
    pack_ids[obj.id.to_hex()] = true
  }
  // Load existing repo object database
  let db = if rfs.is_dir(git_dir) {
    Some(ObjectDb::load(rfs, git_dir))
  } else {
    None
  }
  let repo_cache : Map[String, Bool] = Map([])
  for obj in pack_objects {
    if obj.obj_type == @bit.ObjectType::Commit {
      let info = @bit.parse_commit(obj.data)
      // Check parents exist
      for parent in info.parents {
        let p_hex = parent.to_hex()
        if !pack_ids.contains(p_hex) {
          if !receive_object_exists(rfs, db, repo_cache, parent) {
            return Some(obj.id.to_hex())
          }
        }
      }
      // Check tree exists
      let t_hex = info.tree.to_hex()
      if !pack_ids.contains(t_hex) {
        if !receive_object_exists(rfs, db, repo_cache, info.tree) {
          return Some(obj.id.to_hex())
        }
      }
    }
  }
  None
}

///|
fn receive_object_exists(
  rfs : &@bit.RepoFileSystem,
  db : ObjectDb?,
  cache : Map[String, Bool],
  id : @bit.ObjectId,
) -> Bool raise @bit.GitError {
  let hex = id.to_hex()
  if cache.contains(hex) {
    return cache[hex]
  }
  let exists = match db {
    None => false
    Some(objdb) =>
      match objdb.get(rfs, id) {
        Some(_) => true
        None => false
      }
  }
  cache[hex] = exists
  exists
}