///|
/// Backend contract error
pub enum BackendError {
  InvalidArgument(message~ : String)
  NotFound(message~ : String)
} derive(Show, Eq)

///|
/// Backend capability flags
pub struct BackendCapabilities {
  navigation : Bool
  evaluate : Bool
  element_lookup : Bool
  input_actions : Bool
  screenshot : Bool
} derive(Show, Eq)

///|
/// Backend contract for protocol behavior
pub struct Backend {
  navigate : (String, String, String) -> Result[NavigationResult, BackendError]
  evaluate : (String, String, Bool) -> Result[Json, BackendError]
  locate_nodes : (String, Json, Int?) -> Result[
    Array[NodeReference],
    BackendError,
  ]
  perform_actions : (String?, Array[Json]) -> Result[Unit, BackendError]
  release_actions : (String?) -> Result[Unit, BackendError]
  capture_screenshot : (String, String?) -> Result[String, BackendError]
  capabilities : () -> BackendCapabilities
}

///|
/// Create backend contract object
pub fn Backend::new(
  navigate : (String, String, String) -> Result[NavigationResult, BackendError],
  evaluate : (String, String, Bool) -> Result[Json, BackendError],
  locate_nodes : (String, Json, Int?) -> Result[
    Array[NodeReference],
    BackendError,
  ],
  perform_actions : (String?, Array[Json]) -> Result[Unit, BackendError],
  release_actions : (String?) -> Result[Unit, BackendError],
  capture_screenshot : (String, String?) -> Result[String, BackendError],
  capabilities : () -> BackendCapabilities,
) -> Backend {
  {
    navigate,
    evaluate,
    locate_nodes,
    perform_actions,
    release_actions,
    capture_screenshot,
    capabilities,
  }
}

///|
/// In-memory backend state
pub struct InMemoryBackendState {
  mut next_navigation_id : Int
  contexts : Map[String, String]
  action_log : Array[String]
}

///|
/// Minimal in-memory backend
pub struct InMemoryBackend {
  state : Ref[InMemoryBackendState]
}

///|
/// Create in-memory backend
pub fn InMemoryBackend::new(
  initial_contexts? : Array[String],
) -> InMemoryBackend {
  let contexts : Map[String, String] = {}
  let context_ids = match initial_contexts {
    Some(values) => values
    None => ["ctx-1"]
  }
  for context_id in context_ids {
    contexts[context_id] = "about:blank"
  }
  { state: Ref::new({ next_navigation_id: 1, contexts, action_log: [] }) }
}

///|
/// Export in-memory backend as contract
pub fn InMemoryBackend::to_backend(self : InMemoryBackend) -> Backend {
  let navigate = fn(
    context_id : String,
    url : String,
    _wait : String,
  ) -> Result[NavigationResult, BackendError] {
    if context_id.is_empty() {
      return Err(InvalidArgument(message="context must not be empty"))
    }
    if url.is_empty() {
      return Err(InvalidArgument(message="url must not be empty"))
    }
    if !(self.state.val.contexts.contains(context_id)) {
      return Err(NotFound(message="context not found: " + context_id))
    }
    let nav_id = self.state.val.next_navigation_id
    self.state.val.next_navigation_id = nav_id + 1
    self.state.val.contexts[context_id] = url
    Ok({ navigation: "nav-" + nav_id.to_string(), url })
  }
  let evaluate = fn(
    context_id : String,
    expression : String,
    _await_promise : Bool,
  ) -> Result[Json, BackendError] {
    if !(self.state.val.contexts.contains(context_id)) {
      return Err(NotFound(message="context not found: " + context_id))
    }
    if expression == "1 + 1" {
      return Ok(
        Json::object({
          "type": Json::string("number"),
          "value": Json::number(2.0),
        }),
      )
    }
    if expression == "document.title" {
      return Ok(
        Json::object({
          "type": Json::string("string"),
          "value": Json::string("mock-title"),
        }),
      )
    }
    Ok(
      Json::object({
        "type": Json::string("string"),
        "value": Json::string(expression),
      }),
    )
  }
  let locate_nodes = fn(
    context_id : String,
    locator : Json,
    max_node_count : Int?,
  ) -> Result[Array[NodeReference], BackendError] {
    if !(self.state.val.contexts.contains(context_id)) {
      return Err(NotFound(message="context not found: " + context_id))
    }
    let node_count = match max_node_count {
      Some(count) =>
        if count <= 0 {
          return Err(
            InvalidArgument(message="maxNodeCount must be greater than 0"),
          )
        } else {
          count
        }
      None => 1
    }
    let locator_type = match get_json_string(locator, "type") {
      Some(value) => value
      None => return Err(InvalidArgument(message="locator.type is required"))
    }
    if locator_type == "css" || locator_type == "xpath" {
      match get_json_string(locator, "value") {
        Some(_) => ()
        None =>
          return Err(
            InvalidArgument(
              message="locator.value must be a string for " +
                locator_type +
                " locator",
            ),
          )
      }
    }
    let nodes : Array[NodeReference] = []
    for i in 0.. Result[Unit, BackendError] {
    if actions.length() == 0 {
      return Err(InvalidArgument(message="actions must not be empty"))
    }
    let context_label = match context_id {
      Some(id) => id
      None => "global"
    }
    self.state.val.action_log.push(
      "perform:" + context_label + ":" + actions.length().to_string(),
    )
    Ok(())
  }
  let release_actions = fn(context_id : String?) -> Result[Unit, BackendError] {
    let context_label = match context_id {
      Some(id) => id
      None => "global"
    }
    self.state.val.action_log.push("release:" + context_label)
    Ok(())
  }
  let capture_screenshot = fn(
    context_id : String,
    _format : String?,
  ) -> Result[String, BackendError] {
    if !(self.state.val.contexts.contains(context_id)) {
      return Err(NotFound(message="context not found: " + context_id))
    }
    Ok("mock-screenshot:" + context_id)
  }
  let capabilities = fn() -> BackendCapabilities {
    {
      navigation: true,
      evaluate: true,
      element_lookup: true,
      input_actions: true,
      screenshot: true,
    }
  }
  Backend::new(
    navigate, evaluate, locate_nodes, perform_actions, release_actions, capture_screenshot,
    capabilities,
  )
}

///|
/// Snapshot helper for in-memory contexts
pub fn InMemoryBackend::get_context_url(
  self : InMemoryBackend,
  context_id : String,
) -> String? {
  self.state.val.contexts.get(context_id)
}

///|
/// Snapshot helper for in-memory action history size
pub fn InMemoryBackend::action_log_length(self : InMemoryBackend) -> Int {
  self.state.val.action_log.length()
}

///|
fn get_json_string(json : Json, key : String) -> String? {
  match json {
    Object(map) =>
      match map.get(key) {
        Some(String(value)) => Some(value)
        _ => None
      }
    _ => None
  }
}