///|
/// Parse a string as Int (best effort), returning 0 on failure.
fn parse_int_or(s : String, fallback : Int32) -> Int32 {
  let s = s.trim()
  if s.length() == 0 {
    return fallback
  }
  let mut result = 0
  let mut idx = 0
  let mut neg = false
  if s[0] == '-' {
    neg = true
    idx = 1
  } else if s[0] == '+' {
    idx = 1
  }
  if idx >= s.length() {
    return fallback
  }
  let mut ok = true
  while idx < s.length() {
    let c = s[idx].to_int() - '0'.to_int()
    if c >= 0 && c <= 9 {
      result = result * 10 + c
    } else {
      ok = false
      break
    }
    idx = idx + 1
  }
  if ok {
    if neg {
      -result
    } else {
      result
    }
  } else {
    fallback
  }
}

///|
/// ClickHouse >= 25 requires `{name: Identifier}` for identifiers (table
/// names); older versions accept `{name}` / `{name: String}` instead.
async fn supports_identifier_param(conn : @lib.Connection) -> Bool {
  try {
    let r = conn.execute_query("SELECT version() AS v")
    if r.rows.length() == 0 {
      return false
    }
    let version = r.rows[0].values[0]
    let mut major = 0
    for ch in version.iter() {
      if ch == '.' {
        break
      }
      let c = ch.to_int() - '0'.to_int()
      if c >= 0 && c <= 9 {
        major = major * 10 + c
      } else {
        break
      }
    }
    major >= 25
  } catch {
    _ => false
  }
}

