///| Node.js adapter helpers for Sol.

///|
/// Mars app type used by Sol.
pub type App = @mars.Server

///|
priv struct MarsFetchHandler((@mars.JsRequest) -> @js.Promise[@mars.JsResponse])

///|
extern "js" fn ffi_convert_fetch_handler(handler : @js.Any) -> MarsFetchHandler =
  #| (handler) => handler

///|
extern "js" fn ffi_export_sol_app(fetch_handler : MarsFetchHandler) -> Unit =
  #| (fetchHandler) => { globalThis.__SOL_APP__ = { fetch: fetchHandler }; }

///|
extern "js" fn ffi_is_adapter_mode() -> Bool =
  #| () => globalThis.__SOL_ADAPTER_MODE__ === true ||
  #|   (typeof process !== 'undefined' && process.env?.SOL_ADAPTER === "1")

///|
extern "js" fn ffi_node_port_from_env(default_port : Int) -> Int =
  #| (defaultPort) => {
  #|   const raw = typeof process !== 'undefined' ? process.env?.PORT : undefined;
  #|   const port = Number.parseInt(raw || String(defaultPort), 10);
  #|   return Number.isFinite(port) ? port : defaultPort;
  #| }

///|
extern "js" fn ffi_js_string_literal(value : String) -> String =
  #| (value) => JSON.stringify(value)

///|
extern "js" fn ffi_init_node_fs() -> @js.Promise[Unit] =
  #| () => {
  #|   const importer = globalThis.__SOL_DYNAMIC_IMPORT__ ||
  #|     ((specifier) => import(specifier));
  #|   return importer('node:fs').then(fs => { globalThis.__SOL_FS__ = fs; });
  #| }

///|
extern "js" fn ffi_serve(fetch_handler : MarsFetchHandler, port : Int) -> Unit =
  #| (fetchHandler, port) => {
  #|   const importer = globalThis.__SOL_DYNAMIC_IMPORT__ ||
  #|     ((specifier) => import(specifier));
  #|   importer('@hono/node-server').then(({ serve }) => {
  #|     serve({ fetch: fetchHandler, port });
  #|   });
  #| }

///|
/// Read the Node server port from `process.env.PORT`, defaulting to 7777.
pub fn node_port_from_env(default? : Int = 7777) -> Int {
  ffi_node_port_from_env(default)
}

///|
fn default_node_port_from_env() -> Int {
  node_port_from_env()
}

///|
/// Export a Sol app for generated JS adapter wrappers.
pub fn export_app(app : App) -> Unit {
  let fetch_handler = app.to_fetch_handler()
  ffi_export_sol_app(ffi_convert_fetch_handler(@js.any(fetch_handler)))
}

///|
/// Start a Sol app with `@hono/node-server`.
pub fn serve(app : App, port? : Int) -> Unit {
  let selected_port = port.unwrap_or_else(default_node_port_from_env)
  println("Server running at http://localhost:" + selected_port.to_string())
  let fetch_handler = app.to_fetch_handler()
  ffi_serve(ffi_convert_fetch_handler(@js.any(fetch_handler)), selected_port)
}

///|
fn with_initialized_node_fs(callback : () -> Unit) -> Unit {
  let _ = ffi_init_node_fs().then(fn(_) {
    callback()
    @js.Promise::resolve(@js.any(()))
  })
}

///|
/// Run a configured Sol app on Node, or only export it in adapter mode.
pub fn run(configure : (App) -> Unit, port? : Int) -> Unit {
  with_initialized_node_fs(fn() {
    let app = @mars.Server::new()
    configure(app)
    export_app(app)
    if !ffi_is_adapter_mode() {
      serve(app, port?)
    }
  })
}

///|
/// Run a Sol app factory on Node, or only export it in adapter mode.
pub fn run_app(create_and_configure : () -> App, port? : Int) -> Unit {
  with_initialized_node_fs(fn() {
    let app = create_and_configure()
    export_app(app)
    if !ffi_is_adapter_mode() {
      serve(app, port?)
    }
  })
}

///|
/// Generate a Node.js local-serve wrapper for an already generated Sol main module.
pub fn generate_node_serve_js(main_module : String) -> String {
  let buf = StringBuilder::new()
  let main_module_literal = ffi_js_string_literal(main_module)
  buf.write_string("// Node.js adapter for local serving\n")
  buf.write_string("// Generated by: sol_adapter_node\n\n")
  buf.write_string("process.env.SOL_ADAPTER = '1';\n\n")
  buf.write_string("const app = await import(\{main_module_literal})")
  buf.write_string(".then(m => m.default || globalThis.__SOL_APP__);\n")
  buf.write_string(
    "if (!app) throw new Error('Sol app was not exported by the server module');\n\n",
  )
  buf.write_string("const { serve } = await import('@hono/node-server');\n\n")
  buf.write_string("const port = parseInt(process.env.PORT || '7777', 10);\n")
  buf.write_string(
    "console.log(`Server running at http://localhost:${port}`);\n",
  )
  buf.write_string("serve({ fetch: app.fetch, port });\n")
  buf.to_string()
}