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

///|
/// A statement DuckDB prepared once. DuckDB's `PREPARE` survives across runs, so a
/// repeated `INSERT` binds new values to the same plan instead of being parsed again —
/// which for an analytical engine is where most of the per-statement cost sits.
pub struct DuckdbStmt {
  handle : Int64
  sql : String
  mut params : Array[@moondb.Value]
  mut closed : Bool
}

///|
/// Prepare `sql` on `connection`.
pub fn DuckdbStmt::new(
  connection : Int64,
  sql : String,
) -> DuckdbStmt raise @moondb.DbError {
  let handle = ffi_prepare(connection, @utf8.encode(sql))
  if handle == 0L {
    raise error_of(bytes_to_str(ffi_prepare_message(0L)))
  }
  { handle, sql, params: [], closed: false, }
}

///|
fn DuckdbStmt::ensure_open(self : DuckdbStmt) -> Unit raise @moondb.DbError {
  if self.closed {
    raise @moondb.Closed
  }
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn bind(self, params) {
  self.params = params
}

///|
fn DuckdbStmt::bind_all(self : DuckdbStmt) -> Unit raise @moondb.DbError {
  for i = 0; i < self.params.length(); i = i + 1 {
    let value = self.params[i]
    let (kind, i64, d, bool, bytes) = match value {
      @moondb.Null => (0, 0L, 0.0, 0, b"")
      @moondb.Bool(v) => (3, 0L, 0.0, if v { 1 } else { 0 }, b"")
      @moondb.Int(v) => (1, v.to_int64(), 0.0, 0, b"")
      @moondb.Int64(v) => (1, v, 0.0, 0, b"")
      @moondb.Double(v) => (2, 0L, v, 0, b"")
      @moondb.Text(v) => (4, 0L, 0.0, 0, @utf8.encode(v))
      @moondb.Time(v) => (4, 0L, 0.0, 0, @utf8.encode(v))
      @moondb.Numeric(v) => (4, 0L, 0.0, 0, @utf8.encode(v))
      @moondb.Json(v) => (4, 0L, 0.0, 0, @utf8.encode(v))
      @moondb.Uuid(v) => (5, 0L, 0.0, 0, v)
      @moondb.Blob(v) => (5, 0L, 0.0, 0, v)
      // DuckDB has no array binding through this API; its array column type is read
      // back as text, so a caller that needs to write one passes the literal.
      @moondb.Array(_) =>
        raise @moondb.TypeError(
          "a DuckDB array parameter is not supported; bind its text form instead",
        )
    }
    let rc = ffi_bind(self.handle, i + 1, kind, i64, d, bool, bytes)
    if rc != 0 {
      raise @moondb.TypeError(
        "cannot bind parameter " +
        (i + 1).to_string() +
        ": " +
        bytes_to_str(ffi_prepare_message(self.handle)),
      )
    }
  }
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn execute(self) {
  self.ensure_open()
  self.bind_all()
  let changed = ffi_exec_prepared(self.handle)
  if changed < 0L {
    raise error_of(bytes_to_str(ffi_error()))
  }
  { rows_affected: changed, last_insert_id: 0L, }
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn query(self) {
  self.ensure_open()
  self.bind_all()
  read_result(ffi_query_prepared(self.handle))
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn close(self) {
  if !self.closed {
    self.closed = true
    ffi_prepare_close(self.handle)
  }
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn sql(self) {
  self.sql
}

///|
pub impl @moondb.Stmt for DuckdbStmt with fn params(_self) {
  []
}

///|
pub extend DuckdbStmt with @moondb.Stmt::{
  bind,
  bind_named,
  execute,
  query,
  query_stream,
  close,
  sql,
  params,
}