src/libsql_benchmark.gleam

import gleam/dynamic/decode
import gleam/int
import gleam/io
import gleam/result
import libsql

pub fn main() {
  io.println("=== LibSQL Benchmark ===")
  io.println("")

  let _ = benchmark_local_operations()
  io.println("")

  let _ = benchmark_remote_operations()
  io.println("")

  let _ = benchmark_replica_operations()
  io.println("")

  let _ = benchmark_synced_operations()
  io.println("")

  io.println("=== Benchmark Complete ===")
  Nil
}

fn benchmark_local_operations() {
  io.println("--- Local Database Benchmarks ---")

  let assert Ok(conn) = libsql.open(":memory:")

  benchmark_exec(conn)
  benchmark_insert(conn)
  benchmark_select(conn)
  benchmark_batch_insert(conn)
  benchmark_prepared_statements(conn)
  benchmark_transactions(conn)

  let assert Ok(_) = libsql.close(conn)
}

fn print_result(label: String, iterations: Int, elapsed: Int) {
  let qps = case elapsed {
    0 -> 0
    _ -> {iterations * 1000} / elapsed
  }
  io.println(label <> ": " <> int.to_string(iterations) <> " in " <> int.to_string(elapsed) <> "ms (" <> int.to_string(qps) <> " qps)")
}

fn print_result_rows(label: String, rows: Int, elapsed: Int) {
  let qps = case elapsed {
    0 -> 0
    _ -> {rows * 1000} / elapsed
  }
  io.println(label <> ": " <> int.to_string(rows) <> " rows in " <> int.to_string(elapsed) <> "ms (" <> int.to_string(qps) <> " rows/sec)")
}

fn benchmark_exec(conn) {
  let iterations = 1000
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.exec("select 1", conn)
  })

  let elapsed = now_ms() - start
  print_result("exec('select 1')", iterations, elapsed)
}

fn benchmark_insert(conn) {
  let assert Ok(_) = libsql.exec("create table bench_insert (id integer primary key, value text)", conn)

  let iterations = 5000
  let start = now_ms()

  let _ = run_times(iterations, fn(i) {
    let sql = "insert into bench_insert (value) values ('row_" <> int.to_string(i) <> "')"
    libsql.exec(sql, conn)
  })

  let elapsed = now_ms() - start
  print_result("insert", iterations, elapsed)
}

fn benchmark_select(conn) {
  let assert Ok(_) = libsql.exec("create table bench_select (id integer primary key, value text)", conn)

  let _ = run_times(1000, fn(i) {
    let sql = "insert into bench_select (value) values ('row_" <> int.to_string(i) <> "')"
    libsql.exec(sql, conn)
  })

  let iterations = 500
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.query(
      "select value from bench_select where id = ?",
      conn,
      [libsql.int(500)],
      decode.field(0, decode.string, decode.success),
    )
  })

  let elapsed = now_ms() - start
  print_result("select with param", iterations, elapsed)
}

fn benchmark_batch_insert(conn) {
  let assert Ok(_) = libsql.exec("create table bench_batch (id integer primary key, name text, score real)", conn)

  let batches = generate_batches(1000)
  let iterations = 10
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.exec_batch(
      "insert into bench_batch (name, score) values (?, ?)",
      conn,
      batches,
    )
  })

  let elapsed = now_ms() - start
  let total_rows = iterations * 1000
  print_result_rows("batch insert", total_rows, elapsed)
}

fn generate_batches(count: Int) -> List(List(libsql.Value)) {
  generate_batches_loop(count, [])
}

fn generate_batches_loop(n: Int, acc: List(List(libsql.Value))) -> List(List(libsql.Value)) {
  case n {
    0 -> acc
    _ -> {
      let batch = [libsql.text("name_" <> int.to_string(n)), libsql.float(int.to_float(n))]
      generate_batches_loop(n - 1, [batch, ..acc])
    }
  }
}

fn benchmark_prepared_statements(conn) {
  let assert Ok(_) = libsql.exec("create table bench_prepared (id integer primary key, name text)", conn)

  let assert Ok(stmt) = libsql.prepare("insert into bench_prepared (name) values (?)", conn)

  let iterations = 5000
  let start = now_ms()

  let _ = run_times(iterations, fn(i) {
    libsql.exec_prepared(stmt, [libsql.text("name_" <> int.to_string(i))])
  })

  let _ = libsql.finalize(stmt)

  let elapsed = now_ms() - start
  print_result("prepared insert", iterations, elapsed)
}

fn benchmark_transactions(conn) {
  let assert Ok(_) = libsql.exec("create table bench_tx (id integer primary key, name text)", conn)

  let iterations = 100
  let start = now_ms()

  let _ = run_times(iterations, fn(i) {
    libsql.transaction(conn, fn() {
      use _ <- result.try(libsql.exec("insert into bench_tx (name) values ('tx_" <> int.to_string(i) <> "_1')", conn))
      use _ <- result.try(libsql.exec("insert into bench_tx (name) values ('tx_" <> int.to_string(i) <> "_2')", conn))
      use _ <- result.try(libsql.exec("insert into bench_tx (name) values ('tx_" <> int.to_string(i) <> "_3')", conn))
      Ok(Nil)
    })
  })

  let elapsed = now_ms() - start
  print_result("transaction (3 inserts)", iterations, elapsed)
}

