///|
/// Deterministic rendering controls. The date, message-id and boundary seed
/// are all injected by the caller so that tests are reproducible; nothing here
/// reads the system clock or an RNG.
pub struct RenderConfig {
  date : String
  message_id : String
  boundary_seed : String
} derive(Eq, Debug)

///|
pub fn RenderConfig::new(
  date? : String = "",
  message_id? : String = "",
  boundary_seed? : String = "moonmail",
) -> RenderConfig {
  { date, message_id, boundary_seed }
}

///|
/// A fully deterministic default configuration (no date, no message-id,
/// fixed seed).
pub fn RenderConfig::deterministic() -> RenderConfig {
  { date: "", message_id: "", boundary_seed: "moonmail" }
}

///|
/// Render a `MailMessage` to its RFC 5322 / MIME wire form (CRLF line
/// endings). All header values are validated against injection before being
/// emitted.
pub fn render_message(
  msg : MailMessage,
  config : RenderConfig,
) -> String raise MailFailure {
  let headers = apply_render_config(msg.headers.copy(), config)
  // RFC 2045 section 4: MIME messages carry a MIME-Version field.
  if !has_header(headers, "MIME-Version") {
    headers.push(("MIME-Version", "1.0"))
  }
  match msg.body {
    Text(_) | Html(_) | Binary(_, _) => {
      let part : Part = { headers, body: msg.body }
      render_part(part, config.boundary_seed)
    }
    MultiPart(children, subtype) => {
      let child_renders = []
      for i = 0; i < children.length(); i = i + 1 {
        let r = render_part(children[i], config.boundary_seed + "#\{i}")
        child_renders.push(r)
      }
      let combined = fold_strings(child_renders)
      let boundary = unique_boundary(config.boundary_seed, combined)
      let body_text = assemble_multipart(child_renders, boundary)
      set_header_value(
        headers,
        "Content-Type",
        "multipart/\{subtype.to_string()}; boundary=\"\{boundary}\"",
      )
      let out = Buffer::Buffer()
      out.write_string_utf16le(render_headers(headers))
      out.write_string_utf16le("\r\n")
      out.write_string_utf16le(body_text)
      out.to_string()
    }
  }
}

///|
fn fold_strings(items : Array[String]) -> String {
  let buf = Buffer::Buffer()
  for s in items {
    buf.write_string_utf16le(s)
  }
  buf.to_string()
}

///|
fn apply_render_config(
  headers : Array[(String, String)],
  config : RenderConfig,
) -> Array[(String, String)] {
  if config.date != "" {
    set_header_value(headers, "Date", config.date)
  }
  if config.message_id != "" {
    set_header_value(headers, "Message-ID", config.message_id)
  }
  headers
}

///|
/// Render a single part (leaf or nested multipart) to its full wire text.
fn render_part(part : Part, seed : String) -> String raise MailFailure {
  match part.body {
    MultiPart(children, subtype) => {
      let child_renders = []
      for i = 0; i < children.length(); i = i + 1 {
        let r = render_part(children[i], seed + "#\{i}")
        child_renders.push(r)
      }
      let combined = fold_strings(child_renders)
      let boundary = unique_boundary(seed, combined)
      let body_text = assemble_multipart(child_renders, boundary)
      let headers = part.headers.copy()
      set_header_value(
        headers,
        "Content-Type",
        "multipart/\{subtype.to_string()}; boundary=\"\{boundary}\"",
      )
      let out = Buffer::Buffer()
      out.write_string_utf16le(render_headers(headers))
      out.write_string_utf16le("\r\n")
      out.write_string_utf16le(body_text)
      out.to_string()
    }
    _ => render_leaf(part, seed)
  }
}

///|
fn render_leaf(part : Part, seed : String) -> String raise MailFailure {
  let headers = part.headers.copy()
  let body_text = match part.body {
    Text(content) => {
      let enc = resolve_encoding(part)
      let (body, used) = encode_body(content, enc)
      if !has_header(headers, "Content-Type") {
        headers.push(("Content-Type", "text/plain; charset=utf-8"))
      }
      if !has_header(headers, "Content-Transfer-Encoding") {
        headers.push(("Content-Transfer-Encoding", used.to_string()))
      }
      body
    }
    Html(content) => {
      let enc = resolve_encoding(part)
      let (body, used) = encode_body(content, enc)
      if !has_header(headers, "Content-Type") {
        headers.push(("Content-Type", "text/html; charset=utf-8"))
      }
      if !has_header(headers, "Content-Transfer-Encoding") {
        headers.push(("Content-Transfer-Encoding", used.to_string()))
      }
      body
    }
    Binary(data, filename) => {
      let body = base64_mime_encode(data)
      if !has_header(headers, "Content-Type") {
        headers.push(("Content-Type", "application/octet-stream"))
      }
      if !has_header(headers, "Content-Transfer-Encoding") {
        headers.push(("Content-Transfer-Encoding", "base64"))
      }
      if !has_header(headers, "Content-Disposition") {
        let disp = make_disposition(Attachment, filename)
        headers.push(("Content-Disposition", disp))
      }
      body
    }
    MultiPart(_, _) => raise MailFailure::mime("unreachable")
  }
  let _ = seed
  let out = Buffer::Buffer()
  out.write_string_utf16le(render_headers(headers))
  out.write_string_utf16le("\r\n")
  out.write_string_utf16le(body_text)
  if !body_text.has_suffix("\r\n") {
    out.write_string_utf16le("\r\n")
  }
  out.to_string()
}

///|
fn render_headers(
  headers : Array[(String, String)],
) -> String raise MailFailure {
  let buf = Buffer::Buffer()
  for pair in headers {
    let (name, value) = pair
    buf.write_string_utf16le(make_header(name, value))
  }
  buf.to_string()
}

///|
fn has_header(headers : Array[(String, String)], name : String) -> Bool {
  for pair in headers {
    let (n, _) = pair
    if n.to_lower() == name.to_lower() {
      return true
    }
  }
  false
}

///|
fn set_header_value(
  headers : Array[(String, String)],
  name : String,
  value : String,
) -> Unit {
  set_header(headers, name, value)
}

///|
fn resolve_encoding(part : Part) -> ContentTransferEncoding raise MailFailure {
  match part.header("Content-Transfer-Encoding") {
    Some(v) => parse_cte(v)
    None => Auto
  }
}

///|
pub fn parse_cte(v : String) -> ContentTransferEncoding raise MailFailure {
  match v.trim().to_lower() {
    "7bit" => SevenBit
    "8bit" => EightBit
    "base64" => Base64
    "quoted-printable" => QuotedPrintable
    _ => raise MailFailure::encode("unknown transfer encoding: \{v}")
  }
}