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

///|
/// A transaction that nests: the outermost [`run`](#run) opens a real transaction,
/// every nested one opens a savepoint, and an error undoes only the work of the
/// level that raised.
///
/// `Driver::begin` / `commit` / `rollback` are flat, which is all a backend has to
/// offer, but application code nests: a service method opens a transaction, calls a
/// repository method that opens one too, and the inner failure must not discard the
/// outer work. `Txn` closes the bracket around a body so the pairing cannot be
/// forgotten, and derives the savepoint name from the nesting depth so two levels
/// never collide.
///
/// ```moonbit check
/// test "a nested body that fails keeps the outer work" {
///   let db = @moondb.MockDriver::new()
///   let tx = @moondb.Txn::new(db)
///   tx.run(fn(tx) raise @moondb.DbError {
///     tx
///     .driver()
///     .execute("INSERT INTO hero VALUES (?, ?)", [
///       @moondb.Value::Int(1),
///       @moondb.Value::Text("Nova"),
///     ])
///     |> ignore
///     // The inner level fails; only its own work is undone.
///     (tx.run(fn(_tx) raise @moondb.DbError {
///       raise @moondb.QueryError("inner failed")
///     }) catch {
///       _ => ()
///     })
///     |> ignore
///   })
///   assert_eq(db.row_count(), 1)
/// }
/// ```
pub struct Txn[T] {
  driver : T
  mut depth : Int32
}

///|
/// A transaction handle over `driver`, at depth zero and not yet open.
pub fn[T] Txn::new(driver : T) -> Txn[T] {
  { driver, depth: 0, }
}

///|
/// The connection this handle opens transactions on.
pub fn[T] Txn::driver(self : Txn[T]) -> T {
  self.driver
}

///|
/// How many bodies are currently open, zero when none is.
pub fn[T] Txn::depth(self : Txn[T]) -> Int32 {
  self.depth
}

///|
/// Whether a body is currently open.
pub fn[T] Txn::is_open(self : Txn[T]) -> Bool {
  self.depth > 0
}

///|
/// Run `body` inside a transaction, or inside a savepoint when a transaction is
/// already open. Returning normally commits (or releases the savepoint); raising
/// rolls back (or rolls back to the savepoint) and re-raises the original error.
///
/// A body that catches an inner failure and carries on keeps the outer work, which
/// is the point of the savepoint: only the level that failed is undone.
pub fn[T : Driver] Txn::run(
  self : Txn[T],
  body : (Txn[T]) -> Unit raise DbError,
) -> Unit raise DbError {
  let level = self.depth
  if level == 0 {
    self.driver.begin()
  } else {
    self.driver.execute("SAVEPOINT " + savepoint_name(level), []) |> ignore
  }
  self.depth = level + 1
  let outcome = try {
    body(self)
    Ok(())
  } catch {
    e => Err(e)
  }
  self.depth = level
  match outcome {
    Ok(_) =>
      if level == 0 {
        self.driver.commit()
      } else {
        self.driver.execute("RELEASE SAVEPOINT " + savepoint_name(level), [])
        |> ignore
      }
    Err(e) => {
      if level == 0 {
        self.driver.rollback()
      } else {
        // ROLLBACK TO keeps the savepoint alive so it can still be released; both
        // statements run so the savepoint stack stays balanced with `depth`.
        self.driver.execute("ROLLBACK TO SAVEPOINT " + savepoint_name(level), [])
        |> ignore
        self.driver.execute("RELEASE SAVEPOINT " + savepoint_name(level), [])
        |> ignore
      }
      raise e
    }
  }
}

///|
/// The savepoint name for a nesting level, unique per level and stable across a
/// rollback so the matching `ROLLBACK TO` finds it.
fn savepoint_name(level : Int32) -> String {
  "moondb_sp_" + level.to_string()
}

///|
/// The asynchronous counterpart of [`Txn`], for a backend reached over a socket.
///
/// ```moonbit nocheck
/// let tx = @moondb.AsyncTxn::new(conn)
/// tx.run(async fn(tx) {
///   tx.driver().execute("INSERT INTO hero VALUES ($1, $2)", [Int(1), Text("Nova")])
///   |> ignore
/// })
/// ```
pub struct AsyncTxn[T] {
  driver : T
  mut depth : Int32
}

///|
/// An async transaction handle over `driver`, at depth zero and not yet open.
pub fn[T] AsyncTxn::new(driver : T) -> AsyncTxn[T] {
  { driver, depth: 0, }
}

///|
/// The connection this handle opens transactions on.
pub fn[T] AsyncTxn::driver(self : AsyncTxn[T]) -> T {
  self.driver
}

///|
/// How many bodies are currently open, zero when none is.
pub fn[T] AsyncTxn::depth(self : AsyncTxn[T]) -> Int32 {
  self.depth
}

///|
/// Run `body` inside a transaction, or inside a savepoint when one is already open.
/// Same contract as [`Txn::run`], with every round trip awaited.
pub async fn[T : AsyncDriver] AsyncTxn::run(
  self : AsyncTxn[T],
  body : async (AsyncTxn[T]) -> Unit raise DbError,
) -> Unit raise DbError {
  let level = self.depth
  if level == 0 {
    self.driver.begin()
  } else {
    self.driver.execute("SAVEPOINT " + savepoint_name(level), []) |> ignore
  }
  self.depth = level + 1
  let outcome = try {
    body(self)
    Ok(())
  } catch {
    e => Err(e)
  }
  self.depth = level
  match outcome {
    Ok(_) =>
      if level == 0 {
        self.driver.commit()
      } else {
        self.driver.execute("RELEASE SAVEPOINT " + savepoint_name(level), [])
        |> ignore
      }
    Err(e) => {
      if level == 0 {
        self.driver.rollback()
      } else {
        self.driver.execute("ROLLBACK TO SAVEPOINT " + savepoint_name(level), [])
        |> ignore
        self.driver.execute("RELEASE SAVEPOINT " + savepoint_name(level), [])
        |> ignore
      }
      raise e
    }
  }
}