///|
priv struct ParserState {
  blocks : Array[HostBlock]
  items : Array[ConfigItem]
  mut current_directives : Array[Directive]
}

///|
fn new_parser_state(path : String) -> ParserState {
  let origin = location(path, 1, 1)
  let global_directives : Array[Directive] = []
  let global_block : HostBlock = {
    patterns: ["*"],
    directives: global_directives,
    location: origin,
    span: span_from(origin, origin),
  }
  {
    blocks: [global_block],
    items: [Global(global_directives)],
    current_directives: global_directives,
  }
}

///|
fn config_from(state : ParserState) -> Config {
  { items: state.items, blocks: state.blocks }
}

///|
fn line_end(tokens : Array[Lexeme]) -> SourceLocation {
  match tokens.last() {
    Some(token) => token.span.end_
    None => { path: "", line: 1, column: 1 }
  }
}

///|
fn characters_in(value : String) -> Int {
  value.to_array().length()
}

///|
fn split_keyword(token : Lexeme) -> (String, String?, SourceSpan?) {
  match token.value.split_once("=") {
    Some((left, right)) if !left.is_empty() => {
      let keyword = left.to_owned()
      let value = right.to_owned()
      let value_start : SourceLocation = {
        path: token.span.start.path,
        line: token.span.start.line,
        column: token.span.start.column + characters_in(keyword) + 1,
      }
      (keyword, Some(value), Some(span_from(value_start, token.span.end_)))
    }
    _ => (token.value, None, None)
  }
}

///|
fn strip_equals_separator(
  tokens : Array[Lexeme],
  inline_value : String?,
  inline_span : SourceSpan?,
) -> (Array[Lexeme], String?, SourceSpan?) {
  if inline_value is Some(_) || tokens.is_empty() {
    return (tokens, inline_value, inline_span)
  }
  let first = tokens[0]
  if !first.value.has_prefix("=") {
    return (tokens, inline_value, inline_span)
  }
  let value = first.value[1:].to_owned()
  let value_start : SourceLocation = {
    path: first.span.start.path,
    line: first.span.start.line,
    column: first.span.start.column + 1,
  }
  (
    tokens[1:].to_owned(),
    Some(value),
    Some(span_from(value_start, first.span.end_)),
  )
}

///|
fn make_arguments(
  tokens : Array[Lexeme],
  inline_value : String?,
  inline_span : SourceSpan?,
) -> (Array[String], Array[Argument]) {
  let values : Array[String] = []
  let infos : Array[Argument] = []
  match inline_value {
    Some(value) if !value.is_empty() =>
      match inline_span {
        Some(span) => {
          values.push(value)
          infos.push({ value, span })
        }
        None => ()
      }
    _ => ()
  }
  for token in tokens {
    values.push(token.value)
    infos.push({ value: token.value, span: token.span })
  }
  (values, infos)
}

///|
fn add_line(
  state : ParserState,
  tokens : Array[Lexeme],
  raw_line : String,
) -> Unit raise ParseError {
  guard tokens[0] is first else { return }
  let (raw_keyword, inline_value, inline_span) = split_keyword(first)
  let keyword = lower_ascii(raw_keyword)
  let (rest, normalized_inline_value, normalized_inline_span) = strip_equals_separator(
    tokens[1:].to_owned(),
    inline_value,
    inline_span,
  )
  let (arguments, argument_infos) = make_arguments(
    rest, normalized_inline_value, normalized_inline_span,
  )
  let location = first.span.start
  let span = span_from(location, line_end(tokens))
  if arguments.is_empty() || arguments[0].is_empty() {
    raise MissingArgument(location~, keyword~)
  }
  if keyword == "host" {
    if arguments[0].is_empty() {
      raise InvalidBlockHeader(location~, keyword~)
    }
    let directives : Array[Directive] = []
    let block : HostBlock = { patterns: arguments, directives, location, span }
    state.blocks.push(block)
    state.items.push(Host(block))
    state.current_directives = directives
    return
  }
  if keyword == "match" {
    if arguments[0].is_empty() {
      raise InvalidBlockHeader(location~, keyword~)
    }
    let directives : Array[Directive] = []
    let block : MatchBlock = {
      conditions: arguments,
      directives,
      location,
      span,
    }
    state.items.push(Match(block))
    state.current_directives = directives
    return
  }
  state.current_directives.push({
    keyword,
    arguments,
    argument_infos,
    location,
    span,
    raw: Some(raw_line),
  })
}

///|
fn remove_terminal_carriage_return(raw_line : String) -> String {
  if raw_line.has_suffix("\r") {
    raw_line.unsafe_substring(start=0, end=raw_line.length() - 1)
  } else {
    raw_line
  }
}

///|
fn diagnostic_for(error : ParseError) -> Diagnostic {
  match error {
    MissingArgument(location~, ..) =>
      { error, location, summary: "directive requires at least one argument" }
    UnterminatedQuote(location~) =>
      { error, location, summary: "unterminated quoted argument" }
    DanglingEscape(location~) =>
      { error, location, summary: "escape has no following character" }
    UnexpectedNul(location~) =>
      { error, location, summary: "NUL is not valid in SSH configuration" }
    LineTooLong(location~, ..) =>
      { error, location, summary: "configuration line exceeds parser limit" }
    SourceTooLarge(location~, ..) =>
      { error, location, summary: "configuration source exceeds parser limit" }
    InvalidBlockHeader(location~, ..) =>
      { error, location, summary: "invalid section header" }
    InvalidDirective(location~, ..) =>
      { error, location, summary: "invalid configuration syntax" }
  }
}

///|
/// Parse OpenSSH client configuration syntax strictly.
///
/// The returned config preserves input-order items and source spans. Global
/// directives are represented by the leading normalized `ConfigItem::Global`.
pub fn parse(
  source : String,
  path? : String = "",
) -> Config raise ParseError {
  if source_exceeds_byte_limit(source) {
    raise SourceTooLarge(location=location(path, 1, 1), limit=max_source_bytes)
  }
  let state = new_parser_state(path)
  for line_index, raw_view in source.split("\n") {
    let raw_line = remove_terminal_carriage_return(raw_view.to_owned())
    let tokens = lex_line(raw_line, path, line_index + 1)
    if !tokens.is_empty() {
      add_line(state, tokens, raw_line)
    }
  }
  config_from(state)
}

///|
/// Parse configuration while collecting per-line syntax diagnostics.
///
/// Recovery skips the offending line and continues with the following line,
/// making this entry point suitable for linting and editor integrations.
pub fn parse_recovering(
  source : String,
  path? : String = "",
) -> ParseResult {
  let state = new_parser_state(path)
  let diagnostics : Array[Diagnostic] = []
  if source_exceeds_byte_limit(source) {
    diagnostics.push(
      diagnostic_for(
        SourceTooLarge(location=location(path, 1, 1), limit=max_source_bytes),
      ),
    )
    return { config: config_from(state), diagnostics }
  }
  for line_index, raw_view in source.split("\n") {
    let raw_line = remove_terminal_carriage_return(raw_view.to_owned())
    let tokens = lex_line(raw_line, path, line_index + 1) catch {
      error => {
        diagnostics.push(diagnostic_for(error))
        []
      }
    }
    if !tokens.is_empty() {
      add_line(state, tokens, raw_line) catch {
        error => diagnostics.push(diagnostic_for(error))
      }
    }
  }
  { config: config_from(state), diagnostics }
}