///|
/// A header belonging to one multipart body part. Header names are preserved
/// as received; use `Part::header` for case-insensitive lookup.
pub(all) struct Header {
  name : String
  value : String
} derive(Debug, Eq)

///|
/// One decoded multipart body part. `body` is text in this first core API;
/// the parser never interprets its contents.
pub(all) struct Part {
  headers : Array[Header]
  body : String
} derive(Debug, Eq)

///|
/// Errors reported when a multipart body is malformed or exceeds its limit.
pub(all) suberror MultipartError {
  InvalidBoundary
  InvalidContentType
  BodyTooLarge(Int)
  MissingOpeningBoundary
  MissingHeaderTerminator
  InvalidHeader(String)
  DuplicateHeader(String)
  MissingContentDisposition
  InvalidContentDisposition
  MissingFieldName
  HeaderTooLarge(Int)
  HeaderCountExceeded(Int)
  PartCountExceeded(Int)
  PartTooLarge(Int)
  InvalidStreamEvent
  FieldTooLarge(Int)
  FileTooLarge(Int)
  FieldCountExceeded(Int)
  FileCountExceeded(Int)
  UnexpectedField(String)
  SinkFailure(String)
  MissingClosingBoundary
  UnexpectedBoundary
} derive(Debug)

///|
/// An incremental multipart collector. Chunks may be supplied at arbitrary
/// boundaries; parsing happens on `finish` so a delimiter split across chunks
/// is handled exactly like an unsplit delimiter.
pub struct Parser {
  boundary : String
  max_body_size : Int
  max_part_count : Int
  max_header_size : Int
  max_header_count : Int
  mut buffer : String
  mut failed : MultipartError?
}

///|
/// Creates a parser for the supplied boundary. The boundary must be the raw
/// value from the Content-Type parameter, without the leading `--`.
pub fn Parser::new(
  boundary : String,
  max_body_size? : Int = 8_388_608,
) -> Parser {
  Parser::{
    boundary,
    max_body_size,
    max_part_count: 1_024,
    max_header_size: 16_384,
    max_header_count: 128,
    buffer: "",
    failed: None,
  }
}

///|
/// Creates a parser with explicit resource limits. The parser rejects a body
/// once any configured bound is exceeded.
pub fn Parser::with_limits(
  boundary : String,
  max_body_size? : Int = 8_388_608,
  max_part_count? : Int = 1_024,
  max_header_size? : Int = 16_384,
  max_header_count? : Int = 128,
) -> Parser {
  Parser::{
    boundary,
    max_body_size,
    max_part_count,
    max_header_size,
    max_header_count,
    buffer: "",
    failed: None,
  }
}

///|
/// Creates a parser directly from an HTTP Content-Type value.
pub fn Parser::from_content_type(
  content_type : String,
  max_body_size? : Int = 8_388_608,
) -> Result[Parser, MultipartError] {
  match boundary_from_content_type(content_type) {
    Ok(boundary) => Ok(Parser::new(boundary, max_body_size~))
    Err(error) => Err(error)
  }
}

///|
/// Extracts and validates the boundary parameter from a multipart/form-data
/// Content-Type header. Quoted boundary values are accepted.
pub fn boundary_from_content_type(
  content_type : String,
) -> Result[String, MultipartError] {
  guard parse_parameterized_value(content_type) is Ok(parsed) else {
    return Err(MultipartError::InvalidContentType)
  }
  guard parsed.base.to_lower() == "multipart/form-data" else {
    return Err(MultipartError::InvalidContentType)
  }
  guard parsed.parameter("boundary") is Some(boundary) else {
    return Err(MultipartError::InvalidContentType)
  }
  guard is_valid_boundary(boundary) else {
    return Err(MultipartError::InvalidBoundary)
  }
  Ok(boundary)
}

///|
/// Adds a body chunk. Chunks can be split at any character boundary.
pub fn Parser::feed(
  self : Parser,
  chunk : String,
) -> Result[Unit, MultipartError] {
  match self.failed {
    Some(error) => Err(error)
    None => {
      let next_size = self.buffer.length() + chunk.length()
      if next_size > self.max_body_size {
        let error = MultipartError::BodyTooLarge(self.max_body_size)
        self.failed = Some(error)
        Err(error)
      } else {
        self.buffer = self.buffer + chunk
        Ok(())
      }
    }
  }
}

