// EDNS(0) OPT pseudo-record support (RFC 6891).

///|
pub struct OptRR {
  udp_payload_size : UInt16
  ext_rcode : UInt16
  edns_version : UInt16
  flags : UInt16
  options : Array[OptOption]
}

///|
pub struct OptOption {
  opt_code : UInt16
  opt_data : Array[Byte]
}

///|
pub let edns_flag_do : UInt16 = 0x8000

///|
pub let edns_default_udp_size : UInt16 = 1232

///|
pub fn OptRR::default() -> OptRR {
  {
    udp_payload_size: edns_default_udp_size,
    ext_rcode: 0,
    edns_version: 0,
    flags: 0,
    options: Array::new(capacity=0),
  }
}

///|
pub fn OptRR::with_dnssec(payload_size : UInt16) -> OptRR {
  {
    udp_payload_size: payload_size,
    ext_rcode: 0,
    edns_version: 0,
    flags: edns_flag_do,
    options: Array::new(capacity=0),
  }
}

///|
fn OptRR::validate_fields(self : OptRR) -> Result[Array[Byte], String] {
  if self.udp_payload_size < max_udp_payload {
    return Err("EDNS UDP payload size must be at least 512")
  }
  if self.ext_rcode > 255 || self.edns_version > 255 {
    return Err("EDNS extended RCODE and version must fit in 8 bits")
  }
  encode_opt_options(self.options)
}

///|
fn encode_opt_options(
  options : Array[OptOption],
) -> Result[Array[Byte], String] {
  if options.length() > max_edns_options {
    return Err("too many EDNS options")
  }
  let out : Array[Byte] = Array::new(capacity=32)
  for option in options {
    match validate_edns_option(option) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    if option.opt_data.length() > 65535 {
      return Err("EDNS option exceeds 65535 octets")
    }
    append_u16(out, option.opt_code.to_int())
    append_u16(out, option.opt_data.length())
    append_bytes(out, option.opt_data)
  }
  if out.length() > 65535 {
    Err("OPT RDATA exceeds 65535 octets")
  } else {
    Ok(out)
  }
}

///|
fn decode_opt_options(
  bytes : Array[Byte],
  offset : Int,
  end : Int,
) -> Result[Array[OptOption], String] {
  if offset < 0 || end < offset || end > bytes.length() {
    return Err("invalid OPT RDATA range")
  }
  let options : Array[OptOption] = Array::new(capacity=4)
  let pos = Ref(offset)
  while pos.val < end {
    if end - pos.val < 4 {
      return Err("truncated EDNS option header")
    }
    if options.length() >= max_edns_options {
      return Err("too many EDNS options")
    }
    let code = ((bytes[pos.val].to_int() << 8) | bytes[pos.val + 1].to_int()).to_uint16()
    let length = (bytes[pos.val + 2].to_int() << 8) |
      bytes[pos.val + 3].to_int()
    pos.val = pos.val + 4
    if length > end - pos.val {
      return Err("truncated EDNS option data")
    }
    let data = match wire_copy_range(bytes, pos.val, length) {
      Ok(value) => value
      Err(err) => return Err(err)
    }
    let option = { opt_code: code, opt_data: data }
    match validate_edns_option(option) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    options.push(option)
    pos.val = pos.val + length
  }
  Ok(options)
}

///|
pub fn OptRR::encode_checked(self : OptRR) -> Result[Array[Byte], String] {
  let option_bytes = match self.validate_fields() {
    Ok(bytes) => bytes
    Err(err) => return Err(err)
  }
  let out : Array[Byte] = Array::new(capacity=11 + option_bytes.length())
  out.push(0) // owner name is the root domain
  append_u16(out, qtype_opt.to_int())
  append_u16(out, self.udp_payload_size.to_int())
  let ttl = (self.ext_rcode.to_int() << 24) |
    (self.edns_version.to_int() << 16) |
    self.flags.to_int()
  append_u32(out, ttl.reinterpret_as_uint())
  append_u16(out, option_bytes.length())
  append_bytes(out, option_bytes)
  Ok(out)
}

