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

///|
/// A synchronous `@moondb.Driver` over MySQL — the adapter that lets a moondb
/// query layer (moonorm) talk to MySQL through the same contract as the SQLite
/// and Postgres drivers.
///
/// ## The async wall, and what it means here
///
/// moonbitlang/async sockets are asynchronous, but `@moondb.Driver`'s methods
/// are synchronous, and MoonBit forbids calling an `async` function from a
/// non-`async` one. The bridge is `@async.run_async_main`, which spins an event
/// loop, runs an async body to completion, and returns — the same hook the
/// toolchain uses to enter `async fn main`.
///
/// A socket does **not** survive across two such event loops (each tears its
/// file descriptors down; verified empirically), so every `execute`/`query`
/// opens a fresh connection, authenticates, runs its one statement, and closes.
/// That is correct and efficient enough for autocommit work — which is exactly
/// what the moondb roundtrip is — but it means a transaction cannot be held open
/// across separate `Driver` calls. `begin`/`commit`/`rollback` therefore raise a
/// clear error directing callers to the asynchronous [`MysqlConn`] API, which
/// owns its socket for its whole lifetime and brackets real transactions. This
/// is a language constraint, not a shortcut, and it is documented in the README.
pub struct MysqlDriver {
  host : String
  port : Int32
  user : String
  password : String
  database : String
  tls : @tls.Client?
}

///|
/// Configure a driver. `host` defaults to loopback, `port` to 3306; `password`
/// and `database` may be empty. `tls` turns on TLS for the connection.
pub fn MysqlDriver::new(
  user~ : String,
  password~ : String = "",
  database~ : String = "",
  host~ : String = "127.0.0.1",
  port~ : Int32 = 3306,
  tls? : @tls.Client,
) -> MysqlDriver {
  { host, port, user, password, database, tls, }
}

///|
/// Translate any wire-level or socket error raised inside the event loop onto
/// the coarse `@moondb.DbError` the contract speaks.
fn to_db_error(e : Error) -> @moondb.DbError {
  match e {
    @moondb.Closed => @moondb.Closed
    @moondb.ConnectError(m) => @moondb.ConnectError(m)
    @moondb.QueryError(m) => @moondb.QueryError(m)
    @moondb.TypeError(m) => @moondb.TypeError(m)
    @moondb.Server(d) => @moondb.Server(d)
    @moonmysql.ServerError(code, state, msg) =>
      @moondb.Server(@moondb.ServerDetail::{
        kind: classify_mysql(code, state),
        sqlstate: if state == "" {
          None
        } else {
          Some(state)
        },
        code: Some(code),
        message: msg,
        detail: None,
        hint: None,
        severity: None,
      })
    @moonmysql.ProtocolError(_) | @moonmysql.UnsupportedError(_) =>
      @moondb.QueryError(e.to_string())
    _ => @moondb.ConnectError(e.to_string())
  }
}

///|
/// Classify a MySQL ERR packet. A specific SQLSTATE names the class directly;
/// MySQL's generic `HY000` (and an empty state, as older servers send) carry no
/// refinement, so the errno decides there.
fn classify_mysql(code : Int32, state : String) -> @moondb.ErrorKind {
  if state != "" && state != "HY000" {
    return @moondb.classify_sqlstate(state)
  }
  match code {
    1022 | 1048 | 1062 | 1169 | 1216 | 1217 | 1451 | 1452 => @moondb.Integrity
    1040 | 1041 | 1045 | 1203 | 1205 | 1213 | 1698 | 2006 | 2013 =>
      @moondb.Operational
    1044 | 1049 | 1054 | 1064 | 1146 | 1149 => @moondb.Programming
    1264 | 1265 | 1366 | 1406 => @moondb.Data
    1235 | 1295 => @moondb.NotSupported
    _ => @moondb.Database
  }
}

///|
/// Open a fresh connection, run `op` against it, close it, and surface the
/// result (or a mapped `DbError`) back on the synchronous side of the async wall.
fn[T] MysqlDriver::with_conn(
  self : MysqlDriver,
  op : async (MysqlConn) -> T,
) -> T raise @moondb.DbError {
  let slot : Ref[Result[T, @moondb.DbError]] = Ref(Err(@moondb.Closed))
  @async.run_async_main(() => {
    @async.with_task_group(_g => {
      slot.val = try {
        let conn = MysqlConn::connect(
          self.host,
          self.port,
          self.user,
          self.password,
          self.database,
          tls?=self.tls,
        )
        defer conn.close()
        Ok(op(conn))
      } catch {
        e => Err(to_db_error(e))
      }
    })
  })
  match slot.val {
    Ok(v) => v
    Err(e) => raise e
  }
}