///|
/// Parses all supplied chunks and returns body parts in wire order.
pub fn Parser::finish(self : Parser) -> Result[Array[Part], MultipartError] {
  match self.failed {
    Some(error) => Err(error)
    None =>
      parse_body(
        self.boundary,
        self.buffer,
        self.max_part_count,
        self.max_header_size,
        self.max_header_count,
      )
  }
}

///|
/// Returns a header value using ASCII case-insensitive comparison.
pub fn Part::header(self : Part, name : String) -> String? {
  find_header(self.headers, name)
}

///|
fn find_header(headers : Array[Header], name : String) -> String? {
  let target = name.to_lower()
  for header in headers {
    if header.name.to_lower() == target {
      return Some(header.value)
    }
  }
  None
}

///|
/// Returns a parameter from Content-Disposition, such as `name` or
/// `filename`. Quoted parameter values are unquoted.
pub fn Part::disposition_parameter(self : Part, parameter : String) -> String? {
  match self.header("content-disposition") {
    None => None
    Some(value) => find_parameter(value, parameter)
  }
}

///|
/// Convenience accessor for the form field name.
pub fn Part::name(self : Part) -> String? {
  self.disposition_parameter("name")
}

///|
/// Convenience accessor for the submitted file name, when present.
pub fn Part::filename(self : Part) -> String? {
  self.disposition_parameter("filename")
}

///|
/// Returns the submitted filename with both Unix and Windows directory
/// components removed. An empty string is returned for `.` and `..`; callers
/// should still generate their own storage path rather than trusting a name.
pub fn filename_basename(filename : String) -> String {
  let mut basename = filename
  for segment in filename.split("/") {
    basename = segment.to_owned()
  }
  let without_unix_path = basename
  for segment in without_unix_path.split("\\") {
    basename = segment.to_owned()
  }
  if basename == "." || basename == ".." {
    ""
  } else {
    basename
  }
}

///|
/// Returns the submitted filename without directory components.
pub fn Part::filename_basename(self : Part) -> String? {
  match self.filename() {
    Some(filename) => Some(filename_basename(filename))
    None => None
  }
}

///|
/// Returns the part media type. RFC 7578 defaults an omitted Content-Type to
/// `text/plain` for form fields.
pub fn Part::content_type(self : Part) -> String {
  match self.header("content-type") {
    Some(value) => value
    None => "text/plain"
  }
}

///|
/// Reports whether this part carries a submitted file name.
pub fn Part::is_file(self : Part) -> Bool {
  self.filename() is Some(_)
}

///|
/// Encodes parts as a multipart body using the supplied boundary.
pub fn encode(boundary : String, parts : Array[Part]) -> String {
  let mut output = ""
  for part in parts {
    output = output + "--" + boundary + "\r\n"
    for header in part.headers {
      output = output + header.name + ": " + header.value + "\r\n"
    }
    output = output + "\r\n" + part.body + "\r\n"
  }
  output + "--" + boundary + "--\r\n"
}

///|
fn parse_body(
  boundary : String,
  body : String,
  max_part_count : Int,
  max_header_size : Int,
  max_header_count : Int,
) -> Result[Array[Part], MultipartError] {
  guard is_valid_boundary(boundary) else {
    return Err(MultipartError::InvalidBoundary)
  }
  let opening = "--" + boundary + "\r\n"
  guard body.has_prefix(opening) else {
    return Err(MultipartError::MissingOpeningBoundary)
  }
  let marker = "\r\n--" + boundary
  let parts : Array[Part] = []
  for cursor = opening.length() {
    let remaining = body[cursor:].to_owned()
    guard remaining.find("\r\n\r\n") is Some(header_offset) else {
      return Err(MultipartError::MissingHeaderTerminator)
    }
    if header_offset > max_header_size {
      return Err(MultipartError::HeaderTooLarge(max_header_size))
    }
    if parts.length() >= max_part_count {
      return Err(MultipartError::PartCountExceeded(max_part_count))
    }
    let header_text = remaining[:header_offset].to_owned()
    let headers = match parse_headers(header_text, max_header_count~) {
      Ok(parsed) => parsed
      Err(error) => return Err(error)
    }
    match validate_form_data_headers(headers) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
    let content_start = cursor + header_offset + 4
    guard find_delimiter(body, marker, content_start)
      is Some((content_end, after_marker)) else {
      return Err(MultipartError::MissingClosingBoundary)
    }
    parts.push(Part::{
      headers,
      body: body[content_start:content_end].to_owned(),
    })
    let suffix = body[after_marker:].to_owned()
    if suffix.has_prefix("--") {
      let tail = suffix[2:].to_owned()
      if tail == "" || tail == "\r\n" {
        return Ok(parts)
      }
      return Err(MultipartError::UnexpectedBoundary)
    }
    if suffix.has_prefix("\r\n") {
      continue after_marker + 2
    }
    return Err(MultipartError::UnexpectedBoundary)
  }
}