///|
pub fn OptRR::encode(self : OptRR) -> Array[Byte] {
  match self.encode_checked() {
    Ok(bytes) => bytes
    Err(error) => abort(error)
  }
}

///|
pub fn OptRR::to_rr_checked(self : OptRR) -> Result[RR, String] {
  let option_bytes = match self.validate_fields() {
    Ok(bytes) => bytes
    Err(error) => return Err(error)
  }
  let rdata = RData::OPT(self.options)
  let rdlength = option_bytes.length().to_uint16()
  let ttl = ((self.ext_rcode.to_int() << 24) |
  (self.edns_version.to_int() << 16) |
  self.flags.to_int()).reinterpret_as_uint()
  Ok({
    name: "",
    rtype: qtype_opt,
    rclass: self.udp_payload_size,
    ttl,
    rdlength,
    rdata,
  })
}

///|
/// Compatibility wrapper for the original API. Invalid EDNS fields abort
/// explicitly instead of constructing an RR that bypasses checked validation.
pub fn OptRR::to_rr(self : OptRR) -> RR {
  match self.to_rr_checked() {
    Ok(record) => record
    Err(error) => abort(error)
  }
}

///|
pub fn opt_from_rr(rr : RR) -> Result[OptRR, String] {
  if rr.rtype != qtype_opt {
    return Err("resource record is not an OPT record")
  }
  if rr.name != "" {
    return Err("OPT owner name must be the root domain")
  }
  // RFC 6891 section 6.2.3: receivers treat a peer's advertised size below
  // 512 as 512. Outbound configuration remains strict in `validate_fields`.
  let udp_payload_size = if rr.rclass < max_udp_payload {
    max_udp_payload
  } else {
    rr.rclass
  }
  let ext_rcode = ((rr.ttl >> 24) & 0xFF).to_uint16()
  let edns_version = ((rr.ttl >> 16) & 0xFF).to_uint16()
  let flags = (rr.ttl & 0xFFFF).to_uint16()
  match rr.rdata {
    OPT(options) => {
      let encoded = match encode_opt_options(options) {
        Ok(bytes) => bytes
        Err(error) => return Err(error)
      }
      if rr.rdlength.to_int() != encoded.length() {
        return Err("OPT RDLENGTH does not match encoded option data")
      }
      Ok({ udp_payload_size, ext_rcode, edns_version, flags, options })
    }
    _ => Err("OPT record does not contain OPT RDATA")
  }
}

// This helper decodes only an OPT RDATA field.  The RR class/TTL fields are
// intentionally supplied as defaults because a caller that needs them should
// decode the surrounding RR and call opt_from_rr.

///|
pub fn decode_opt_rr(
  bytes : Array[Byte],
  offset : Int,
  rdlength : UInt16,
  _msg_start : Int,
) -> Result[(OptRR, Int), String] {
  let end = offset + rdlength.to_int()
  let options = match decode_opt_options(bytes, offset, end) {
    Ok(value) => value
    Err(err) => return Err(err)
  }
  Ok(
    (
      {
        udp_payload_size: max_udp_payload,
        ext_rcode: 0,
        edns_version: 0,
        flags: 0,
        options,
      },
      end,
    ),
  )
}

///|
pub fn build_query_with_edns_checked(
  id : UInt16,
  name : String,
  qtype : UInt16,
  recurse : Bool,
  opt : OptRR,
) -> Result[Message, String] {
  let query = build_query(id, name, qtype, recurse)
  match opt.to_rr_checked() {
    Ok(record) => Ok({ ..query, additionals: [record] })
    Err(error) => Err(error)
  }
}

///|
/// Compatibility wrapper for the original API. Resolver internals use the
/// checked variant so malformed input is returned as a typed error.
pub fn build_query_with_edns(
  id : UInt16,
  name : String,
  qtype : UInt16,
  recurse : Bool,
  opt : OptRR,
) -> Message {
  match build_query_with_edns_checked(id, name, qtype, recurse, opt) {
    Ok(query) => query
    Err(error) => abort(error)
  }
}