///|
/// Structural values preserve order and bind each literal to its syntax position.
/// Numeric atoms remain text until a profile checks its exact range. The original
/// Response remains available for lexical spelling and diagnostic text.
pub(all) enum ImapValue {
  Atom(String)
  Quoted(String)
  Nil
  Literal(Bytes)
  List(Array[ImapValue])
} derive(Debug, Eq)

///|
pub(all) enum TypedResponse {
  Status(String, String, Array[ImapValue], String) // tag or *, status, code, text
  Data(Array[ImapValue])
  Continuation(String)
} derive(Debug, Eq)

///|
priv struct ValueParser {
  chars : Array[Char]
  literals : Array[Bytes]
  mut at : Int
  mut literal_at : Int
  mut nodes : Int
}

///|
fn ValueParser::spaces(self : ValueParser) -> Unit {
  while self.at < self.chars.length() && self.chars[self.at] == ' ' {
    self.at += 1
  }
}

///|
fn ValueParser::sequence(
  self : ValueParser,
  end : Char?,
  depth : Int,
) -> Array[ImapValue] raise ImapError {
  if depth > 32 {
    raise Invalid("response nesting limit")
  }
  let out = []
  self.spaces()
  while self.at < self.chars.length() {
    if end is Some(c) && self.chars[self.at] == c {
      self.at += 1
      return out
    }
    if self.chars[self.at] == ')' ||
      (end == Some(']') && self.chars[self.at] == ']') {
      raise Invalid("unexpected response delimiter")
    }
    self.nodes += 1
    if self.nodes > 8192 {
      raise Invalid("response node limit")
    }
    out.push(self.value(depth))
    if self.at < self.chars.length() &&
      self.chars[self.at] != ' ' &&
      !(end is Some(c) && self.chars[self.at] == c) {
      // A literal's framing CRLF is consumed inside value(); its following
      // syntax still needs the same SP/list terminator as any other value.
      raise Invalid("missing response value separator")
    }
    self.spaces()
  }
  if end != None {
    raise Invalid("unclosed response list/code")
  }
  out
}

///|
fn ValueParser::value(
  self : ValueParser,
  depth : Int,
) -> ImapValue raise ImapError {
  let start = self.at
  match self.chars[self.at] {
    '(' => {
      self.at += 1
      List(self.sequence(Some(')'), depth + 1))
    }
    '"' => {
      self.at += 1
      let out = StringBuilder()
      while self.at < self.chars.length() {
        let c = self.chars[self.at]
        self.at += 1
        if c == '"' {
          return Quoted(out.to_string())
        }
        if c == '\\' {
          if self.at == self.chars.length() ||
            !(self.chars[self.at] == '\\' || self.chars[self.at] == '"') {
            raise Invalid("invalid quoted escape")
          }
          out.write_char(self.chars[self.at])
          self.at += 1
        } else {
          if c == '\r' || c == '\n' || c == '\u0000' {
            raise Invalid("invalid quoted byte")
          }
          out.write_char(c)
        }
      }
      raise Invalid("unclosed quoted string")
    }
    '{' => {
      self.at += 1
      let digits = self.at
      while self.at < self.chars.length() &&
            self.chars[self.at] >= '0' &&
            self.chars[self.at] <= '9' {
        self.at += 1
      }
      if self.at == digits ||
        self.at + 1 >= self.chars.length() ||
        self.chars[self.at] != '}' ||
        self.chars[self.at + 1] != '\n' {
        raise Invalid("literal marker/framing mismatch")
      }
      let size = @strconv.parse_int(
        String::from_array(self.chars[digits:self.at]),
      ) catch {
        _ => raise Invalid("literal marker overflow")
      }
      if self.literal_at >= self.literals.length() ||
        self.literals[self.literal_at].length() != size {
        raise Invalid("literal payload mismatch")
      }
      let bytes = self.literals[self.literal_at]
      self.literal_at += 1
      self.at += 2
      Literal(bytes)
    }
    _ => {
      // BODY[HEADER.FIELDS (FROM TO)] is one attribute name, not an AST list.
      // Preserve section syntax opaquely; full-body projection accepts BODY[].
      let mut section = false
      while self.at < self.chars.length() {
        let c = self.chars[self.at]
        if !section && (c == ' ' || c == ')' || c == ']') {
          break
        }
        if c == '[' {
          if section || self.at == start {
            raise Invalid("invalid section token")
          }
          section = true
        } else if c == ']' {
          section = false
        } else if c == '\n' ||
          c == '\r' ||
          c == '\u0000' ||
          c == '{' ||
          c == '"' ||
          (!section && c == '(') ||
          c == '\t' {
          raise Invalid("invalid response atom")
        }
        self.at += 1
      }
      if section || self.at == start {
        raise Invalid("invalid response atom/section")
      }
      let value = String::from_array(self.chars[start:self.at])
      if value.has_suffix("~") {
        raise Invalid("literal8 is not supported")
      }
      if value.to_upper() == "NIL" {
        Nil
      } else {
        Atom(value)
      }
    }
  }
}

