/// MIME message parser: handles multipart message structure.
///
/// Implements RFC 2045/2046 MIME message parsing including:
///   - Multipart boundary detection and splitting
///   - Recursive parsing of nested multipart bodies
///   - Per-part header extraction
///   - Attachment identification
///
/// The parser works on the raw text level, scanning line by line for
/// boundary delimiters. Each part's headers and body are captured and
/// converted into the shared AST types.

///|
/// Parse a full MIME message from raw text.
/// Returns a Message with an optional MimePart tree.
pub fn parse_mime_message(input : String) -> Message {
  // First extract headers using the base message parser
  let base = parse_message(input)
  let headers = base.headers

  // Determine the Content-Type
  let ct_value = get_header_from_array(headers, "Content-Type")
  let mime_part = match ct_value {
    None => None
    Some(v) => {
      let ct = parse_content_type(v)
      match ct {
        None => None
        Some(parsed) =>
          if is_multipart(parsed) {
            // Find boundary parameter
            let boundary = content_type_param(parsed, "boundary")
            match boundary {
              None => None
              Some(b) => {
                let body = get_message_body(base)
                match body {
                  None => None
                  Some(body_text) => {
                    let sub_parts = split_multipart(body_text, b)
                    let parts : Array[MimePart] = []
                    for sub in sub_parts {
                      let part = parse_mime_part(sub)
                      parts.push(part)
                    }
                    Some(MimePart::{ headers: [], body: Body::Multipart(parts) })
                  }
                }
              }
            }
          } else {
            // Non-multipart: treat the body as a single part
            let body = get_message_body(base)
            match body {
              None => None
              Some(body_text) => {
                let part_headers : Array[Header] = []
                for h in headers {
                  if is_mime_header(h.name) {
                    part_headers.push(h)
                  }
                }
                // Decode body if base64 or quoted-printable
                let decoded = decode_body_if_needed(body_text, headers)
                Some(MimePart::{
                  headers: part_headers,
                  body: Body::Text(decoded),
                })
              }
            }
          }
      }
    }
  }

  Message::{ headers, body: base.body, mime_part }
}

///|
/// Get a header value from an array of headers (case-insensitive).
fn get_header_from_array(headers : Array[Header], name : String) -> String? {
  let lower = name.to_lower()
  for h in headers {
    if h.name.to_lower() == lower {
      return Some(h.value)
    }
  }
  None
}

///|
/// Get the body text of a message, trimming a single trailing newline.
fn get_message_body(msg : Message) -> String? {
  match msg.body {
    None => None
    Some(b) => {
      let trimmed = b.trim(chars="\r\n").to_owned()
      if trimmed.length() > 0 {
        Some(trimmed)
      } else {
        None
      }
    }
  }
}

///|
/// Check whether a header name is a MIME-specific header.
fn is_mime_header(name : String) -> Bool {
  let lower = name.to_lower()
  lower == "content-type" ||
  lower == "content-disposition" ||
  lower == "content-transfer-encoding" ||
  lower == "content-id" ||
  lower == "content-description" ||
  lower == "mime-version"
}

///|
/// Decode a message body based on its Content-Transfer-Encoding header.
/// Supports base64 and quoted-printable; other encodings pass through.
fn decode_body_if_needed(body_text : String, headers : Array[Header]) -> String {
  let cte = get_header_from_array(headers, "Content-Transfer-Encoding")
  let lower = match cte {
    None => ""
    Some(v) => v.to_lower()
  }
  if lower.contains("base64") {
    // Remove whitespace and decode
    let compact = remove_whitespace(body_text)
    match base64_decode(compact) {
      Some(decoded) => decoded
      None => body_text
    }
  } else if lower.contains("quoted-printable") {
    match quoted_printable_decode(body_text) {
      Some(decoded) => decoded
      None => body_text
    }
  } else {
    body_text
  }
}

