// Defensive RFC 9309 robots.txt parser.

///|
pub fn parse_robots(input : String) -> Result[RobotsFile, RobotsError] {
  parse_robots_with_limits(input, Limits::default())
}

///|
pub fn parse_robots_with_limits(
  input : String,
  limits : Limits,
) -> Result[RobotsFile, RobotsError] {
  Ok(parse_robots_inner(input, limits)) catch {
    e => Err(unwrap_robots_error(e))
  }
}

///|
fn limit_error(line : Int, offset : Int, context : String) -> RobotsError {
  robots_error(Limit, LimitExceeded, line, offset, context)
}

///|
fn finish_group(
  groups : Array[RobotsGroup],
  agents : Array[String],
  rules : Array[RobotsRule],
  start_line : Int,
  end_line : Int,
  limits : Limits,
  offset : Int,
) -> Unit raise {
  if agents.length() == 0 {
    return
  }
  if groups.length() >= limits.max_groups {
    raise limit_error(start_line, offset, "too many user-agent groups")
  }
  groups.push(robots_group(agents, rules, start_line, end_line))
}

///|
fn parse_robots_inner(input : String, limits : Limits) -> RobotsFile raise {
  let input_bytes = byte_len(input)
  if input_bytes > limits.max_input_bytes {
    raise limit_error(
      1,
      limits.max_input_bytes,
      "input exceeds max_input_bytes",
    )
  }
  if contains_illegal_control(input) {
    raise robots_error(
      Input,
      ControlCharacter,
      1,
      0,
      "input contains an illegal control character",
    )
  }
  let groups : Array[RobotsGroup] = []
  let other_records : Array[OtherRecord] = []
  let mut agents : Array[String] = []
  let mut rules : Array[RobotsRule] = []
  let mut group_start = 0
  let mut group_end = 0
  let mut comments = 0
  let mut blanks = 0
  let lines = input.split("\n").to_array()
  let logical_lines = if input.has_suffix("\n") && lines.length() > 0 {
    lines.length() - 1
  } else {
    lines.length()
  }
  if logical_lines > limits.max_lines {
    raise limit_error(limits.max_lines + 1, 0, "input exceeds max_lines")
  }
  let mut offset = 0
  for index = 0; index < logical_lines; index = index + 1 {
    let line_no = index + 1
    let raw0 = if index == 0 {
      strip_utf8_bom(strip_one_trailing_cr(lines[index].to_owned()))
    } else {
      strip_one_trailing_cr(lines[index].to_owned())
    }
    let raw_bytes = byte_len(raw0)
    if raw_bytes > limits.max_record_bytes {
      raise limit_error(line_no, offset, "record exceeds max_record_bytes")
    }
    comments = comments + inline_comment_count(raw0)
    let clean = strip_comment(raw0).trim().to_owned()
    if clean.length() == 0 {
      blanks = blanks + 1
      offset = offset + byte_len(lines[index].to_owned()) + 1
      continue
    }
    match clean.split_once(":") {
      None => raise robots_error(Line, MissingColon, line_no, offset, clean)
      Some(parts) => {
        let raw_name = parts.0.trim().to_owned()
        let value = parts.1.trim().to_owned()
        if !valid_record_name(raw_name) {
          raise robots_error(Line, InvalidRecordName, line_no, offset, raw_name)
        }
        match record_kind(raw_name) {
          UserAgentRecord => {
            if rules.length() > 0 {
              finish_group(
                groups, agents, rules, group_start, group_end, limits, offset,
              )
              agents = Array::new()
              rules = Array::new()
              group_start = 0
              group_end = 0
            }
            if value.length() == 0 {
              raise robots_error(
                UserAgent,
                EmptyUserAgent,
                line_no,
                offset,
                clean,
              )
            }
            let token = lower_ascii(value)
            if !valid_product_token(token) {
              raise robots_error(
                UserAgent,
                InvalidUserAgent,
                line_no,
                offset,
                value,
              )
            }
            if agents.length() >= limits.max_user_agents_per_group {
              raise limit_error(
                line_no, offset, "too many user-agents in one group",
              )
            }
            agents.push(token)
            if group_start == 0 {
              group_start = line_no
            }
            group_end = line_no
          }
          AllowRecord | DisallowRecord => {
            if agents.length() == 0 {
              raise robots_error(
                Rule,
                RuleBeforeFirstGroup,
                line_no,
                offset,
                clean,
              )
            }
            if byte_len(value) > limits.max_pattern_bytes {
              raise limit_error(
                line_no, offset, "pattern exceeds max_pattern_bytes",
              )
            }
            if has_malformed_percent_encoding(value) {
              raise robots_error(
                Pattern,
                InvalidPercentEncoding,
                line_no,
                offset,
                value,
              )
            }
            if rules.length() >= limits.max_rules_per_group {
              raise limit_error(line_no, offset, "too many rules in one group")
            }
            let kind = if record_kind(raw_name) == AllowRecord {
              Allow
            } else {
              Disallow
            }
            rules.push(robots_rule(kind, value, line_no, offset))
            group_end = line_no
          }
          SitemapRecord | OtherRecordKind =>
            other_records.push(other_record(raw_name, value, line_no, offset))
        }
      }
    }
    offset = offset + byte_len(lines[index].to_owned()) + 1
  }
  finish_group(groups, agents, rules, group_start, group_end, limits, offset)
  robots_file(groups, other_records, comments, blanks, logical_lines)
}