///|
/// Errors raised by the driver.
pub suberror DbError {
  /// Server rejected the request. `code` is ClickHouse's exception code
  /// (`Code: N.` / `X-ClickHouse-Exception-Code`) when the server reported
  /// one, otherwise the HTTP status; `name` is `"DB::Exception"` or
  /// `"HTTPError"` accordingly.
  ServerError(code~ : Int32, name~ : String, message~ : String)
  /// Connection-level error (I/O, malformed response, unexpected EOF)
  ConnectionError(String)
} derive(Debug)

///|
/// Re-export the derived `Debug` method explicitly (MoonBit no longer
/// promotes trait impl methods to inherent methods implicitly).
pub extend DbError with @debug.Debug::{to_repr}

///|
/// 构造服务器错误(供 native 等子包复用)。
pub fn DbError::server_error(
  code~ : Int32,
  name~ : String,
  message~ : String,
) -> DbError {
  ServerError(code~, name~, message~)
}

///|
/// 构造连接错误(供 native 等子包复用)。
pub fn DbError::connection_error(message : String) -> DbError {
  ConnectionError(message)
}

///|
/// Trim ClickHouse error response to a single-line summary.
/// Bodies often look like: "Code: 60. DB::Exception: Table doesn't exist..."
fn extract_error(body : String) -> String {
  // Take first 500 chars or so for a readable message
  if body.length() <= 500 {
    body
  } else {
    substr(body, 0, 500)
  }
}

///|
/// Build a `ServerError` from an HTTP error response.
///
/// ClickHouse reports the real exception code in the
/// `X-ClickHouse-Exception-Code` response header and in the body's
/// `Code: N.` prefix; the HTTP status is only a transport-level fallback.
fn http_error(
  status : Int32,
  headers : Map[String, String],
  body : String,
) -> DbError {
  let message = extract_error(body)
  let parsed = match headers.get("x-clickhouse-exception-code") {
    Some(value) =>
      match parse_leading_int(value) {
        Some(code) => Some(code)
        None => parse_clickhouse_error_code(body)
      }
    None => parse_clickhouse_error_code(body)
  }
  match parsed {
    Some(code) => ServerError(code~, name="DB::Exception", message~)
    None => ServerError(code=status, name="HTTPError", message~)
  }
}

///|
/// Parse the `Code: .` prefix of a ClickHouse exception body.
fn parse_clickhouse_error_code(body : String) -> Int32? {
  let prefix = "Code: "
  guard body.has_prefix(prefix) else { return None }
  parse_leading_int(body[6:].to_owned())
}

///|
/// Parse the leading ASCII digits of `s`; `None` when there are none.
fn parse_leading_int(s : String) -> Int32? {
  let mut value = 0
  let mut any = false
  for c in s {
    if c >= '0' && c <= '9' {
      value = value * 10 + (c.to_int() - '0'.to_int())
      any = true
    } else {
      break
    }
  }
  if any {
    Some(value)
  } else {
    None
  }
}