///|
fn is_valid_boundary(boundary : String) -> Bool {
  guard boundary.length() > 0 && boundary.length() <= 70 else { return false }
  guard !boundary.has_suffix(" ") else { return false }
  for character in boundary {
    let allowed = character.is_ascii_digit() ||
      character.is_ascii_lowercase() ||
      character.is_ascii_uppercase() ||
      character == '\'' ||
      character == '(' ||
      character == ')' ||
      character == '+' ||
      character == '_' ||
      character == ',' ||
      character == '-' ||
      character == '.' ||
      character == '/' ||
      character == ':' ||
      character == '=' ||
      character == '?' ||
      character == ' '
    if !allowed {
      return false
    }
  }
  true
}

///|
/// Finds the next syntactically valid delimiter. A body may legally contain a
/// marker-like byte sequence when it is not followed by CRLF or `--`, so that
/// sequence must remain part of the part body.
fn find_delimiter(body : String, marker : String, start : Int) -> (Int, Int)? {
  for scan = start {
    let remaining = body[scan:].to_owned()
    guard remaining.find(marker) is Some(offset) else { return None }
    let delimiter_start = scan + offset
    let after_marker = delimiter_start + marker.length()
    let suffix = body[after_marker:].to_owned()
    if suffix.has_prefix("--") || suffix.has_prefix("\r\n") {
      return Some((delimiter_start, after_marker))
    }
    continue after_marker
  }
}

///|
fn parse_headers(
  text : String,
  max_header_count? : Int = 128,
) -> Result[Array[Header], MultipartError] {
  let headers : Array[Header] = []
  for line in text.split("\r\n") {
    if headers.length() >= max_header_count {
      return Err(MultipartError::HeaderCountExceeded(max_header_count))
    }
    let source = line.to_owned()
    guard source.split_once(":") is Some((name, value)) else {
      return Err(MultipartError::InvalidHeader(source))
    }
    let normalized_name = name.trim().to_owned()
    guard is_valid_header_name(normalized_name) else {
      return Err(MultipartError::InvalidHeader(source))
    }
    let normalized_value = value.trim().to_owned()
    guard is_valid_header_value(normalized_value) else {
      return Err(MultipartError::InvalidHeader(source))
    }
    headers.push(Header::{ name: normalized_name, value: normalized_value })
  }
  Ok(headers)
}

///|
/// MIME field names use the HTTP token character set. Rejecting whitespace,
/// controls, and separators prevents ambiguous downstream interpretation.
fn is_valid_header_name(name : String) -> Bool {
  guard !name.is_empty() else { return false }
  for character in name {
    let allowed = character.is_ascii_digit() ||
      character.is_ascii_lowercase() ||
      character.is_ascii_uppercase() ||
      character == '!' ||
      character == '#' ||
      character == '$' ||
      character == '%' ||
      character == '&' ||
      character == '\'' ||
      character == '*' ||
      character == '+' ||
      character == '-' ||
      character == '.' ||
      character == '^' ||
      character == '_' ||
      character == '`' ||
      character == '|' ||
      character == '~'
    if !allowed {
      return false
    }
  }
  true
}

///|
fn is_valid_header_value(value : String) -> Bool {
  for character in value {
    if character.is_control() && character != '\t' {
      return false
    }
  }
  true
}