///|
/// WebDriver BiDi WebSocket Server (Deno backend)
///
/// Uses Deno.serve with WebSocket upgrade for BiDi protocol.

///|
/// BiDi server configuration
pub(all) struct BidiServerConfig {
  host : String
  port : Int
  /// Optional auth token. If set, clients must connect to /session/.
  /// If empty, authentication is disabled (legacy behavior).
  auth_token : String
}

///|
/// Default configuration (generates a random auth token)
pub fn BidiServerConfig::default() -> BidiServerConfig {
  { host: "127.0.0.1", port: 9222, auth_token: generate_auth_token() }
}

///|
pub fn BidiServerConfig::web_socket_url(self : BidiServerConfig) -> String {
  if self.auth_token.is_empty() {
    "ws://\{self.host}:\{self.port}"
  } else {
    "ws://\{self.host}:\{self.port}/session/\{self.auth_token}"
  }
}

///|
/// Generate a random auth token using crypto.randomUUID
extern "js" fn js_generate_auth_token() -> String =
  #| () => {
  #|   try {
  #|     return crypto.randomUUID().replace(/-/g, "");
  #|   } catch (_e) {
  #|     return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
  #|   }
  #| }

///|
pub fn generate_auth_token() -> String {
  js_generate_auth_token()
}

///|
/// Extract URL pathname from a request
extern "js" fn js_get_request_pathname(request : @core.Any) -> String =
  #| (req) => {
  #|   try {
  #|     return new URL(req.url).pathname;
  #|   } catch (_e) {
  #|     return "/";
  #|   }
  #| }

///|
/// BiDi server state
pub struct BidiServer {
  config : BidiServerConfig
  priv protocol : BidiProtocol
  mut server : @core.Any?
}

///|
/// Create new BiDi server
pub fn BidiServer::new(
  config? : BidiServerConfig = BidiServerConfig::default(),
) -> BidiServer {
  { config, protocol: BidiProtocol::new(), server: None }
}

///|
/// Validate auth token from request path.
/// Expected path: /session/ when auth is enabled.
fn BidiServer::validate_auth(self : BidiServer, request : @core.Any) -> Bool {
  let token = self.config.auth_token
  if token.is_empty() {
    return true // Auth disabled
  }
  let path = js_get_request_pathname(request)
  path == "/session/" + token
}

///|
/// Deno.upgradeWebSocket FFI
extern "js" fn upgrade_websocket(request : @core.Any) -> @core.Any =
  #| (req) => Deno.upgradeWebSocket(req)

///|
/// Env flag helper for optional debug tracing.
extern "js" fn env_flag_enabled(name : String) -> Bool =
  #| (name) => {
  #|   try {
  #|     return typeof Deno !== "undefined" && Deno.env.get(name) === "1";
  #|   } catch (_e) {
  #|     return false;
  #|   }
  #| }

///|
/// Console log helper for optional server tracing.
extern "js" fn js_console_log(message : String) -> Unit =
  #| (message) => console.log(message)

///|
/// Convert JS value to string for debug logs.
extern "js" fn js_value_to_string(value : @core.Any) -> String =
  #| (value) => {
  #|   try {
  #|     if (value && typeof value === "object") {
  #|       const msg = value.message ?? value.reason ?? value.type;
  #|       if (msg !== undefined) return String(msg);
  #|     }
  #|     return String(value);
  #|   } catch (_e) {
  #|     return "[unprintable]";
  #|   }
  #| }

///|
/// Debug trace hook (disabled unless CRATER_BIDI_SERVER_TRACE=1).
fn bidi_server_trace(message : String) -> Unit {
  if env_flag_enabled("CRATER_BIDI_SERVER_TRACE") {
    js_console_log("[bidi-server] " + message)
  }
}

///|
/// Check if request is WebSocket upgrade
fn is_websocket_request(request : @core.Any) -> Bool {
  let headers : @core.Any = request._get("headers")
  let upgrade_any = headers._call("get", [@core.any("upgrade")])
  // Check if upgrade header exists (not null/undefined)
  if @core.is_nullish(upgrade_any) {
    return false
  }
  let upgrade : String = upgrade_any.cast()
  upgrade.to_lower() == "websocket"
}

///|
/// Handle WebSocket message, returns all response/event messages
fn BidiServer::handle_message(
  self : BidiServer,
  data : String,
) -> Array[String] {
  match self.protocol.process_message(data) {
    Ok(_) => {
      let messages = self.protocol.take_messages()
      let result : Array[String] = []
      for msg in messages {
        result.push(msg.to_json())
      }
      result
    }
    Err(e) =>
      [
        "{\"type\":\"error\",\"error\":\"unknown error\",\"message\":\"" +
        e +
        "\"}",
      ]
  }
}

