// DNS response pretty-printer (dig-like output).
// Formats decoded DNS messages and RRs for human-readable display.

// Format a full DNS message for display (similar to dig output)

///|
pub fn pretty_print_message(msg : Message) -> String {
  let out = Ref("")

  // Header section
  out.val = out.val + ";; ->>HEADER<<- opcode: QUERY"
  out.val = out.val + ", status: " + rcode_to_string(msg.header.rcode())
  out.val = out.val + ", id: " + msg.header.id.to_string()
  out.val = out.val + "\n"

  // Flags
  out.val = out.val + ";; flags:"
  if msg.header.is_response() {
    out.val = out.val + " qr"
  }
  if msg.header.is_truncated() {
    out.val = out.val + " tc"
  }
  out.val = out.val + "; QUERY: " + msg.header.qdcount.to_string()
  out.val = out.val + ", ANSWER: " + msg.header.ancount.to_string()
  out.val = out.val + ", AUTHORITY: " + msg.header.nscount.to_string()
  out.val = out.val + ", ADDITIONAL: " + msg.header.arcount.to_string()
  out.val = out.val + "\n\n"

  // Question section
  if msg.questions.length() > 0 {
    out.val = out.val + ";; QUESTION SECTION:\n"
    for q in msg.questions {
      out.val = out.val + ";" + q.name + ".    "
      out.val = out.val + qtype_to_string(q.qtype) + "\n"
    }
    out.val = out.val + "\n"
  }

  // Answer section
  if msg.answers.length() > 0 {
    out.val = out.val + ";; ANSWER SECTION:\n"
    for rr in msg.answers {
      out.val = out.val + pretty_print_rr(rr) + "\n"
    }
    out.val = out.val + "\n"
  }

  // Authority section
  if msg.authorities.length() > 0 {
    out.val = out.val + ";; AUTHORITY SECTION:\n"
    for rr in msg.authorities {
      out.val = out.val + pretty_print_rr(rr) + "\n"
    }
    out.val = out.val + "\n"
  }

  // Additional section
  if msg.additionals.length() > 0 {
    out.val = out.val + ";; ADDITIONAL SECTION:\n"
    for rr in msg.additionals {
      out.val = out.val + pretty_print_rr(rr) + "\n"
    }
    out.val = out.val + "\n"
  }

  out.val
}

// Format a single resource record for display

///|
pub fn pretty_print_rr(rr : RR) -> String {
  let result = Ref("")
  result.val = rr.name + ".    "
  result.val = result.val + rr.ttl.to_string() + "    "
  result.val = result.val + qtype_to_string(rr.rtype) + "    "
  result.val = result.val + pretty_print_rdata(rr.rdata)
  result.val
}

// Format RData for display

///|
pub fn pretty_print_rdata(rdata : RData) -> String {
  match rdata {
    A(ip) => pretty_ipv4(ip)
    AAAA(w1, w2, w3, w4) => pretty_ipv6(w1, w2, w3, w4)
    CNAME(target) => target + "."
    NS(ns) => ns + "."
    PTR(ptr) => ptr + "."
    MX(pref, exchange) => pref.to_string() + " " + exchange + "."
    TXT(strings) => {
      let result = Ref("")
      for s in strings {
        result.val = result.val + "\"" + s + "\" "
      }
      result.val
    }
    SOA(mname, rname, serial, refresh, retry, expire, minimum) =>
      mname +
      ". " +
      rname +
      ". (" +
      serial.to_string() +
      " " +
      refresh.to_string() +
      " " +
      retry.to_string() +
      " " +
      expire.to_string() +
      " " +
      minimum.to_string() +
      ")"
    SRV(priority, weight, port, target) =>
      priority.to_string() +
      " " +
      weight.to_string() +
      " " +
      port.to_string() +
      " " +
      target +
      "."
    OPT(options) => "OPT (" + options.length().to_string() + " options)"
    Unknown(data) => "\\# " + data.length().to_string() + " bytes"
  }
}

///|
/// Format RDATA for one-answer-per-line command output.
///
/// This deliberately keeps DNS names without a presentation trailing dot and
/// leaves SOA fields space separated so scripts can compare the value with
/// `dig +short` after their type-specific normalization.  It is a semantic
/// formatter, not a zone-file serializer.
pub fn short_print_rdata(rdata : RData) -> String {
  match rdata {
    A(ip) => pretty_ipv4(ip)
    AAAA(w1, w2, w3, w4) => pretty_ipv6(w1, w2, w3, w4)
    CNAME(name) | NS(name) | PTR(name) => name
    MX(preference, exchange) => preference.to_string() + " " + exchange
    TXT(strings) => strings.join("")
    SOA(mname, rname, serial, refresh, retry, expire, minimum) =>
      mname +
      " " +
      rname +
      " " +
      serial.to_string() +
      " " +
      refresh.to_string() +
      " " +
      retry.to_string() +
      " " +
      expire.to_string() +
      " " +
      minimum.to_string()
    SRV(priority, weight, port, target) =>
      priority.to_string() +
      " " +
      weight.to_string() +
      " " +
      port.to_string() +
      " " +
      target
    OPT(_) => "OPT"
    Unknown(data) => "\\# " + data.length().to_string() + " bytes"
  }
}