///|
/// Remove all whitespace characters from a string.
fn remove_whitespace(input : String) -> String {
  let sb = StringBuilder::new()
  for c in input {
    if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
      sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
/// Decode a quoted-printable encoded string (RFC 2045 section 6.7).
fn quoted_printable_decode(input : String) -> String? {
  let sb = StringBuilder::new()
  let len = input.length()
  let mut i = 0
  while i < len {
    let c = input.get_char(i).unwrap()
    if c == '=' {
      if i + 1 < len && input.get_char(i + 1).unwrap() == '\n' {
        // Soft line break: skip both characters
        i = i + 2
        continue
      } else if i + 2 < len &&
        input.get_char(i + 1).unwrap() == '\r' &&
        input.get_char(i + 2).unwrap() == '\n' {
        // Soft line break with CRLF: skip all three
        i = i + 3
        continue
      } else if i + 2 < len {
        // Hex escape: =XX
        let h1 = input.get_char(i + 1).unwrap()
        let h2 = input.get_char(i + 2).unwrap()
        let byte = hex_value(h1, h2)
        match byte {
          Some(b) => sb.write_char(Int::unsafe_to_char(b))
          None => {
            // Invalid escape, pass through
            sb.write_char('=')
            i = i + 1
            continue
          }
        }
        i = i + 3
        continue
      } else {
        sb.write_char(c)
        i = i + 1
        continue
      }
    }
    sb.write_char(c)
    i = i + 1
  }
  Some(sb.to_string())
}

///|
/// Parse a two-char hex string to an integer value.
fn hex_value(h1 : Char, h2 : Char) -> Int? {
  let hi = hex_digit(h1)
  let lo = hex_digit(h2)
  match (hi, lo) {
    (Some(a), Some(b)) => Some(a * 16 + b)
    _ => None
  }
}

///|
/// Convert a hex character to its numeric value.
fn hex_digit(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - '0'.to_int())
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 'A'.to_int() + 10)
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 'a'.to_int() + 10)
  } else {
    None
  }
}

///|
/// Split a multipart body into raw sub-part strings using a boundary.
fn split_multipart(body : String, boundary : String) -> Array[String] {
  let results : Array[String] = []
  let opening = "--" + boundary
  let closing = opening + "--"

  // Split body into lines for boundary detection
  let lines = split_lines(body)
  let mut current : Array[String] = []
  let mut in_part = false

  for line in lines {
    let trimmed = line.trim(chars=" \t").to_owned()
    if trimmed == opening {
      // Start a new part
      if in_part {
        // Save previous part (drop its trailing blank separator)
        let part_text = join_lines(current)
        results.push(trim_part(part_text))
      }
      current = []
      in_part = true
      continue
    } else if trimmed == closing {
      // End of multipart
      if in_part {
        let part_text = join_lines(current)
        results.push(trim_part(part_text))
      }
      in_part = false
      break
    } else if trimmed.has_prefix("--") && in_part {
      // Nested or other boundary delimiter: treat as part boundary stop
      // Only stop if it is not a valid content line
      continue
    }

    if in_part {
      current.push(line)
    }
  }

  // If no closing boundary was found, flush remaining part
  if in_part {
    let part_text = join_lines(current)
    results.push(trim_part(part_text))
  }

  results
}

///|
/// Split a string into lines (keeping line contents, without newlines).
fn split_lines(input : String) -> Array[String] {
  let results : Array[String] = []
  let mut start = 0
  let mut i = 0
  let len = input.length()
  while i < len {
    let c = input.get_char(i).unwrap()
    if c == '\n' {
      let line = input.unsafe_substring(start~, end=i)
      // Strip trailing \r
      let clean = if line.has_suffix("\r") {
        line.unsafe_substring(start=0, end=line.length() - 1)
      } else {
        line
      }
      results.push(clean)
      start = i + 1
    }
    i = i + 1
  }
  if start < len {
    results.push(input.unsafe_substring(start~, end=len))
  }
  results
}

///|
/// Join an array of lines back into a string with newlines.
fn join_lines(lines : Array[String]) -> String {
  let sb = StringBuilder::new()
  let mut first = true
  for line in lines {
    if !first {
      sb.write_string("\n")
    }
    sb.write_string(line)
    first = false
  }
  sb.to_string()
}