///|
/// ClickHouse HTTP driver demo: comprehensive test of the current features.
/// Run with `moon run cmd/main`.
///
/// Single unified API: `execute_query(sql, params?)` covers SELECT / DDL /
/// inline-VALUES INSERT; `insert()` streams bulk rows as the HTTP body;
/// `execute_stream()` reads large result sets row by row.
async fn main {
  // 原生协议(TCP 9000)演示:CLICKHOUSE_NATIVE=1 时走 native 分支
  if @env.get_env_var("CLICKHOUSE_NATIVE") == Some("1") {
    run_native_demo()
    return
  }
  // Connection settings come from env vars (CLICKHOUSE_HOST / CLICKHOUSE_PORT /
  // CLICKHOUSE_USER / CLICKHOUSE_PASSWORD / CLICKHOUSE_DATABASE) with sensible
  // defaults, so the same demo works against any ClickHouse:
  //   CLICKHOUSE_PORT=38123 moon run cmd/main
  let host = @env.get_env_var("CLICKHOUSE_HOST").unwrap_or("127.0.0.1")
  let port = match @env.get_env_var("CLICKHOUSE_PORT") {
    Some(p) => parse_int_or(p, 8123)
    None => 8123
  }
  let user = @env.get_env_var("CLICKHOUSE_USER").unwrap_or("default")
  let password = @env.get_env_var("CLICKHOUSE_PASSWORD").unwrap_or("barn")
  let database = @env.get_env_var("CLICKHOUSE_DATABASE").unwrap_or("default")
  let conn = @lib.connect(
    host~,
    port~,
    user~,
    password~,
    database~,
    client_name="moonbit-cli",
  )
  defer conn.close()

  // ---- 0. Verify connection config ----
  println(
    "[0] connected to " +
    conn.host +
    ":" +
    conn.port.to_string() +
    " db=" +
    conn.database +
    " user=" +
    conn.user,
  )

  try {
    // ---- 0b. Reset demo tables (idempotent reruns) ----
    ignore(conn.execute_query("DROP TABLE IF EXISTS demo_drv"))
    ignore(conn.execute_query("DROP TABLE IF EXISTS demo_drv_ident"))

    // ---- 1. Health check ----
    conn.ping()
    println("[1] ping ok")

    // ---- 2. Cancel is a no-op over HTTP ----
    conn.cancel()
    println("[2] cancel() called (HTTP: no-op)")

    // ---- 3. SELECT with named parameters ----
    let r_sel = conn.execute_query(
      "SELECT {x: UInt32} AS a, {s} AS b, {t: DateTime} AS t",
      params=Map::from_array([
        ("x", "1"),
        ("s", "hi"),
        ("t", "2024-01-01 00:00:00"),
      ]),
    )
    println(
      "[3] SELECT with params: rows=" +
      r_sel.rows.length().to_string() +
      " cols=" +
      r_sel.columns.length().to_string(),
    )
    for row in r_sel.rows {
      println(
        "    a=" + row.values[0] + " b=" + row.values[1] + " t=" + row.values[2],
      )
    }
    if r_sel.columns.length() > 0 {
      println(
        "    schema: " +
        r_sel.columns[0].name +
        ":" +
        r_sel.columns[0].type_ +
        ", " +
        r_sel.columns[1].name +
        ":" +
        r_sel.columns[1].type_,
      )
    }

    // ---- 4. DDL: CREATE TABLE (no params needed) ----
    let ddl =
      #|CREATE TABLE IF NOT EXISTS demo_drv (
      #|  id   Int32,
      #|  name String,
      #|  ts   DateTime DEFAULT now()
      #|) ENGINE = MergeTree()
      #|ORDER BY id
    ignore(conn.execute_query(ddl))
    println("[4] CREATE TABLE demo_drv ok")

    // ---- 5. INSERT — same `?` placeholder convention as other DB drivers.
    ignore(
      conn.execute(
        "INSERT INTO demo_drv (id, name, ts) VALUES " +
        "(?, ?, ?), (?, ?, ?), (?, ?, ?)",
        [
          "1", "alice", "2024-01-01 00:00:00", "2", "bob", "2024-01-01 00:00:01",
          "3", "carol", "2024-06-15 12:30:00",
        ],
      ),
    )
    println("[5] INSERT demo_drv ok (3 rows via positional `?`)")

    // ---- 5b. INSERT via insert() — bulk rows streamed as TSV body ----
    let bulk_rows : Array[Array[String]] = [
      ["4", "dave", "2024-06-15 12:30:01"],
      ["5", "erin", "2024-06-15 12:30:02"],
      ["6", "frank", "2024-06-15 12:30:03"],
    ]
    conn.insert("demo_drv", ["id", "name", "ts"], bulk_rows)
    println("[5b] insert() ok (3 rows via streamed TSV body)")

    // ---- 5c. insert() with JSONEachRow (Ndjson) format ----
    let nd_rows : Array[Array[String]] = [
      ["7", "grace", "2024-06-15 12:30:04"],
      ["8", "heidi", "2024-06-15 12:30:05"],
    ]
    conn.insert(
      "demo_drv",
      ["id", "name", "ts"],
      nd_rows,
      format=@lib.InsertFormat::Ndjson,
    )
    println("[5c] insert() ok (2 rows via JSONEachRow body)")

    // ---- 5d. execute_stream — row-by-row reading (no full buffering) ----
    let cur = conn.execute_stream("SELECT id, name FROM demo_drv ORDER BY id")
    defer cur.close()
    // Schema is available immediately, before any row is read.
    let schema = cur.columns().map(fn(c) { c.name + ":" + c.type_ }).join(", ")
    println("[5d] execute_stream() schema: " + schema)
    let mut stream_count = 0
    let mut stream_sum = 0
    while true {
      match cur.next() {
        None => break
        Some(row) => {
          stream_count = stream_count + 1
          stream_sum = stream_sum + parse_int_or(row.values[0], 0)
        }
      }
    }
    println(
      "[5d] execute_stream() rows=" +
      stream_count.to_string() +
      " id-sum=" +
      stream_sum.to_string(),
    )

    // ---- 6. SELECT verify INSERT (with WHERE filter via param) ----
    let r2 = conn.execute_query(
      "SELECT id, name, ts FROM demo_drv WHERE id >= {lo: Int32} ORDER BY id",
      params=Map::from_array([("lo", "0")]),
    )
    println(
      "[6] SELECT demo_drv WHERE id >= {lo}: rows=" +
      r2.rows.length().to_string(),
    )
    for row in r2.rows {
      println(
        "    id=" +
        row.values[0] +
        " name=" +
        row.values[1] +
        " ts=" +
        row.values[2],
      )
    }

    // ---- 7. SELECT with to_map() ----
    let rows_map = r2.to_map()
    println("[7] to_map() result:")
    for m in rows_map {
      println(
        "    " +
        m.get_or_default("id", "?") +
        " -> " +
        m.get_or_default("name", "?"),
      )
    }

    // ---- 8. ServerError: nonexistent table ----
    // The `{tn}` placeholder binds a table name; ClickHouse >= 25 needs the
    // explicit `Identifier` type for identifier positions.
    let tn_placeholder = if supports_identifier_param(conn) {
      "{tn: Identifier}"
    } else {
      "{tn}"
    }
    try {
      ignore(
        conn.execute_query(
          "SELECT * FROM " + tn_placeholder,
          params=Map::from_array([("tn", "system.no_such_table_xyz")]),
        ),
      )
      println("[8] UNEXPECTED: query succeeded")
    } catch {
      @lib.DbError::ServerError(code~, name=_, message~) =>
        println(
          "[8] caught ServerError(code=" + code.to_string() + "): " + message,
        )
      @lib.DbError::ConnectionError(msg) =>
        println("[8] caught ConnectionError: " + msg)
      _ as e => println("[8] caught other: " + e.to_string())
    }

    // ---- 9. Syntax error ----
    try {
      ignore(conn.execute_query("SLECT 1")) // typo: SLECT
      println("[9] UNEXPECTED: syntax error not caught")
    } catch {
      @lib.DbError::ServerError(code~, name=_, message~) =>
        println(
          "[9] caught syntax error (code=" + code.to_string() + "): " + message,
        )
      @lib.DbError::ConnectionError(msg) =>
        println("[9] caught ConnectionError: " + msg)
      _ => println("[9] caught unknown")
    }

    // ---- 10. Ping after errors ----
    conn.ping()
    println("[10] ping after errors ok")

    // ---- 11. compress=true — LZ4-compressed responses ----
    // ClickHouse compresses the response body (`compress=1`); the driver
    // decodes the LZ4 blocks transparently. Use it for large result sets.
    let conn_c = @lib.connect(
      host~,
      port~,
      user~,
      password~,
      database~,
      client_name="moonbit-cli-compress",
      compress=true,
    )
    defer conn_c.close()
    let r_c = conn_c.execute_query(
      "SELECT number, randomPrintableASCII(64) FROM numbers(5000)",
    )
    println(
      "[11] compress=true: rows=" +
      r_c.rows.length().to_string() +
      " (LZ4 decompressed)",
    )

    // ---- 12. timeout_ms — bound every request ----
    // A positive timeout cancels slow requests and raises ConnectionError.
    let conn_t = @lib.connect(
      host~,
      port~,
      user~,
      password~,
      database~,
      client_name="moonbit-cli-timeout",
      timeout_ms=2000,
    )
    defer conn_t.close()
    try {
      ignore(conn_t.execute_query("SELECT sleep(3)"))
      println("[12] UNEXPECTED: sleep(3) finished within 2000ms")
    } catch {
      @lib.DbError::ConnectionError(msg) =>
        println("[12] timeout demo (expected): " + msg)
      @lib.DbError::ServerError(code=_, name=_, message~) =>
        println("[12] ServerError instead of timeout: " + message)
      _ as e => println("[12] other: " + e.to_string())
    }

    // ---- 13. https=true — TLS connection ----
    // Point `port` at a TLS-enabled ClickHouse (e.g. 8443 with HTTPS) and the
    // same API works over TLS. `skip_verify=true` disables certificate
    // validation for self-signed setups. Against the local plain-HTTP server
    // the handshake fails — the driver surfaces it as a connection error.
    let conn_tls = @lib.connect(
      host~,
      port~,
      user~,
      password~,
      database~,
      client_name="moonbit-cli-tls",
      https=true,
      skip_verify=true,
    )
    defer conn_tls.close()
    try {
      conn_tls.ping()
      println("[13] https ping ok (server has TLS)")
    } catch {
      @lib.DbError::ConnectionError(msg) =>
        println("[13] https attempt on plain-HTTP server (expected): " + msg)
      _ as e =>
        println(
          "[13] https attempt failed (server has no TLS): " + e.to_string(),
        )
    }

    // ---- 14. ConnectionPool — reuse keep-alive connections ----
    // `conn.pool(size)` keeps up to `size` connections and reuses them across
    // requests; extra concurrent requests wait for a free connection.
    let pool = conn.pool(size=3)
    defer pool.close()
    pool.ping()
    let r_p = pool.execute_query("SELECT number FROM numbers(3)")
    println("[14] pool execute_query: rows=" + r_p.rows.length().to_string())
    // Three concurrent queries share the pool's 3 connections.
    @async.with_task_group() <| group => {
      for _ in 0..<3 {
        ignore(
          group.spawn(() => {
            let rr = pool.execute_query("SELECT count() FROM numbers(1000)")
            println("    pool task rows=" + rr.rows.length().to_string())
          }),
        )
      }
    }
    // Streaming through the pool: the connection is returned on close().
    let cur_p = pool.execute_stream("SELECT number FROM numbers(5)")
    let mut pn = 0
    while true {
      match cur_p.next() {
        None => break
        Some(_) => pn = pn + 1
      }
    }
    cur_p.close()
    println("[14] pool execute_stream rows=" + pn.to_string())

    // ---- 15. Aggregation (GROUP BY) ----
    let r_agg = conn.execute_query(
      "SELECT name, count() AS n FROM demo_drv GROUP BY name ORDER BY n DESC LIMIT 3",
    )
    println("[15] GROUP BY top-3:")
    for row in r_agg.rows {
      println("    " + row.values[0] + " -> " + row.values[1] + " row(s)")
    }

    // ---- 16. insert() with a large generated batch ----
    // The point of insert() is bulk loading: 10k rows streamed as the HTTP
    // body in one request (no giant VALUES literal).
    let big_rows : Array[Array[String]] = []
    for i in 0..<10000 {
      big_rows.push([
        i.to_string(),
        "name_" + i.to_string(),
        "2024-06-16 00:00:00",
      ])
    }
    conn.insert("demo_drv", ["id", "name", "ts"], big_rows)
    let r_big = conn.execute_query("SELECT count() AS c FROM demo_drv")
    println(
      "[16] insert() 10k rows streamed; total rows=" + r_big.rows[0].values[0],
    )

    // ---- 17. Identifier parameter — bind a table name ----
    // ClickHouse >= 25 requires `{name: Identifier}` for identifiers (older
    // versions accept the untyped `{t}` form). CREATE TABLE keeps a literal
    // name because CREATE + Identifier substitution is buggy in some
    // ClickHouse versions; SELECT and DROP use the bound identifier.
    let id_tbl = "demo_drv_ident"
    let id_ph = if supports_identifier_param(conn) {
      "{t: Identifier}"
    } else {
      "{t}"
    }
    ignore(
      conn.execute_query(
        "CREATE TABLE IF NOT EXISTS demo_drv_ident (x Int32) ENGINE = Memory",
      ),
    )
    ignore(
      conn.execute_query(
        "INSERT INTO demo_drv_ident VALUES ({x: Int32})",
        params=Map::from_array([("x", "42")]),
      ),
    )
    let r_id = conn.execute_query(
      "SELECT sum(x) AS s FROM " + id_ph,
      params=Map::from_array([("t", id_tbl)]),
    )
    println("[17] identifier param sum(x)=" + r_id.rows[0].values[0])
    ignore(
      conn.execute_query(
        "DROP TABLE IF EXISTS " + id_ph,
        params=Map::from_array([("t", id_tbl)]),
      ),
    )
    // ---- Cleanup ----
    ignore(conn.execute_query("DROP TABLE IF EXISTS demo_drv"))
    println("[cleanup] DROP TABLE demo_drv ok")
  } catch {
    @lib.DbError::ServerError(code~, name=_, message~) =>
      println("TOP-LEVEL ServerError: code=" + code.to_string() + " " + message)
    @lib.DbError::ConnectionError(msg) =>
      println("TOP-LEVEL ConnectionError: " + msg)
    _ as e => println("TOP-LEVEL unknown: " + e.to_string())
  }

  println("All tests completed.")
}