///|
/// Opaque handle to the running WebSocket server and its client registry.
#external
pub type NodeWsRuntime

///|
/// Write a timestamped line to `.runtime-monitor/server.log` and echo to stdout.
///
/// The log directory is created automatically. Useful for persistent
/// server-side diagnostics without a separate logging framework.
pub extern "js" fn server_log(text : String) -> Unit =
  #| (text) => {
  #|   const fs = require("fs");
  #|   const path = require("path");
  #|   const dir = path.join(process.cwd(), ".runtime-monitor");
  #|   if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  #|   const file = path.join(dir, "server.log");
  #|   fs.appendFileSync(file, new Date().toISOString() + "  " + text + "\n");
  #|   console.log("[ws-server] " + text);
  #| }

///|
extern "js" fn _start_ws_server(
  port : Int,
  path : String,
  on_open : (String) -> Unit,
  on_message : (String, String) -> Unit,
  on_close : (String) -> Unit,
) -> NodeWsRuntime =
  #| (port, path, onOpen, onMessage, onClose) => {
  #|   const crypto = require("crypto");
  #|   const { WebSocketServer } = require("ws");
  #|   const clients = new Map();
  #|   const wss = new WebSocketServer({ port, path });
  #|   wss.on("connection", (socket) => {
  #|     const sid = crypto.randomUUID();
  #|     clients.set(sid, socket);
  #|     onOpen(sid);
  #|     socket.on("message", (buf) => onMessage(sid, String(buf)));
  #|     socket.on("close", () => { clients.delete(sid); onClose(sid); });
  #|   });
  #|   wss.on("listening", () =>
  #|     console.log(`[ws-server] listening on ws://localhost:${port}${path}`)
  #|   );
  #|   return { wss, clients };
  #| }

///|
extern "js" fn _send_ws(
  runtime : NodeWsRuntime,
  sid : String,
  text : String,
) -> Unit =
  #| (rt, sid, text) => {
  #|   const socket = rt.clients.get(sid);
  #|   if (socket && socket.readyState === 1) socket.send(text);
  #| }

///|
/// Start a WebSocket server on `port` at `path` with fully typed JSON messages.
///
/// - `on_open(sid)` — called when a client connects; `sid` is a UUID
/// - `on_message(sid, msg)` — called with the already-parsed `In` value;
///   JSON parse errors are silently dropped
/// - `on_close(sid)` — called when a client disconnects
///
/// Returns a typed send function `(sid, msg) -> Unit`. The function serializes
/// `msg` to JSON internally, so callers never touch raw text.
/// Keep the returned value alive to prevent GC of the server runtime.
///
/// Both `port` and `path` are caller-supplied — nothing is hardcoded.
///
/// ## Example
///
/// ```moonbit nocheck
/// let send = run_json_server(
///   5022,
///   "/ws",
///   fn(sid) { log("connect \{sid}") },
///   fn(sid, op : MyClientOp) { handle(sid, op) },
///   fn(sid) { log("disconnect \{sid}") },
/// )
/// // later: send(sid, my_server_event)
/// ```
pub fn[In : @json.FromJson, Out : ToJson] run_json_server(
  port : Int,
  path : String,
  on_open : (String) -> Unit,
  on_message : (String, In) -> Unit,
  on_close : (String) -> Unit,
) -> (String, Out) -> Unit {
  let runtime = _start_ws_server(
    port,
    path,
    on_open,
    fn(sid, text) {
      try {
        let msg : In = @json.from_json(@json.parse(text))
        on_message(sid, msg)
      } catch {
        _ => ()
      }
    },
    on_close,
  )
  fn(sid : String, msg : Out) {
    _send_ws(runtime, sid, msg.to_json().stringify())
  }
}