///|
/// Setup WebSocket event handlers with async support
fn BidiServer::setup_websocket(self : BidiServer, socket : @core.Any) -> Unit {
  reset_runtime_js_state()
  reset_paint_provider()
  bidi_server_trace("setup_websocket: begin reset")
  self.protocol.reset_session_runtime_state()
  bidi_server_trace("setup_websocket: reset done")
  // Store socket reference for async responses
  let socket_epoch = self.protocol.set_socket(socket)
  bidi_server_trace("setup_websocket: socket epoch=" + socket_epoch.to_string())

  // onopen
  socket["onopen"] = @core.any(
    @core.from_fn1(fn(_event : @core.Any) {
      // Connection opened, could log or initialize
      bidi_server_trace("ws onopen")
    }),
  )

  // onmessage - process BiDi messages (supports async via socket)
  socket["onmessage"] = @core.any(
    @core.from_fn1(fn(event : @core.Any) {
      bidi_server_trace("ws onmessage: received")
      let raw_data = event._get("data")
      if @core.is_nullish(raw_data) {
        bidi_server_trace("ws onmessage: nullish data")
        return
      }
      let data : String = raw_data.cast()
      // handle_message returns sync responses; async responses sent via socket
      let messages = self.handle_message(data)
      bidi_server_trace(
        "ws onmessage: responses=" + messages.length().to_string(),
      )
      for msg in messages {
        socket._call("send", [@core.any(msg)]) |> ignore
      }
    }),
  )

  // onerror
  socket["onerror"] = @core.any(
    @core.from_fn1(fn(event : @core.Any) {
      // Error handling
      bidi_server_trace("ws onerror: " + js_value_to_string(event))
    }),
  )

  // onclose
  socket["onclose"] = @core.any(
    @core.from_fn1(fn(_event : @core.Any) {
      // Connection closed
      bidi_server_trace("ws onclose: epoch=" + socket_epoch.to_string())
      if self.protocol.is_current_socket_epoch(socket_epoch) {
        bidi_server_trace("ws onclose: reset current epoch")
        self.protocol.reset_session_runtime_state()
        self.protocol.clear_socket()
        bidi_server_trace("ws onclose: reset done")
      }
    }),
  )
}

///|
/// Create HTTP response for non-WebSocket requests
extern "js" fn create_response(body : String, status : Int) -> @core.Any =
  #| (body, status) => new Response(body, { status, headers: { "Content-Type": "application/json" } })

///|
fn bidi_status_response_body(config : BidiServerConfig) -> String {
  let auth_enabled = if config.auth_token.is_empty() { "false" } else { "true" }
  let web_socket_url = escape_json_string(config.web_socket_url())
  "{\"ready\":true,\"message\":\"Crater WebDriver BiDi Server\",\"webSocketUrl\":\"" +
  web_socket_url +
  "\",\"authEnabled\":" +
  auth_enabled +
  "}"
}

///|
/// Start the BiDi server using Deno.serve
pub fn BidiServer::start(self : BidiServer) -> Unit {
  let d = @deno.deno()

  // Create request handler
  let handler = @core.from_fn1(fn(request : @core.Any) -> @core.Any {
    if is_websocket_request(request) {
      // Security: validate auth token before upgrading
      if !self.validate_auth(request) {
        let body = "{\"error\":\"unauthorized\",\"message\":\"Invalid or missing auth token\"}"
        return create_response(body, 403)
      }
      // Upgrade to WebSocket
      let upgrade_result = upgrade_websocket(request)
      let socket : @core.Any = upgrade_result._get("socket")
      let response : @core.Any = upgrade_result._get("response")
      self.setup_websocket(socket)
      response
    } else {
      // Return status response for HTTP requests
      let body = bidi_status_response_body(self.config)
      create_response(body, 200)
    }
  })

  // Start server with Deno.serve
  let opts : @core.Any = @core.from_entries([
    ("port", @core.any(self.config.port)),
    ("hostname", @core.any(self.config.host)),
  ]).cast()

  // Call Deno.serve with options and handler
  self.server = Some(d.as_any()._call("serve", [opts, @core.any(handler)]))
}

///|
/// Stop the server
pub fn BidiServer::stop(self : BidiServer) -> Unit {
  match self.server {
    Some(srv) => {
      srv._call("shutdown", []) |> ignore
      self.server = None
    }
    None => ()
  }
}