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

///|
/// One SQL text compiled by the server, run with new values on every call.
///
/// The handle comes from `sp_prepare` and is released by `sp_unprepare`; between the two,
/// a run is a `sp_execute` RPC that sends the handle and the values and nothing else — the
/// statement text does not travel again.
///
/// A statement's declaration is part of what the server compiles: `sp_prepare` is told the
/// type of every parameter, and those types come from the values. So a handle is compiled
/// at the first `bind` that has values (or immediately, for a statement with no
/// placeholders), and a later `bind` whose values declare different types drops the old
/// handle and compiles a new one rather than sending values the plan did not expect.
pub struct SqlServerStmt {
  conn : SqlServerConn
  sql : String
  mut params : Array[@moondb.Value]
  /// The server's handle, or 0 while nothing has been compiled.
  mut handle : Int32
  /// The declaration list the live handle was compiled with.
  mut declaration : String
  mut closed : Bool
}

///|
pub fn SqlServerStmt::new(conn : SqlServerConn, sql : String) -> SqlServerStmt {
  { conn, sql, params: [], handle: 0, declaration: "", closed: false, }
}

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

///|
/// Compile the statement if it has no live handle for the values now bound. A statement
/// with no placeholders can be compiled the moment it is prepared; one with placeholders
/// has to wait for the values, because their types are part of what gets compiled.
async fn SqlServerStmt::ensure_plan(
  self : SqlServerStmt,
) -> Unit raise @moondb.DbError {
  let slots = count_placeholders(self.sql)
  if slots != self.params.length() {
    raise @moondb.TypeError(
      "statement has " +
      slots.to_string() +
      " placeholders but " +
      self.params.length().to_string() +
      " values",
    )
  }
  let declaration = declaration_list(self.params)
  if self.handle != 0 && self.declaration == declaration {
    return
  }
  let stale = self.handle
  self.handle = 0
  self.declaration = ""
  if stale != 0 {
    self.conn.unprepare(stale) catch {
      _ => () // the handle is dead either way; a failed release is not this call's news
    }
  }
  self.handle = self.conn.prepare_plan(self.sql, declaration)
  self.declaration = declaration
}

///|
pub impl @moondb.AsyncStmt for SqlServerStmt with fn bind(self, params) {
  self.check_open()
  self.params = params
  self.ensure_plan()
}

///|
pub impl @moondb.AsyncStmt for SqlServerStmt with fn execute(self) {
  self.check_open()
  self.ensure_plan()
  let response = self.conn.run_plan(self.handle, self.params)
  { rows_affected: response.rows_affected, last_insert_id: 0L, }
}

///|
pub impl @moondb.AsyncStmt for SqlServerStmt with fn query(self) {
  self.check_open()
  self.ensure_plan()
  let response = self.conn.run_plan(self.handle, self.params)
  if response.sets.length() == 0 {
    return []
  }
  response.sets[0]
}

///|
pub impl @moondb.AsyncStmt for SqlServerStmt with fn close(self) {
  if self.closed {
    return
  }
  self.closed = true
  if self.handle != 0 {
    let handle = self.handle
    self.handle = 0
    self.conn.unprepare(handle) catch {
      _ => ()
    }
  }
}

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

///|
/// The statement binds positionally: `?` becomes `@pN` when the text travels, and no slot
/// has a name of its own.
pub impl @moondb.AsyncStmt for SqlServerStmt with fn params(_self) {
  []
}

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