///|
/// SQL text for a bulk insert: `INSERT INTO  () FORMAT `.
/// Identifiers are interpolated verbatim — quote them yourself if needed.
fn build_insert_sql(
  table : String,
  columns : Array[String],
  format : InsertFormat,
) -> String {
  let buf = Buffer::Buffer(size_hint=64)
  buf.write_bytes(@utf8.encode("INSERT INTO " + table + " ("))
  for i in 0.. 0 {
      buf.write_bytes(@utf8.encode(", "))
    }
    buf.write_bytes(@utf8.encode(columns[i]))
  }
  buf.write_bytes(@utf8.encode(") FORMAT "))
  buf.write_bytes(@utf8.encode(format_name(format)))
  @utf8.decode_lossy(buf.to_bytes())
}

///|
/// ClickHouse FORMAT keyword for an InsertFormat.
fn format_name(format : InsertFormat) -> String {
  match format {
    Tsv => "TSV"
    Ndjson => "JSONEachRow"
  }
}

///|
/// Encode a single row for the given wire format.
fn encode_row(
  columns : Array[String],
  row : Array[String],
  format : InsertFormat,
) -> Bytes {
  let buf = Buffer::Buffer(size_hint=32)
  match format {
    Tsv => {
      let mut first = true
      for v in row {
        if !first {
          buf.write_byte(b'\t')
        }
        buf.write_bytes(@utf8.encode(tsv_escape(v)))
        first = false
      }
      buf.write_byte(b'\n')
    }
    Ndjson => {
      buf.write_byte(b'{')
      for i in 0.. 0 {
          buf.write_byte(b',')
        }
        buf.write_byte(b'"')
        buf.write_bytes(@utf8.encode(json_escape(columns[i])))
        buf.write_bytes(@utf8.encode("\":"))
        let value = if i < row.length() { row[i] } else { "" }
        buf.write_byte(b'"')
        buf.write_bytes(@utf8.encode(json_escape(value)))
        buf.write_byte(b'"')
      }
      buf.write_byte(b'}')
      buf.write_byte(b'\n')
    }
  }
  buf.to_bytes()
}

///|
/// Escape a TSV field: `\`, tab, LF and CR become backslash sequences.
fn tsv_escape(s : String) -> String {
  let bytes = @utf8.encode(s)
  let buf = Buffer::Buffer(size_hint=bytes.length() + 8)
  for b in bytes {
    match b {
      b'\\' => buf.write_bytes(@utf8.encode("\\\\"))
      b'\t' => buf.write_bytes(@utf8.encode("\\t"))
      b'\n' => buf.write_bytes(@utf8.encode("\\n"))
      b'\r' => buf.write_bytes(@utf8.encode("\\r"))
      _ => buf.write_byte(b)
    }
  }
  @utf8.decode_lossy(buf.to_bytes())
}

///|
/// Escape a string as the inside of a JSON string literal.
fn json_escape(s : String) -> String {
  let bytes = @utf8.encode(s)
  let buf = Buffer::Buffer(size_hint=bytes.length() + 8)
  for b in bytes {
    match b {
      b'"' => buf.write_bytes(@utf8.encode("\\\""))
      b'\\' => buf.write_bytes(@utf8.encode("\\\\"))
      b'\n' => buf.write_bytes(@utf8.encode("\\n"))
      b'\r' => buf.write_bytes(@utf8.encode("\\r"))
      b'\t' => buf.write_bytes(@utf8.encode("\\t"))
      b'\x00'.. {
        let code = b.to_int()
        buf.write_bytes(@utf8.encode("\\u00"))
        buf.write_byte(hex_nibble_byte(code / 16))
        buf.write_byte(hex_nibble_byte(code % 16))
      }
      _ => buf.write_byte(b)
    }
  }
  @utf8.decode_lossy(buf.to_bytes())
}

///|
/// Parse a TabSeparatedWithNamesAndTypes response body.
///
/// Layout:
///   line 0: column names tab-separated
///   line 1: column types tab-separated
///   line 2..: data rows tab-separated
fn parse_tsv_response(body : String) -> ResultSet {
  let lines = split_lines(body)
  if lines.length() < 2 {
    return { columns: [], rows: [], }
  }
  let names = split_tsv_line(lines[0])
  let types = split_tsv_line(lines[1])
  let columns : Array[Column] = []
  for i in 0.. 0 {
      let values = split_tsv_line(lines[i])
      rows.push({ values, })
    }
  }
  { columns, rows, }
}

///|
/// Split a string by lines. Handles both `\n` and `\r\n` line endings.
fn split_lines(s : String) -> Array[String] {
  let result : Array[String] = []
  let bytes = @utf8.encode(s)
  let mut start = 0
  for i in 0.. 0 && bytes[i - 1] == b'\r' { i - 1 } else { i }
      result.push(bytes_to_string(bytes, start, end))
      start = i + 1
    }
  }
  if start < bytes.length() {
    let end = if bytes.length() > start && bytes[bytes.length() - 1] == b'\r' {
      bytes.length() - 1
    } else {
      bytes.length()
    }
    if end > start {
      result.push(bytes_to_string(bytes, start, end))
    }
  }
  ignore(s)
  result
}

///|
/// Strip a single trailing carriage return (for CRLF-terminated lines).
fn strip_cr(s : String) -> String {
  if s.length() > 0 && s[s.length() - 1] == '\r' {
    substr(s, 0, s.length() - 1)
  } else {
    s
  }
}

///|
/// Split a TSV line by tab. Unescapes `\t`, `\n`, `\r`, `\\`.
fn split_tsv_line(line : String) -> Array[String] {
  let result : Array[String] = []
  let bytes = @utf8.encode(line)
  let buf = Buffer::Buffer(size_hint=line.length())
  let mut i = 0
  while i < bytes.length() {
    let b = bytes[i]
    if b == b'\\' && i + 1 < bytes.length() {
      let next = bytes[i + 1]
      match next {
        b'n' => buf.write_byte(b'\n')
        b'r' => buf.write_byte(b'\r')
        b't' => buf.write_byte(b'\t')
        b'\\' => buf.write_byte(b'\\')
        _ => {
          buf.write_byte(b)
          buf.write_byte(next)
        }
      }
      i = i + 2
    } else if b == b'\t' {
      result.push(buf_to_string(buf))
      buf.reset()
      i = i + 1
    } else {
      buf.write_byte(b)
      i = i + 1
    }
  }
  // Always push final field, even if empty (handles trailing-tab case)
  result.push(buf_to_string(buf))
  ignore(line)
  result
}

///|
fn hex_nibble_byte(v : Int32) -> Byte {
  if v < 10 {
    (v + 0x30).to_byte()
  } else {
    (v + 0x61 - 10).to_byte()
  }
}