///|
const TXN_UNSUPPORTED : String = "the synchronous @moondb.Driver adapter is autocommit-only: MoonBit's async wall makes each call open a fresh connection, so a transaction cannot span Driver calls — use the async MysqlConn API (begin/commit/rollback) for real transactions (see README)"

///|
pub impl @moondb.Driver for MysqlDriver with fn execute(
  self : MysqlDriver,
  sql : String,
  params : Array[@moondb.Value],
) -> @moondb.ExecResult raise @moondb.DbError {
  self.with_conn(conn => conn.execute(sql, params))
}

///|
pub impl @moondb.Driver for MysqlDriver with fn query(
  self : MysqlDriver,
  sql : String,
  params : Array[@moondb.Value],
) -> Array[@moondb.Row] raise @moondb.DbError {
  self.with_conn(conn => conn.query(sql, params))
}

///|
pub impl @moondb.Driver for MysqlDriver with fn begin(_self : MysqlDriver) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError(TXN_UNSUPPORTED)
}

///|
pub impl @moondb.Driver for MysqlDriver with fn commit(_self : MysqlDriver) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError(TXN_UNSUPPORTED)
}

///|
pub impl @moondb.Driver for MysqlDriver with fn rollback(_self : MysqlDriver) -> Unit raise @moondb.DbError {
  raise @moondb.QueryError(TXN_UNSUPPORTED)
}

///|
pub impl @moondb.Driver for MysqlDriver with fn close(_self : MysqlDriver) -> Unit {
  // Reconnect-per-call keeps no socket open between calls; nothing to release.

}

///|

///|
/// This driver speaks MySQL: positional `?`, savepoints, unsigned 64-bit columns
/// that keep their value above 2^63-1, more than one result set per statement, and
/// no `RETURNING`.
pub impl @moondb.Driver for MysqlDriver with fn dialect(_self) {
  @moondb.Dialect::Mysql
}

///|
pub extend MysqlDriver with @moondb.Driver::{
  execute,
  begin,
  ping,
  close,
  query_stream,
  commit,
  query,
  rollback,
  dialect,
  capabilities,
  prepare,
}

// The async seam. `MysqlConn` owns its socket for its whole lifetime — unlike
// `MysqlDriver`, which reconnects per call — so it is the connection that
// supports real transactions and belongs in an async pool.

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn execute(
  self : MysqlConn,
  sql : String,
  params : Array[@moondb.Value],
) -> @moondb.ExecResult raise @moondb.DbError {
  MysqlConn::execute(self, sql, params) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn query(
  self : MysqlConn,
  sql : String,
  params : Array[@moondb.Value],
) -> Array[@moondb.Row] raise @moondb.DbError {
  MysqlConn::query(self, sql, params) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn query_stream(
  self : MysqlConn,
  sql : String,
  params : Array[@moondb.Value],
) -> &@moondb.AsyncCursor raise @moondb.DbError {
  MysqlConn::query_stream(self, sql, params) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn begin(self : MysqlConn) -> Unit raise @moondb.DbError {
  MysqlConn::begin(self) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn commit(self : MysqlConn) -> Unit raise @moondb.DbError {
  MysqlConn::commit(self) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn rollback(self : MysqlConn) -> Unit raise @moondb.DbError {
  MysqlConn::rollback(self) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncDriver for MysqlConn with fn close(self : MysqlConn) -> Unit {
  MysqlConn::close(self)
}

///|
pub impl @moondb.AsyncCursor for MysqlRowStream with fn next(
  self : MysqlRowStream,
) -> @moondb.Row? raise @moondb.DbError {
  // The stream's own `next` reports wire failures as `MysqlError`; the seam
  // speaks `DbError`, so a transport/decoding failure is mapped at the boundary.
  MysqlRowStream::next(self) catch {
    e => raise to_db_error(e)
  }
}

///|
pub impl @moondb.AsyncCursor for MysqlRowStream with fn close(
  self : MysqlRowStream,
) -> Unit {
  // Closing never surfaces a failure: the caller is done with the result either
  // way, and a half-read connection is dropped by `release`'s reset.
  MysqlRowStream::close(self) catch {
    _ => ()
  }
}

///|

///|
/// This connection speaks MySQL.
pub impl @moondb.AsyncDriver for MysqlConn with fn dialect(_self) {
  @moondb.Dialect::Mysql
}

///|
/// Prepare the statement on this connection and hand back the handle. A prepared
/// statement lives on the connection that owns the socket, which is why this is the
/// async path and not the reconnect-per-call adapter's.
pub impl @moondb.AsyncDriver for MysqlConn with fn prepare(self, sql) {
  self.prepare_stmt(sql) catch {
    e => raise to_db_error(e)
  }
}

///|
pub extend MysqlConn with @moondb.AsyncDriver::{
  ping,
  dialect,
  capabilities,
  prepare,
}