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

///|
/// A statement MySQL prepares once and runs many times.
///
/// `prepare` sends `COM_STMT_PREPARE` and keeps the server-side handle; every
/// `execute`/`query` sends only the statement id and the values, which is both
/// cheaper than re-parsing and the path that binds binary payloads without hex
/// expansion. `close` sends `COM_STMT_CLOSE`, after which the handle raises `Closed`.
///
/// A `:name` statement is rewritten to MySQL's `?` spelling at prepare time and the
/// slot names are kept, so `bind_named` works without the caller counting anything.
/// A handle belongs to the connection that prepared it: one wire, one statement at a
/// time.
pub struct MysqlStmt {
  conn : MysqlConn
  id : Int32
  text : String
  slots : Array[String]
  mut params : Array[@moondb.Value]
  mut closed : Bool
}

///|
/// Ask the server to prepare `sql` and return the handle.
pub async fn MysqlConn::prepare_stmt(
  self : MysqlConn,
  sql : String,
) -> MysqlStmt {
  if self.closed {
    raise @moondb.Closed
  }
  let named = @moondb.rewrite_named(sql, @moondb.Dialect::Mysql)
  let ok = self.compile(named.sql)
  if !named.slots.is_empty() && ok.num_params != named.slots.length() {
    self.stmt_close(ok.id)
    raise @moonmysql.ProtocolError(
      "the server counted " +
      ok.num_params.to_string() +
      " parameters, the rewrite produced " +
      named.slots.length().to_string(),
    )
  }
  {
    conn: self,
    id: ok.id,
    text: sql,
    slots: named.slots,
    params: [],
    closed: false,
  }
}

///|
/// Raise `Closed` once the handle has been released.
fn MysqlStmt::ensure_open(self : MysqlStmt) -> Unit raise @moondb.DbError {
  if self.closed {
    raise @moondb.Closed
  }
}

///|
impl @moondb.AsyncStmt for MysqlStmt with fn bind(self, params) {
  self.ensure_open()
  self.params = params
}

///|
/// Run the statement as a non-row statement and report rows changed. A statement
/// that answers with rows is refused, the same way `execute` refuses one.
impl @moondb.AsyncStmt for MysqlStmt with fn execute(self) {
  MysqlStmt::run_execute(self) catch {
    e => raise to_db_error(e)
  }
}

///|
/// The wire half of `execute`, raising the driver's own error type.
async fn MysqlStmt::run_execute(self : MysqlStmt) -> @moondb.ExecResult {
  self.ensure_open()
  self.conn.send_execute(self.id, self.params)
  let first = self.conn.read_packet()
  if @moonmysql.is_err_packet(first) {
    raise @moonmysql.parse_err(first)
  }
  if !@moonmysql.is_ok_packet(first) {
    raise @moonmysql.ProtocolError(
      "execute() was given a row-returning statement; use query()",
    )
  }
  let ok = @moonmysql.parse_ok(first)
  { rows_affected: ok.affected_rows, last_insert_id: ok.last_insert_id, }
}

///|
/// Run the statement and materialise every row from the binary protocol.
impl @moondb.AsyncStmt for MysqlStmt with fn query(self) {
  MysqlStmt::run_query(self) catch {
    e => raise to_db_error(e)
  }
}

///|
/// The wire half of `query`, raising the driver's own error type.
async fn MysqlStmt::run_query(self : MysqlStmt) -> Array[@moondb.Row] {
  self.ensure_open()
  self.conn.read_prepared_rows(self.id, self.params)
}

///|
impl @moondb.AsyncStmt for MysqlStmt with fn close(self) {
  if !self.closed {
    self.closed = true
    self.conn.stmt_close(self.id)
  }
}

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

///|
impl @moondb.AsyncStmt for MysqlStmt with fn params(self) {
  self.slots
}