// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// The error surface: the HTTP interface answers a rejected statement with status 4xx/5xx
// and a body that starts `Code: N. DB::Exception: …`, and puts the same number in
// `X-ClickHouse-Exception-Code`. Mapping that number onto `ErrorKind` is what lets a
// caller branch on the class without parsing the message.

///|
/// The `DbError` a failed response becomes.
fn error_of(response : Response) -> @moondb.DbError {
  let body = @utf8.decode(response.body) catch { _ => "" }
  let code = match exception_code(response, body) {
    Some(n) => Some(n)
    None => None
  }
  let message = exception_message(body)
  let kind = match code {
    Some(n) => kind_of(n)
    None =>
      if response.status >= 500 {
        @moondb.ErrorKind::Operational
      } else {
        @moondb.ErrorKind::Database
      }
  }
  let text = if message == "" {
    let body_text = @utf8.decode(response.body) catch { _ => "" }
    let trimmed = body_text.trim().to_owned()
    "HTTP " +
    response.status.to_string() +
    (if trimmed == "" {
      " with an empty body"
    } else {
      ": " + clip(trimmed, 300)
    })
  } else {
    message
  }
  @moondb.Server({
    kind,
    sqlstate: None,
    code,
    message: text,
    detail: None,
    hint: None,
    severity: None,
  })
}

///|
/// The numeric code, from the header the server sets or from the body's first line.
fn exception_code(response : Response, body : String) -> Int32? {
  match response.header("X-ClickHouse-Exception-Code") {
    Some(text) => return parse_code(text.trim().to_owned())
    None => ()
  }
  if body.has_prefix("Code: ") {
    let rest = body[6:].to_owned()
    match rest.find(".") {
      Some(at) => parse_code(rest[:at].trim().to_owned())
      None => None
    }
  } else {
    None
  }
}

///|
/// The first `limit` characters of `text`, so a server that echoes a whole statement
/// back cannot turn one error into a wall of text.
fn clip(text : String, limit : Int32) -> String {
  if text.length() <= limit {
    text
  } else {
    text[:limit].to_owned() + "…"
  }
}

///|
/// A number the server sent, or `None` when the field is not one.
fn parse_code(text : String) -> Int32? {
  let value = @string.parse_int(text) catch { _ => return None }
  Some(value)
}

///|
/// The server's own sentence, without the `Code: N. DB::Exception: ` prefix.
fn exception_message(body : String) -> String {
  let text = if body.has_prefix("Code: ") {
    match body.find(". ") {
      Some(at) => body[at + 2:].to_owned()
      None => body
    }
  } else {
    body
  }
  let trimmed = match text.find("DB::Exception: ") {
    Some(at) => text[at + "DB::Exception: ".length():].to_owned()
    None => text
  }
  trimmed.trim().to_owned()
}

///|
/// The failure class for a ClickHouse error code. The numbers are the server's
/// `ErrorCodes`, grouped the way the interface groups its own error kinds: a syntax or
/// reference error is the program's fault, a limit is the environment's, a constraint
/// is the data's.
fn kind_of(code : Int32) -> @moondb.ErrorKind {
  match code {
    // UNKNOWN_TABLE, UNKNOWN_IDENTIFIER, UNKNOWN_FUNCTION, UNKNOWN_DATABASE,
    // UNKNOWN_TYPE, ILLEGAL_TYPE_OF_ARGUMENT, NUMBER_OF_ARGUMENTS_DOESNT_MATCH,
    // SYNTAX_ERROR, BAD_ARGUMENTS, UNSUPPORTED_METHOD, …
    47 | 60 | 46 | 81 | 50 | 43 | 42 | 62 | 36 | 89 | 44 =>
      @moondb.ErrorKind::Programming
    // MEMORY_LIMIT_EXCEEDED, TOO_MANY_SIMULTANEOUS_QUERIES, SOCKET_TIMEOUT,
    // TIMEOUT_EXCEEDED, TOO_MANY_PARTS, NO_FREE_CONNECTION, …
    241 | 202 | 209 | 159 | 252 | 18 => @moondb.ErrorKind::Operational
    // AUTHENTICATION_FAILED, ACCESS_DENIED
    516 | 497 => @moondb.ErrorKind::Operational
    // CANNOT_PARSE_TEXT, VALUE_IS_OUT_OF_RANGE, CANNOT_PARSE_NUMBER, …
    6 | 69 | 72 | 70 => @moondb.ErrorKind::Data
    // CONSTRAINT_VIOLATED (a check constraint, not a unique key)
    469 => @moondb.ErrorKind::Integrity
    // NOT_IMPLEMENTED
    48 => @moondb.ErrorKind::NotSupported
    _ => @moondb.ErrorKind::Database
  }
}