///|
/// Trim leading and trailing blank lines from a part body.
fn trim_part(input : String) -> String {
  let lines = split_lines(input)
  let mut start = 0
  let mut end = lines.length()
  while start < end &&
        lines[start].trim(chars=" \t\r\n").to_owned().length() == 0 {
    start = start + 1
  }
  while end > start &&
        lines[end - 1].trim(chars=" \t\r\n").to_owned().length() == 0 {
    end = end - 1
  }
  let trimmed : Array[String] = []
  let mut i = start
  while i < end {
    trimmed.push(lines[i])
    i = i + 1
  }
  join_lines(trimmed)
}

///|
/// Parse a single MIME part from its raw text (headers + body).
fn parse_mime_part(raw : String) -> MimePart {
  let base = parse_message(raw)
  let headers = base.headers
  let body = get_message_body(base)

  // Decode the body based on Content-Transfer-Encoding
  let decoded = match body {
    None => ""
    Some(b) => decode_body_if_needed(b, headers)
  }

  // Check whether this part is itself multipart (nested)
  let ct_value = get_header_from_array(headers, "Content-Type")
  let nested = match ct_value {
    None => None
    Some(v) => {
      let ct = parse_content_type(v)
      match ct {
        None => None
        Some(parsed) if is_multipart(parsed) => {
          let boundary = content_type_param(parsed, "boundary")
          match boundary {
            None => None
            Some(b) => {
              let sub_parts = split_multipart(decoded, b)
              let parts : Array[MimePart] = []
              for sub in sub_parts {
                parts.push(parse_mime_part(sub))
              }
              Some(Body::Multipart(parts))
            }
          }
        }
        _ => None
      }
    }
  }

  let part_body = match nested {
    Some(b) => b
    None => Body::Text(decoded)
  }

  MimePart::{ headers, body: part_body }
}

///|
/// Check whether a MIME part is an attachment (has attachment disposition).
pub fn is_attachment(part : MimePart) -> Bool {
  let cd = get_header_from_array(part.headers, "Content-Disposition")
  match cd {
    None => false
    Some(v) => {
      let parsed = parse_content_disposition(v)
      match parsed {
        None => false
        Some(d) => d.disposition == "attachment"
      }
    }
  }
}

///|
/// Get the filename from a MIME part, if any.
pub fn part_filename(part : MimePart) -> String? {
  let cd = get_header_from_array(part.headers, "Content-Disposition")
  match cd {
    None => {
      // Try Content-Type name parameter
      let ct = get_header_from_array(part.headers, "Content-Type")
      match ct {
        None => None
        Some(v) => {
          let parsed = parse_content_type(v)
          match parsed {
            None => None
            Some(c) => content_type_param(c, "name")
          }
        }
      }
    }
    Some(v) => {
      let parsed = parse_content_disposition(v)
      match parsed {
        None => None
        Some(d) => disposition_param(d, "filename")
      }
    }
  }
}

///|
/// Collect all attachments from a MIME part tree.
pub fn collect_attachments(part : MimePart) -> Array[MimePart] {
  let results : Array[MimePart] = []
  match part.body {
    Body::Multipart(parts) =>
      for p in parts {
        let sub = collect_attachments(p)
        for s in sub {
          results.push(s)
        }
      }
    _ => if is_attachment(part) { results.push(part) }
  }
  results
}

///|
/// Get the plain-text content from a MIME part tree, if any.
pub fn plain_text(part : MimePart) -> String? {
  match part.body {
    Body::Text(t) => Some(t)
    Body::Multipart(parts) => {
      for p in parts {
        let ct = get_header_from_array(p.headers, "Content-Type")
        match ct {
          None => {
            let t = plain_text(p)
            match t {
              Some(s) => return Some(s)
              None => ()
            }
          }
          Some(v) =>
            if v.to_lower().contains("text/plain") {
              let t = plain_text(p)
              match t {
                Some(s) => return Some(s)
                None => ()
              }
            } else {
              let t = plain_text(p)
              match t {
                Some(s) => return Some(s)
                None => ()
              }
            }
        }
      }
      None
    }
    Body::Binary(_) => None
  }
}

///|
/// Extract the text content of a MIME part as a string.
/// For text parts returns the decoded text; for binary parts returns None.
pub fn part_body_text(part : MimePart) -> String? {
  match part.body {
    Body::Text(t) => Some(t)
    Body::Binary(_) => None
    Body::Multipart(_) => None
  }
}