///|
/// Parse already framed response syntax with exact literal association. Limits:
/// 64 KiB syntax, 64 literals/2 MiB total, depth 32, 8192 structural values.
/// Unknown extensions remain structural data; this is not full extension semantics.
pub fn Response::parse(self : Response) -> TypedResponse raise ImapError {
  if self.line.length() > 65536 ||
    @utf8.encode(self.line).length() > 65536 ||
    self.literals.length() > 64 {
    raise Invalid("response limits")
  }
  let mut total = 0
  for bytes in self.literals {
    if bytes.length() > 1048576 || bytes.length() > 2097152 - total {
      raise Invalid("literal limits")
    }
    total += bytes.length()
  }
  if self.line == "+" || self.line.has_prefix("+ ") {
    if !self.literals.is_empty() {
      raise Invalid("continuation literal")
    }
    return Continuation(
      if self.line == "+" {
        ""
      } else {
        self.line[2:].to_owned()
      },
    )
  }
  let fields = words(self.line)
  if fields.length() < 2 {
    raise Invalid("response prefix")
  }
  let status = fields[1].to_upper()
  if status == "OK" ||
    status == "NO" ||
    status == "BAD" ||
    status == "BYE" ||
    status == "PREAUTH" {
    if !self.literals.is_empty() ||
      self.line.contains("\n") ||
      self.line.contains("\r") ||
      self.line.contains("\u0000") {
      raise Invalid("status response syntax")
    }
    let prefix = fields[0] + " " + fields[1]
    if !self.line.has_prefix(prefix) {
      raise Invalid("status prefix spacing")
    }
    let rest = self.line[prefix.length():].trim().to_owned()
    if rest.has_prefix("[") {
      let parser : ValueParser = {
        chars: rest.to_array(),
        literals: [],
        at: 1,
        literal_at: 0,
        nodes: 0,
      }
      let code = parser.sequence(Some(']'), 0)
      if code.is_empty() {
        raise Invalid("empty response code")
      }
      if parser.at < parser.chars.length() && parser.chars[parser.at] != ' ' {
        raise Invalid("status code separator")
      }
      return Status(
        fields[0],
        status,
        code,
        String::from_array(parser.chars[parser.at:]).trim().to_owned(),
      )
    }
    return Status(fields[0], status, [], rest)
  }
  if fields[0] != "*" {
    raise Invalid("unknown tagged response status")
  }
  let parser : ValueParser = {
    chars: self.line.to_array(),
    literals: self.literals,
    at: 0,
    literal_at: 0,
    nodes: 0,
  }
  let values = parser.sequence(None, 0)
  if parser.literal_at != self.literals.length() {
    raise Invalid("unbound response literal")
  }
  Data(values)
}

///|
fn numeric_atom(
  value : ImapValue,
  maximum : Int64,
  nonzero? : Bool = false,
) -> Int64 raise ImapError {
  let text = match value {
    Atom(s) => s
    _ => raise Invalid("expected numeric atom")
  }
  if text.is_empty() ||
    !text.iter().all(c => c >= '0' && c <= '9') ||
    (nonzero && text.has_prefix("0")) {
    raise Invalid("invalid numeric atom")
  }
  let n = @strconv.parse_int64(text) catch {
    _ => raise Invalid("numeric overflow")
  }
  if n > maximum || (nonzero && n == 0L) {
    raise Invalid("numeric range")
  }
  n
}

///|
fn atom_name(value : ImapValue) -> String raise ImapError {
  match value {
    Atom(s) => s.to_upper()
    _ => raise Invalid("expected response atom")
  }
}

///|
fn response_flags(value : ImapValue) -> Array[String] raise ImapError {
  match value {
    List(xs) =>
      xs.map(x => {
        match x {
          Atom(s) => s
          _ => raise Invalid("non-atom flag")
        }
      })
    _ => raise Invalid("expected flag list")
  }
}