// Format IPv4 from integer to dotted decimal

///|
fn pretty_ipv4(ip : Int) -> String {
  let a = (ip >> 24) & 0xFF
  let b = (ip >> 16) & 0xFF
  let c = (ip >> 8) & 0xFF
  let d = ip & 0xFF
  a.to_string() +
  "." +
  b.to_string() +
  "." +
  c.to_string() +
  "." +
  d.to_string()
}

// Format IPv6 from four 32-bit words

///|
fn pretty_ipv6(w1 : Int, w2 : Int, w3 : Int, w4 : Int) -> String {
  let g = fn(v : Int) -> String {
    let r = Ref("")
    let val = Ref(v)
    if val.val == 0 {
      return "0"
    }
    while val.val > 0 {
      let d = val.val % 16
      if d < 10 {
        r.val = "0123456789"[d].unsafe_to_char().to_string() + r.val
      } else {
        r.val = "abcdef"[d - 10].unsafe_to_char().to_string() + r.val
      }
      val.val = val.val / 16
    }
    r.val
  }
  g((w1 >> 16) & 0xFFFF) +
  ":" +
  g(w1 & 0xFFFF) +
  ":" +
  g((w2 >> 16) & 0xFFFF) +
  ":" +
  g(w2 & 0xFFFF) +
  ":" +
  g((w3 >> 16) & 0xFFFF) +
  ":" +
  g(w3 & 0xFFFF) +
  ":" +
  g((w4 >> 16) & 0xFFFF) +
  ":" +
  g(w4 & 0xFFFF)
}

// Format a DnsResult for short display (just answers)

///|
pub fn pretty_print_result(result : DnsResult) -> String {
  let out = Ref("")
  if result.cname_chain.length() > 1 {
    out.val = out.val + ";; CNAME chain: "
    for i = 0; i < result.cname_chain.length(); i = i + 1 {
      if i > 0 {
        out.val = out.val + " -> "
      }
      out.val = out.val + result.cname_chain[i]
    }
    out.val = out.val + "\n"
  }
  for rr in result.answers {
    out.val = out.val + pretty_print_rdata(rr.rdata) + "\n"
  }
  out.val
}

///|
fn pretty_json_escape(value : String) -> String {
  let out = StringBuilder()
  for char in value {
    match char {
      '"' => out.write_string("\\\"")
      '\\' => out.write_string("\\\\")
      '\n' => out.write_string("\\n")
      '\r' => out.write_string("\\r")
      '\t' => out.write_string("\\t")
      _ => {
        let code = char.to_int()
        if code == 8 {
          out.write_string("\\b")
        } else if code == 12 {
          out.write_string("\\f")
        } else if code < 0x20 {
          let hex = code.to_string(radix=16)
          out.write_string("\\u00")
          if hex.length() == 1 {
            out.write_char('0')
          }
          out.write_string(hex)
        } else {
          out.write_char(char)
        }
      }
    }
  }
  out.to_string()
}

// Format a DnsResult as JSON

///|
pub fn pretty_print_result_json(
  result : DnsResult,
  name : String,
  qtype : UInt16,
  server : String,
) -> String {
  let out = Ref("")
  out.val = "{\n"
  out.val = out.val + "  \"name\": \"" + pretty_json_escape(name) + "\",\n"
  out.val = out.val +
    "  \"type\": \"" +
    pretty_json_escape(qtype_to_string(qtype)) +
    "\",\n"
  out.val = out.val + "  \"server\": \"" + pretty_json_escape(server) + "\",\n"
  out.val = out.val + "  \"cname_chain\": ["
  for i = 0; i < result.cname_chain.length(); i = i + 1 {
    if i > 0 {
      out.val = out.val + ", "
    }
    out.val = out.val + "\"" + pretty_json_escape(result.cname_chain[i]) + "\""
  }
  out.val = out.val + "],\n"
  out.val = out.val + "  \"answers\": [\n"
  for i = 0; i < result.answers.length(); i = i + 1 {
    let rr = result.answers[i]
    out.val = out.val + "    {\n"
    out.val = out.val +
      "      \"name\": \"" +
      pretty_json_escape(rr.name) +
      "\",\n"
    out.val = out.val +
      "      \"type\": \"" +
      pretty_json_escape(qtype_to_string(rr.rtype)) +
      "\",\n"
    out.val = out.val + "      \"ttl\": " + rr.ttl.to_string() + ",\n"
    out.val = out.val +
      "      \"data\": \"" +
      pretty_json_escape(pretty_print_rdata(rr.rdata)) +
      "\"\n"
    if i < result.answers.length() - 1 {
      out.val = out.val + "    },\n"
    } else {
      out.val = out.val + "    }\n"
    }
  }
  out.val = out.val + "  ]\n"
  out.val = out.val + "}\n"
  out.val
}