///|
pub fn v2_signature() -> Bytes {
  Bytes::from_array([
    b'\r', b'\n', b'\r', b'\n', b'\x00', b'\r', b'\n', b'Q', b'U', b'I', b'T', b'\n',
  ])
}

///|
fn parse_v2_command(value : Int) -> Result[Command, ProxyError] {
  if value == 0 {
    Ok(Local)
  } else if value == 1 {
    Ok(Proxy)
  } else {
    Err(proxy_error(InvalidCommand, 12, "v2 command nibble must be 0 or 1"))
  }
}

///|
fn parse_v2_family(value : Int) -> Result[AddressFamily, ProxyError] {
  if value == 0 {
    Ok(Unspec)
  } else if value == 1 {
    Ok(Inet)
  } else if value == 2 {
    Ok(Inet6)
  } else if value == 3 {
    Ok(Unix)
  } else {
    Err(proxy_error(InvalidFamily, 13, "unsupported v2 address family"))
  }
}

///|
fn parse_v2_transport(value : Int) -> Result[Transport, ProxyError] {
  if value == 0 {
    Ok(Unspec)
  } else if value == 1 {
    Ok(Stream)
  } else if value == 2 {
    Ok(Datagram)
  } else {
    Err(proxy_error(InvalidTransport, 13, "unsupported v2 transport"))
  }
}

///|
fn parse_v2_proxy_body(
  family : AddressFamily,
  transport : Transport,
  body : Bytes,
  policy : DecodePolicy,
) -> Result[(AddressBlock, Array[RawTlv]), ProxyError] {
  if family == Unspec {
    if transport != Unspec {
      return Err(
        proxy_error(
          InvalidFamilyTransport,
          13,
          "UNSPEC family requires UNSPEC transport",
        ),
      )
    }
    if !policy.allow_unspec {
      return Err(
        proxy_error(PolicyViolation, 13, "UNSPEC is disabled by policy"),
      )
    }
    let raw = if body.length() == 0 { NoAddress } else { RawUnsupported(body) }
    return Ok((raw, []))
  }
  if transport == Unspec {
    return Err(
      proxy_error(
        InvalidFamilyTransport,
        13,
        "non-UNSPEC family requires STREAM or DGRAM",
      ),
    )
  }
  let needed = if family == Inet {
    12
  } else if family == Inet6 {
    36
  } else {
    216
  }
  if body.length() < needed {
    return Err(
      proxy_error(
        TruncatedAddress,
        16,
        "v2 address block is shorter than its family minimum",
      ),
    )
  }
  let address = if family == Inet {
    let src = match
      Ipv4Address::new(Bytes::from_array(body.to_array()[0:4]).to_fixedarray()) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    let dst = match
      Ipv4Address::new(Bytes::from_array(body.to_array()[4:8]).to_fixedarray()) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    let sp = (body[8].to_int() << 8) | body[9].to_int()
    let dp = (body[10].to_int() << 8) | body[11].to_int()
    Ipv4({
      source_address: src,
      destination_address: dst,
      source_port: sp,
      destination_port: dp,
    })
  } else if family == Inet6 {
    let src = match
      Ipv6Address::new(Bytes::from_array(body.to_array()[0:16]).to_fixedarray()) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    let dst = match
      Ipv6Address::new(
        Bytes::from_array(body.to_array()[16:32]).to_fixedarray(),
      ) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    let sp = (body[32].to_int() << 8) | body[33].to_int()
    let dp = (body[34].to_int() << 8) | body[35].to_int()
    Ipv6({
      source_address: src,
      destination_address: dst,
      source_port: sp,
      destination_port: dp,
    })
  } else {
    UnixAddress({
      source: Bytes::from_array(body.to_array()[0:108]),
      destination: Bytes::from_array(body.to_array()[108:216]),
    })
  }
  let tlv_data = Bytes::from_array(body.to_array()[needed:])
  match
    parse_tlvs(
      tlv_data,
      offset=16 + needed,
      preserve_unknown=policy.preserve_unknown_tlvs,
    ) {
    Err(err) => Err(err)
    Ok(tlvs) => Ok((address, tlvs))
  }
}

///|
pub fn decode_v2(
  input : Bytes,
  policy : DecodePolicy,
) -> Result[DecodedFrame, ProxyError] {
  if policy.expected_protocol == V1Only {
    return Err(proxy_error(PolicyViolation, 0, "policy accepts only v1"))
  }
  if input.length() < 16 {
    return Err(
      proxy_error(NeedMoreData, input.length(), "v2 fixed header is incomplete"),
    )
  }
  if !bytes_equal_constant_time(
      Bytes::from_array(input.to_array()[0:12]),
      v2_signature(),
    ) {
    return Err(proxy_error(InvalidV2Signature, 0, "v2 signature differs"))
  }
  let version = input[12].to_int() >> 4
  if version != 2 {
    return Err(
      proxy_error(UnsupportedVersion, 12, "v2 version nibble must be 2"),
    )
  }
  let command = match parse_v2_command(input[12].to_int() & 15) {
    Err(err) => return Err(err)
    Ok(v) => v
  }
  let length = (input[14].to_int() << 8) | input[15].to_int()
  if length > policy.max_v2_payload_bytes {
    return Err(
      proxy_error(
        HeaderTooLarge,
        14,
        "declared v2 payload exceeds configured bound",
      ),
    )
  }
  let total = 16 + length
  if input.length() < total {
    return Err(
      proxy_error(
        NeedMoreData,
        input.length(),
        "declared v2 header body is incomplete",
      ),
    )
  }
  let whole = Bytes::from_array(input.to_array()[0:total])
  let body = Bytes::from_array(input.to_array()[16:total])
  let header = if command == Local {
    if !policy.allow_local_command {
      Err(
        proxy_error(PolicyViolation, 12, "LOCAL command is disabled by policy"),
      )
    } else {
      // RFC wire fields after the LOCAL command are ignored. Normalize them so
      // reserved family/transport nibbles cannot influence the returned model.
      ProxyHeader::new(V2, Local, Unspec, Unspec, NoAddress, [])
    }
  } else {
    let family = match parse_v2_family(input[13].to_int() >> 4) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    let transport = match parse_v2_transport(input[13].to_int() & 15) {
      Err(err) => return Err(err)
      Ok(v) => v
    }
    match parse_v2_proxy_body(family, transport, body, policy) {
      Err(err) => Err(err)
      Ok((address, tlvs)) =>
        ProxyHeader::new(V2, Proxy, family, transport, address, tlvs)
    }
  }
  match header {
    Err(err) => Err(err)
    Ok(parsed) => {
      let has_crc = find_tlv(parsed.tlvs, TLV_CRC32C) is Some(_)
      if policy.require_crc32c && !has_crc {
        return Err(proxy_error(MissingCrc32c, 16, "policy requires CRC32C"))
      }
      if policy.verify_crc32c {
        if has_crc {
          match verify_proxy_crc32c(whole) {
            Err(err) => return Err(err)
            Ok(_) => ()
          }
        }
      }
      Ok({
        header: parsed,
        remaining: Bytes::from_array(input.to_array()[total:]),
        consumed: total,
      })
    }
  }
}