fn benchmark_remote_operations() {
  io.println("--- Remote Database Benchmarks ---")

  let url = "libsql://test-rednithin.aws-ap-south-1.turso.io"
  let token = "DUMMY_TOKEN"

  case libsql.open_remote(url, token) {
    Ok(conn) -> {
      // Clean up tables from previous runs to keep remote state small
      let _ = libsql.exec("drop table if exists bench_remote", conn)
      let _ = libsql.exec("drop table if exists bench_synced", conn)

      let _ = benchmark_remote_exec(conn)
      let _ = benchmark_remote_select(conn)
      let _ = libsql.close(conn)
      Ok(Nil)
    }
    Error(err) -> {
      io.println("Remote benchmark skipped: " <> err.message)
      Error(err)
    }
  }
}

fn benchmark_remote_exec(conn) {
  let iterations = 10
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.exec("select 1", conn)
  })

  let elapsed = now_ms() - start
  print_result("remote exec('select 1')", iterations, elapsed)
}

fn benchmark_remote_select(conn) {
  let assert Ok(_) = libsql.exec("drop table if exists bench_remote", conn)
  let assert Ok(_) = libsql.exec("create table bench_remote (id integer primary key, value text)", conn)
  let _ = run_times(10, fn(i) {
    libsql.exec("insert into bench_remote (value) values ('row_" <> int.to_string(i) <> "')", conn)
  })

  let iterations = 20
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.query(
      "select value from bench_remote where id = ?",
      conn,
      [libsql.int(5)],
      decode.field(0, decode.string, decode.success),
    )
  })

  let elapsed = now_ms() - start
  print_result("remote select", iterations, elapsed)

  let assert Ok(_) = libsql.exec("drop table bench_remote", conn)
}

fn benchmark_replica_operations() {
  io.println("--- Replica Database Benchmarks ---")

  let url = "libsql://test-rednithin.aws-ap-south-1.turso.io"
  let token = "DUMMY_TOKEN"
  let db_path = "/tmp/libsql_benchmark_replica.db"

  case libsql.open_replica(db_path, url, token) {
    Ok(conn) -> {
      let _ = benchmark_replica_sync(conn)
      let _ = benchmark_replica_local_query(conn)
      let _ = libsql.close(conn)
      Ok(Nil)
    }
    Error(err) -> {
      io.println("Replica benchmark skipped: " <> err.message)
      Error(err)
    }
  }
}

fn benchmark_replica_sync(conn) {
  let iterations = 1
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.sync(conn)
  })

  let elapsed = now_ms() - start
  print_result("replica sync", iterations, elapsed)
}

fn benchmark_replica_local_query(conn) {
  let _ = libsql.sync(conn)

  let iterations = 500
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.query(
      "select 1",
      conn,
      [],
      decode.field(0, decode.int, decode.success),
    )
  })

  let elapsed = now_ms() - start
  print_result("replica local query", iterations, elapsed)
}

fn benchmark_synced_operations() {
  io.println("--- Synced Database Benchmarks ---")

  let url = "libsql://test-rednithin.aws-ap-south-1.turso.io"
  let token = "DUMMY_TOKEN"
  let db_path = "/tmp/libsql_benchmark_synced.db"

  case libsql.open_synced_database(db_path, url, token) {
    Ok(conn) -> {
      // Initial sync to pull remote state
      let _ = libsql.sync(conn)

      let _ = benchmark_synced_local_insert(conn)
      let _ = benchmark_synced_local_query(conn)
      let _ = benchmark_synced_push(conn)

      // Clean up
      let _ = libsql.exec("drop table if exists bench_synced", conn)
      let _ = libsql.close(conn)
      Ok(Nil)
    }
    Error(err) -> {
      io.println("Synced benchmark skipped: " <> err.message)
      Error(err)
    }
  }
}

fn benchmark_synced_local_insert(conn) {
  let assert Ok(_) = libsql.exec("drop table if exists bench_synced", conn)
  let assert Ok(_) = libsql.exec("create table bench_synced (id integer primary key, name text)", conn)

  let iterations = 1000
  let start = now_ms()

  let _ = run_times(iterations, fn(i) {
    libsql.exec("insert into bench_synced (name) values ('name_" <> int.to_string(i) <> "')", conn)
  })

  let elapsed = now_ms() - start
  print_result("synced local insert", iterations, elapsed)
}

fn benchmark_synced_local_query(conn) {
  let iterations = 500
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.query(
      "select name from bench_synced where id = ?",
      conn,
      [libsql.int(500)],
      decode.field(0, decode.string, decode.success),
    )
  })

  let elapsed = now_ms() - start
  print_result("synced local query", iterations, elapsed)
}

fn benchmark_synced_push(conn) {
  let iterations = 3
  let start = now_ms()

  let _ = run_times(iterations, fn(_) {
    libsql.sync(conn)
  })

  let elapsed = now_ms() - start
  print_result("synced push", iterations, elapsed)
}

fn run_times(n: Int, f: fn(Int) -> a) -> Nil {
  case n {
    0 -> Nil
    _ -> {
      let _ = f(n)
      run_times(n - 1, f)
    }
  }
}

@external(erlang, "erlang", "monotonic_time")
fn monotonic_time() -> Int

@external(erlang, "erlang", "convert_time_unit")
fn convert_time_unit(time: Int, from: a, to: b) -> Int

fn now_ms() -> Int {
  let time = monotonic_time()
  convert_time_unit(time, 1_000_000_000, 1_000)
}