// Cloudflare D1 Database bindings

///|
/// D1 Error type for database operations
pub suberror D1Error {
  D1Error(String)
}

///|
pub impl Show for D1Error with fn output(self, logger) {
  match self {
    D1Error(msg) => {
      logger.write_string("D1Error(")
      logger.write_string(msg)
      logger.write_string(")")
    }
  }
}

///|
#external
pub type D1Database

///|
pub fn D1Database::as_any(self : D1Database) -> @core.Any = "%identity"

///|
/// Prepare a SQL statement
pub fn D1Database::prepare(self : Self, query : String) -> D1PreparedStatement {
  self.as_any()._call("prepare", [@core.any(query)]).cast()
}

///|
/// Execute a SQL statement directly (for statements that don't return data)
pub async fn D1Database::exec(
  self : Self,
  query : String,
) -> D1ExecResult raise D1Error {
  let promise : @js_async.Promise[D1ExecResult] = self
    .as_any()
    ._call("exec", [@core.any(query)])
    .cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Batch execute multiple prepared statements in a transaction
pub async fn D1Database::batch(
  self : Self,
  statements : Array[D1PreparedStatement],
) -> Array[D1Result] raise D1Error {
  let stmts_js = @core.new_array()
  let mut i = 0
  while i < statements.length() {
    @core.any(stmts_js)._call("push", [@core.any(statements[i])]) |> ignore
    i = i + 1
  }
  let promise : @js_async.Promise[Array[D1Result]] = self
    .as_any()
    ._call("batch", [@core.any(stmts_js)])
    .cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Dump the entire database (returns ArrayBuffer as Bytes)
pub async fn D1Database::dump(self : Self) -> Bytes raise D1Error {
  let promise : @js_async.Promise[Bytes] = self
    .as_any()
    ._call("dump", [])
    .cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Prepared statement type
#external
pub type D1PreparedStatement

///|
pub fn D1PreparedStatement::as_any(self : D1PreparedStatement) -> @core.Any = "%identity"

///|
/// Bind parameters to a prepared statement
/// Note: This wraps parameters in an array and spreads them to match Cloudflare's API
pub fn D1PreparedStatement::bind(
  self : Self,
  params : Array[@core.Any],
) -> Self {
  // Cloudflare's bind() expects variadic arguments, not an array
  // We need to spread the array when calling bind
  let stmt_js = self.as_any()
  let bind_fn = stmt_js["bind"]

  // Convert MoonBit array to JavaScript array
  let params_js = @core.new_array()
  let mut i = 0
  while i < params.length() {
    @core.any(params_js)._call("push", [@core.any(params[i])]) |> ignore
    i = i + 1
  }

  // Use apply to spread the array as individual arguments
  bind_fn._call("apply", [stmt_js, @core.any(params_js)]).cast()
}

///|
/// Bind a single parameter
pub fn D1PreparedStatement::bind1(self : Self, param : @core.Any) -> Self {
  self.bind([param])
}

///|
/// Bind two parameters
pub fn D1PreparedStatement::bind2(
  self : Self,
  p1 : @core.Any,
  p2 : @core.Any,
) -> Self {
  self.bind([p1, p2])
}

///|
/// Bind three parameters
pub fn D1PreparedStatement::bind3(
  self : Self,
  p1 : @core.Any,
  p2 : @core.Any,
  p3 : @core.Any,
) -> Self {
  self.bind([p1, p2, p3])
}

///|
/// Execute the statement and return first row
pub async fn D1PreparedStatement::first(
  self : Self,
) -> @core.Any? raise D1Error {
  let promise : @js_async.Promise[@core.Any] = self
    .as_any()
    ._call("first", [])
    .cast()
  try {
    let result = promise.wait()
    if @core.is_null(result) || @core.typeof_(result) == "undefined" {
      None
    } else {
      Some(result)
    }
  } catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Execute the statement and return first column value
pub async fn D1PreparedStatement::first_col(
  self : Self,
  col_name : String,
) -> @core.Any? raise D1Error {
  let promise : @js_async.Promise[@core.Any] = self
    .as_any()
    ._call("first", [@core.any(col_name)])
    .cast()
  try {
    let result = promise.wait()
    if @core.is_null(result) || @core.typeof_(result) == "undefined" {
      None
    } else {
      Some(result)
    }
  } catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Execute the statement and return all rows
pub async fn D1PreparedStatement::all(self : Self) -> D1Result raise D1Error {
  let promise : @js_async.Promise[D1Result] = self
    .as_any()
    ._call("all", [])
    .cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Execute the statement (for INSERT, UPDATE, DELETE)
pub async fn D1PreparedStatement::run(self : Self) -> D1Result raise D1Error {
  let promise : @js_async.Promise[D1Result] = self
    .as_any()
    ._call("run", [])
    .cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Execute and return raw results
pub async fn D1PreparedStatement::raw(
  self : Self,
  columnNames? : Bool,
) -> Array[@core.Any] raise D1Error {
  let promise_any = if columnNames is None {
    self.as_any()._call("raw", [])
  } else {
    let entries : Array[(String, @core.Any)] = []
    if columnNames is Some(v) {
      entries.push(("columnNames", @core.any(v)))
    }
    let opts = @core.from_entries(entries)
    self.as_any()._call("raw", [opts])
  }
  let promise : @js_async.Promise[Array[@core.Any]] = promise_any.cast()
  promise.wait() catch {
    err => raise D1Error(err.to_string())
  }
}

///|
/// Result of a D1 query (external type to properly handle JS object properties)
#external
pub type D1Result

///|
pub fn D1Result::as_any(self : D1Result) -> @core.Any = "%identity"

///|
/// Check if the query was successful
pub fn D1Result::success(self : D1Result) -> Bool {
  self.as_any()["success"].cast()
}

///|
/// Get the error message if any
pub fn D1Result::error(self : D1Result) -> String? {
  let err = self.as_any()["error"]
  if @core.typeof_(err) == "undefined" || @core.is_null(err) {
    None
  } else {
    Some(err.cast())
  }
}

///|
/// Get the query metadata
pub fn D1Result::meta(self : D1Result) -> D1Meta? {
  let meta = self.as_any()["meta"]
  if @core.typeof_(meta) == "undefined" || @core.is_null(meta) {
    None
  } else {
    Some(meta.cast())
  }
}

///|
/// Get results from D1Result as a raw JS array
pub fn D1Result::results_raw(self : D1Result) -> @core.Any {
  let results = self.as_any()["results"]
  if @core.typeof_(results) == "undefined" || @core.is_null(results) {
    @core.new_array()
  } else {
    results
  }
}

///|
/// Get results from D1Result
///
/// Note: The returned array is a snapshot and should be treated as immutable.
pub fn D1Result::get_results(self : D1Result) -> Array[@core.Any] {
  self.results_raw().cast()
}

///|
/// Metadata about query execution (external type)
#external
pub type D1Meta

///|
pub fn D1Meta::as_any(self : D1Meta) -> @core.Any = "%identity"

///|
/// Query duration in milliseconds
pub fn D1Meta::duration(self : D1Meta) -> Double? {
  let v = self.as_any()["duration"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Number of rows read
pub fn D1Meta::rows_read(self : D1Meta) -> Int? {
  let v = self.as_any()["rows_read"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Number of rows written
pub fn D1Meta::rows_written(self : D1Meta) -> Int? {
  let v = self.as_any()["rows_written"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Last inserted row ID
pub fn D1Meta::last_row_id(self : D1Meta) -> Int? {
  let v = self.as_any()["last_row_id"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Whether database was changed
pub fn D1Meta::changed_db(self : D1Meta) -> Bool? {
  let v = self.as_any()["changed_db"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Number of rows changed
pub fn D1Meta::changes(self : D1Meta) -> Int? {
  let v = self.as_any()["changes"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Database size after query
pub fn D1Meta::size_after(self : D1Meta) -> Int? {
  let v = self.as_any()["size_after"]
  if @core.typeof_(v) == "undefined" || @core.is_null(v) {
    None
  } else {
    Some(v.cast())
  }
}

///|
/// Result of exec operation (external type)
#external
pub type D1ExecResult

///|
pub fn D1ExecResult::as_any(self : D1ExecResult) -> @core.Any = "%identity"

///|
/// Number of statements executed
pub fn D1ExecResult::count(self : D1ExecResult) -> Int {
  self.as_any()["count"].cast()
}

///|
/// Total duration in milliseconds
pub fn D1ExecResult::duration(self : D1ExecResult) -> Double {
  self.as_any()["duration"].cast()
}

///|
/// Legacy type alias for compatibility
pub type D1ResultSet = D1Result

// ============================================================================
// Type conversion utilities for D1 row data
// ============================================================================

///|
/// Convert a JavaScript number from D1 result to MoonBit Int64
/// D1 returns SQLite INTEGER as JavaScript numbers, but MoonBit Int64
/// uses { hi, lo } representation in JS target.
pub fn js_number_to_int64(value : @core.Any) -> Int64 {
  js_number_to_int64_ffi(value)
}

///|
extern "js" fn js_number_to_int64_ffi(value : @core.Any) -> Int64 =
  #| (n) => {
  #|   // JavaScript number to MoonBit Int64 ({hi, lo} representation)
  #|   if (typeof n !== 'number') n = Number(n);
  #|   // Handle negative numbers correctly
  #|   if (n < 0) {
  #|     // Two's complement for negative numbers
  #|     const abs = Math.abs(n);
  #|     const lo = (~(abs >>> 0) + 1) >>> 0;
  #|     const hi = (~Math.floor(abs / 0x100000000) + (lo === 0 ? 1 : 0)) >>> 0;
  #|     return { hi, lo };
  #|   }
  #|   return { hi: Math.floor(n / 0x100000000) >>> 0, lo: (n >>> 0) };
  #| }

///|
/// Convert a JavaScript number from D1 result to MoonBit Int (32-bit)
pub fn js_number_to_int(value : @core.Any) -> Int {
  js_number_to_int_ffi(value)
}

///|
extern "js" fn js_number_to_int_ffi(value : @core.Any) -> Int =
  #| (n) => (typeof n === 'number' ? n : Number(n)) | 0